253 lines
11 KiB
Text
253 lines
11 KiB
Text
= Forge
|
|
Scott Duensing <scott@kangaroopunch.com>
|
|
:revnumber: 3.00
|
|
:revdate: 2026
|
|
:doctype: book
|
|
:toc: left
|
|
:toclevels: 3
|
|
:sectnums:
|
|
:sectnumlevels: 3
|
|
:source-highlighter: rouge
|
|
:icons: font
|
|
:experimental:
|
|
|
|
[preface]
|
|
== About Forge
|
|
|
|
Forge is Singe's authoring tool: a way to make a game by describing it --
|
|
placing things, attaching behaviours, writing rules -- and having ordinary
|
|
Singe Lua written for you. It is itself a Singe game and is distributed on
|
|
its own, beside the engine rather than inside it. This document covers the
|
|
description format, the vocabulary, the compiler and the editor. The engine
|
|
calls the generated code makes (`playerNew`, `collidePointRect`,
|
|
`onKeyPressed`, `scriptPush` and the rest) are documented in the Singe Manual.
|
|
|
|
== Describing a Game Instead of Writing One
|
|
|
|
A game can be written as a *description* -- a table of layers, entities and
|
|
rules -- and compiled into an ordinary Singe game.
|
|
`Forge/AuthorCompile.singe` does the compiling and `Author.singe` is the runtime
|
|
the result calls. **No part of Forge ships with Singe** -- not the editor, not
|
|
the compiler, not the runtime -- so a game built with it carries its own copy of
|
|
that runtime and stands entirely on its own. Nothing is interpreted at run time: the rules
|
|
become real Lua `if` statements, so a description costs nothing per frame on a
|
|
Raspberry Pi, and the game it produces can be opened, read and edited by hand
|
|
like any other.
|
|
|
|
There is no notion of genre anywhere in it. A game declares which of the
|
|
engine's own layers it uses, and that is the only difference between a light
|
|
gun game, a platformer and a quick-time event over video.
|
|
|
|
=== The three nouns
|
|
|
|
*Layers* are what the game draws through: `world2d` (physics in the XY plane,
|
|
drawn into the overlay), `overlay` (flat drawing over everything) and `disc`
|
|
(the video the game is played over). A game lists the ones it wants.
|
|
|
|
*Entities* are things on a layer. Each has a position, a `look` (`box`,
|
|
`sprite` or `text`) and any number of behaviours. Every entity is a node,
|
|
whether or not the 3D scene is drawing, which is what lets a 2D game built this
|
|
way run on a machine with no GPU.
|
|
|
|
*Behaviours* are bundles over engine calls that already exist: `platformer` is
|
|
the character controller (`playerNew` and friends), `solid` is a
|
|
static body, `drift` moves at a constant velocity. Attaching one is a line in
|
|
the description rather than code.
|
|
|
|
*Rules* are conditions and actions. Every rule is tested every frame, in the
|
|
order written, and all of a rule's conditions must hold for its actions to run.
|
|
|
|
.A description, in full
|
|
[source,lua]
|
|
----
|
|
return {
|
|
title = "One rule",
|
|
layers = { { kind = "world2d", gravity = 1500 } },
|
|
entities = {
|
|
{ id = "ground", x = 360, y = 440,
|
|
look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
|
|
behaviours = { { kind = "solid" } } },
|
|
{ id = "hero", x = 120, y = 380,
|
|
look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
|
|
behaviours = { { kind = "platformer", speed = 210, jump = 620 } } }
|
|
},
|
|
rules = {
|
|
{ note = "Run right",
|
|
when = { { "keyHeld", key = "RIGHT" } },
|
|
act = { { "run", entity = "hero", direction = 1 } } }
|
|
}
|
|
}
|
|
----
|
|
|
|
Compile it and run what comes out:
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Forge/AuthorCompile.singe")
|
|
dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe"))
|
|
----
|
|
|
|
`testScripts/author/platformer.game` and `testScripts/author/qte.game` are
|
|
worked examples, and `testScripts/scene52.singe` and `scene53.singe` compile
|
|
and play them.
|
|
|
|
=== The vocabulary, and adding to it
|
|
|
|
Conditions and actions are not built into the compiler. Each is an entry in
|
|
the `AUTHOR` table declaring its parameters and the Lua it emits, so a new kind
|
|
of game is a set of entries rather than a new release. The conditions today
|
|
are `keyHeld`, `switchHeld`, `timeBetween`, `discBetween`, `onGround`,
|
|
`touching`, `below`, `flagSet` and `once`; the actions are `run`, `jump`,
|
|
`moveTo`, `setText`, `show`, `addScore`, `setFlag`, `discTo` and `lua`.
|
|
|
|
`once` deserves a word. Rules run every frame, so anything that should happen
|
|
a single time -- a door opening, a score awarded -- needs it:
|
|
|
|
[source,lua]
|
|
----
|
|
{ when = { { "touching", entity = "hero", other = "prize" },
|
|
{ "once", tag = "prize" } },
|
|
act = { { "addScore", amount = 100 },
|
|
{ "show", entity = "prize", visible = false } } }
|
|
----
|
|
|
|
=== The way out
|
|
|
|
The `lua` action takes a line of Lua and emits it as it stands. It is there on
|
|
purpose: when a rule needs something the vocabulary cannot say, that rule drops
|
|
to Lua and the rest of the game is unaffected. A description is a convenience,
|
|
not a cage, and the compiled output is a normal game you can stop describing
|
|
and start editing whenever it suits you.
|
|
|
|
=== The editor
|
|
|
|
`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
|
|
its own directory beside the games, appears in the menu like one, and packs to
|
|
`Forge.game` with `--pack`; the build does that itself (the `forge` target),
|
|
with this manual inside.
|
|
Nothing in it is a preview: the canvas is the same overlay at the same
|
|
coordinates the game will be played in, so what is placed is what is seen.
|
|
|
|
Started from the menu it opens on a chooser: the descriptions in its data
|
|
directory, any dropped into the `Forge` directory itself (those are opened as a
|
|
copy, since inside a `.game` they are read only), and *New game*, which writes
|
|
a starter -- ground, a hero that runs and jumps, a score readout -- and opens
|
|
it. The first run copies this manual, `Forge.pdf`, out of `Forge.game` into
|
|
that data directory, and the chooser says where it is. `Esc` in the editor closes the description (twice, when it has unsaved
|
|
changes) and `Esc` on the chooser leaves Forge. `P` plays: the description is
|
|
saved, compiled beside a copy of the runtime, and handed to the engine with
|
|
`scriptPush`; when the game ends Forge comes back on the same file.
|
|
|
|
A script can drive the editor instead, which is how the test scenes do it:
|
|
|
|
[source,lua]
|
|
----
|
|
FORGE_LIBRARY = true
|
|
dofile("Forge/Forge.singe")
|
|
forgeBegin("mygame.game")
|
|
|
|
function onOverlayUpdate()
|
|
local x, y = mouseGetPosition(0)
|
|
|
|
forgeDraw(x, y)
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
The entity list and the details are an RmlUi document; the canvas beside them is
|
|
drawn into the overlay and picked with `collidePointRect`.
|
|
The two compose because the engine offers a button to the GUI first and passes
|
|
on what it did not use, while pointer motion is never consumed at all -- so
|
|
point `forgePress`, `forgeDrag` and `forgeRelease` at the mouse
|
|
callbacks and clicks on the panels will not reach the canvas.
|
|
|
|
`forgeSave()` writes the description back, `forgeBuild(path)`
|
|
compiles what is on screen (and puts the runtime beside it, so the result
|
|
plays), and `forgeMove(index, x, y)` moves an entity without a pointer, which
|
|
is how `testScripts/scene54.singe` drives it.
|
|
|
|
=== Editing the entities
|
|
|
|
`A` adds a box in the middle of the canvas, `D` duplicates the selected entity
|
|
a little to one side, and `Delete` removes it. `Enter` walks the entity's
|
|
fields the same way it walks a rule's parameters: `id`, `x`, `y`, the look's
|
|
`kind`, then whatever that look takes, then `behaviours` -- typed as a list of
|
|
kinds, `platformer, solid` -- and then each behaviour's own parameters as
|
|
`platformer.speed` and so on. The fields come from the `AUTHOR` manifest, so a
|
|
new look or behaviour is editable the moment it is declared. Renaming an
|
|
entity renames it in every rule that talks about it. A `sprite` look is drawn
|
|
with its image once the file name is right, and as a box until then.
|
|
|
|
From a script, `forgeEntityAdd(x, y)`, `forgeEntityDuplicate()`,
|
|
`forgeEntityDelete()` and `forgeEntitySet(entity, field, value)` do the same.
|
|
|
|
=== Editing the rules
|
|
|
|
The panel shows either the entities or the event sheet; `Tab` swaps them. In
|
|
the rules, the selected rule opens in place and its conditions and actions are
|
|
listed under it, because a rule only means anything whole -- a `when` without a
|
|
`then` tells you nothing.
|
|
|
|
Point `onKeyPressed` at `forgeKey` and the whole editor
|
|
works without a pointer, which is how the bundled menu has always been driven
|
|
and what a cabinet wants:
|
|
|
|
[cols="1,4"]
|
|
|===
|
|
| `Tab` | entities or rules
|
|
| Up, Down | move through the list, and through the parts of the open rule
|
|
| Left, Right | slide the panel
|
|
| `Enter` | type a value for whatever is selected; again for its next value
|
|
| `Esc` | put the value back
|
|
| `A`, `D`, `Delete` | add, duplicate, delete the selected entity
|
|
| `N` | a new rule
|
|
| `C`, `T` | add a condition, an action -- picked from the vocabulary, with its help beside it
|
|
| `[`, `]` | move the rule up or down the sheet
|
|
| `Delete` | delete the selected rule, or the selected condition or action
|
|
| `U`, `R` | undo and redo, forty steps deep; a drag or a typed form is one step, and a change by hand ends the redo history
|
|
| `S`, `B`, `P` | save, build, play
|
|
|===
|
|
|
|
With the rule itself selected rather than one of its parts, `Enter` edits its
|
|
note.
|
|
|
|
A typed number comes back a number rather than a string, because `100` and
|
|
`"100"` compile to different source and a description that changed shape when a
|
|
value was retyped would stop round-tripping.
|
|
|
|
From a script, `forgeRuleNew(note)`, `forgeRuleAdd("when"|"act",
|
|
name)`, `forgePartSet(key, value)`, `forgePartDelete()`, `forgeRuleMove(by)`,
|
|
`forgeUndo()` and `forgeRedo()` do the same work. Adding a condition selects it, so its
|
|
parameters can be set at once.
|
|
|
|
The vocabulary comes from the `AUTHOR` manifest, so the rule editor never needs
|
|
changing when a condition or an action is added: it offers whatever is
|
|
declared. A new parameter gets a sensible starting value for its type, so a
|
|
rule compiles the moment it is made rather than only once every field is
|
|
filled in.
|
|
|
|
A description survives the round trip: loading one, saving it and loading it
|
|
again compiles to the same game, byte for byte. The editor depends on that and
|
|
the test asserts it.
|
|
|
|
=== Releasing a game
|
|
|
|
`forgeExport(folder, name)` writes everything a finished game needs into a
|
|
directory of its own: the compiled script, a `games.dat` so the menu lists it,
|
|
the description it was built from so it can be opened again, and **a copy of
|
|
the runtime**, taken out of Forge. `--pack` turns that directory into a `.game`
|
|
like any other, and it runs on a machine that has never had Forge on it.
|
|
|
|
Every built game finds its own directory to load that runtime from, using
|
|
`debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the
|
|
engine was *launched* with, so a game reached by `dofile` -- a test, a
|
|
launcher, a preview -- would otherwise look beside the caller.
|
|
|
|
The panel slides. Drag the tab on its outer edge and it moves across the
|
|
window, so an entity that lives underneath it -- a score readout at `12, 12`
|
|
does -- is never permanently out of reach. `forgePanelTo(x)` moves it from
|
|
a script, and `forgeOverPanel(x)` says whether a point is currently
|
|
covered.
|
|
|
|
[#migrating]
|