singe/docs/Forge.adoc
2026-09-14 22:32:53 -05:00

1757 lines
86 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:
:imagesdir: images
[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.
== Tutorials
Ten sittings, each ending with a game that plays, each needing only the one
before it. They use the *kit* -- pictures, sounds, a clip, and a model --
that travels inside `Forge.game` and is named from a game as
`Forge/kit/hero.png`; `Forge/kit/README.txt` lists it. Every tutorial has a
scene in `testScripts/` that presses the same keys the text names and checks
the result against the description the tutorial ends with, which ships in
`Forge/tutorials/` and appears in the chooser, so a reader who loses the
thread can open the finished game and compare. The pictures come from those
scenes too.
=== Tutorial 1: Ten minutes
image::tutorials/01-playing.png[The starter game, playing, 600]
**What you will make.** Nothing new -- the game Forge starts you with, played,
changed, saved, and played again. Ten minutes, and you know where everything
is.
**What you need.** Forge, started from the Singe menu. No files.
**Steps.**
. Start Forge. It opens on the *chooser*: a list of the descriptions in
its data directory (none yet), the samples and tutorials that travel inside
`Forge.game`, and *New game* at the bottom. The line under the list says
where `Forge.pdf` -- this book -- was put, the first time Forge ran.
+
image::tutorials/01-chooser.png[The chooser, 600]
. Press kbd:[Down] until *New game* is lit, then kbd:[Enter]. Forge writes a
starter description into its data directory and opens it. On the left is
the *panel*; everything to its right is the *canvas*, which is the game's
own screen at the game's own coordinates. A hero stands on a strip of
ground, and a readout sits in the corner.
+
image::tutorials/01-entities.png[The entities panel, 600]
. The panel is on *Entities* -- the things placed in this room, each named
`id (type)`. Under the list are the fields of whatever is selected; with
nothing selected, the room's own. The last line of that box is Forge's
*message*: it says what just happened, and while you type, what you are
typing.
. Press kbd:[Tab]. The panel turns to *Types*: what an entity can be. The
starter has three -- `ground`, `hero`, `readout` -- each with a small
picture of its look. Below are the lit type's fields: its look, the
look's size and colour, its vars, and its behaviours.
+
image::tutorials/01-types.png[The types panel, 600]
. Press kbd:[Tab] again for *Rules*, the sheet that says what happens. Each
rule is a line; the lit one is opened to show what it is *on*, *when* it
applies, and what it does *then*. Two more kbd:[Tab]s pass *Tracks* and
*Dialogues*, empty for now, and come back round to *Entities*.
+
image::tutorials/01-rules.png[The rules panel, 600]
. Back on *Entities*, press the hero on the canvas, or its row in the list.
It gets a yellow frame, and the box under the list shows its fields:
`id`, `type`, `x`, `y`, and the rest. Press kbd:[Enter]. The message
says *type a value for id*, and the id shows selected in its row. Press
kbd:[Enter] again until it is *x* that is selected -- or click the `x`
row -- type `200`, and press kbd:[Enter]; the hero moves as you type.
Then kbd:[Esc] to leave the form. A field passed over with nothing typed
keeps its value, and the arrows put a caret into a value to change part
of it.
. kbd:[Tab] to *Types*, kbd:[Down] to `hero` (or click it), and
kbd:[Enter] through the form the same way: `look.r` to `90`, `look.b` to `240` -- the hero turns
blue as you type -- and `platformer.speed` to `260`. kbd:[Esc] when the
last is in.
+
image::tutorials/01-recoloured.png[The hero recoloured, 600]
. Press kbd:[S]. The star after the title goes: the description is saved,
as a file you could open in a text editor.
. Press kbd:[P]. The game is built and played: kbd:[Left] and kbd:[Right]
run, kbd:[Space] jumps, and it is a little faster than it was. kbd:[Esc]
ends the game, and Forge comes back on the same description.
**What happened.** A game in Forge is a *description*: layers, types, rooms
of entities, and rules. The editor only edits that description, and
kbd:[P] compiles it into an ordinary Singe game and runs it. Nothing you saw
is special to the editor: the panels are the description's parts, the canvas
is the game's screen, and the file kbd:[S] wrote is the whole game. The
types and the rest are explained in
<<_describing_a_game_instead_of_writing_one>>; the keys are all in
<<_keys>>.
**Try this.**
* Make the hero jump higher: `platformer.jump` in the types panel.
* Give the ground a colour of its own.
* Drag the hero somewhere else with the mouse, and play again.
**The description.** What kbd:[S] wrote, in full. `Forge/tutorials/01-tenMinutes.game`
in the chooser is this file.
[source,lua]
----
include::../assets/Forge/tutorials/01-tenMinutes.game[]
----
=== Tutorial 2: A platformer
image::tutorials/02-playing.png[Coins, spikes, and a door, 600]
**What you will make.** The starter game with something to do: coins to
collect, spikes that send the hero back, and a door to a second room.
**What you need.** Tutorial 1, or its finished description
(`01-tenMinutes` in the chooser). From the kit: `coin.png`, `spike.png`,
`door.png`, `coin.wav`, and `hit.wav`.
**Steps.**
. Press kbd:[2] for the *Types* panel. (The digits kbd:[1] to kbd:[5] go
straight to a panel; kbd:[Tab] walks them in turn.) Press kbd:[A]: a
type called `thing` appears, lit, a plain grey box.
. kbd:[Enter] opens its form on `name`. Type `coin`, kbd:[Enter]. The next
field is `look`: type `sprite`, kbd:[Enter]. The look's own fields
follow; kbd:[Enter] past `look.anchor` and `look.faces` to `look.file`,
which opens a list of the files under the game's directory and in the kit.
Type `coin` to narrow it, and kbd:[Enter] takes `Forge/kit/coin.png`.
kbd:[Enter] on to `behaviours`, type `trigger`, kbd:[Enter], then
kbd:[Esc]. The type's row now shows the coin.
+
image::tutorials/02-coinType.png[The coin type, 600]
. Press kbd:[1] for *Entities* and kbd:[A]: a coin of the lit type appears
in the middle of the canvas, selected. Drag it down to the ground. Press
kbd:[D] to duplicate it and drag the copy along; kbd:[D] again for a third,
dragged up where a jump will reach it.
+
image::tutorials/02-coins.png[Three coins placed, 600]
. The same way, make a type `spike` from `spike.png` and a type `door` from
`door.png`, each a `trigger`, and each with `look.anchor` set to `feet` so
that it stands on the ground where it is placed rather than being centred
there. Place one of each near the right edge, the door beyond the spikes.
+
image::tutorials/02-placed.png[Spikes and a door, 600]
. Press kbd:[3] for *Rules* and kbd:[N]: a new rule, on `frame`.
kbd:[Enter] and type its `note`, `Take a coin`, kbd:[Enter], kbd:[Esc].
Press kbd:[E] to choose what it is on: type `enter` and kbd:[Enter].
The rule's fields now include `a` and `b`, the trigger and what entered
it: kbd:[Enter] to `a`, type `coin`; kbd:[Enter] to `b`, type `hero`;
kbd:[Enter], kbd:[Esc].
. Press kbd:[T] for an action: type `addScore`, kbd:[Enter]. It appears
under the rule, lit, and kbd:[Enter] opens its `amount`: type `10`,
kbd:[Enter], kbd:[Esc]. kbd:[T] again for `destroy`; its `entity` is
already `self`, which in an `enter` rule is the trigger -- the coin.
kbd:[T] once more for `playSound`; kbd:[Enter] opens the file list, type
`coin.wav`, kbd:[Enter].
+
image::tutorials/02-rule.png[The coin rule, 600]
. Two more rules the same way. `Spikes hurt`, on `enter` with `a` `spike`
and `b` `hero`: a `moveTo` with `entity` `hero`, `x` `200`, and `y` `380`,
and a `playSound` of `hit.wav`. `Through the door`, on `enter` with `a`
`door` and `b` `hero`: a `goTo` with `room` `cave`, `entity` `hero`, `x`
`60`, and `y` `380`.
. The cave does not exist yet. Press kbd:[1] and then kbd:[N]: a second
room, empty, and the panel shows it (`room 2 of 2`). With nothing
selected -- click the game's name at the top of the panel, or an empty
spot on the canvas -- the form is the game's and then the room's:
kbd:[Enter] to `name`, type `cave`,
kbd:[Enter], kbd:[Esc]. Press kbd:[2], light `ground` with the arrows,
kbd:[1], kbd:[A], and drag it to the bottom; the same for a `readout` in
the corner and a `coin`. kbd:[PgUp] and kbd:[PgDn] move between rooms.
+
image::tutorials/02-cave.png[The cave, 600]
. kbd:[S], then kbd:[P]. Run into a coin: it goes, the score climbs, and
the clip plays. Run into the spikes and you are back at the start. Reach
the door and you are in the cave.
**What happened.** A *type* is what a thing is: a look and a list of
behaviours. An *entity* is one placed in a room. The `trigger` behaviour
gives an instance a volume that reports what enters and leaves it, and the
`enter` *event* is what a rule listens for; its `a` and `b` say which two
types, and inside the rule `self` is the trigger and `other` is what came in.
Rooms are separate screens with their own entities, and `goTo` takes an
entity with it. Events and the rest are in <<_events>>; every behaviour and
action is in <<_the_vocabulary>>.
**Try this.**
* Make the spikes cost a point: an `addScore` of `-5` in the spike rule.
* Put a `door` in the cave that leads back, with a second `goTo`.
* Rename an entity in its form: the rules that name it change with it.
**The description.** The rules tutorial 1 left are unchanged; what is new
is the three types, their entities, the three rules, and the second room.
[source,lua]
----
include::../assets/Forge/tutorials/02-platformer.game[]
----
=== Tutorial 3: Sprites and sound
image::tutorials/03-playing.png[The hero from the sheet, 600]
**What you will make.** The same game, with a hero drawn from a sprite
sheet who faces the way he runs, coins that spin, a jump that sounds, and
music.
**What you need.** Tutorial 2. From the kit: `hero.png`, `jump.wav`, and
`loop.ogg`.
**Steps.**
. kbd:[2] for *Types*, and light `hero` with the arrows. kbd:[Enter] to
`look`, type `sprite`, kbd:[Enter]; on to `look.file`, and take
`Forge/kit/hero.png` from the list; on to `look.frames`, type `8`. The
sheet is eight columns: standing, four of walking, a jump, a fall, and a
hit, all facing right. The hero on the canvas is now the first of them.
. kbd:[Enter] on to `behaviours`. It says `platformer`; type
`platformer, frames` -- a comma list, and a behaviour already there keeps
its values. Two new fields follow the platformer's: `frames.fps`, type
`10`; and `frames.states`, type `idle=1-1, walk=2-5, jump=6-6, fall=7-7`
-- each state's run of frames. kbd:[Enter], kbd:[Esc].
+
image::tutorials/03-heroType.png[The hero type, 600]
. Light `coin`. Its `behaviours` become `trigger, spin`, and `spin.rate`
`180` -- degrees a second.
+
image::tutorials/03-coinSpin.png[Coins that spin, 600]
. kbd:[3] for *Rules*. kbd:[Down] to rule 3, `Jump, with ground
underfoot`, and once more onto its first part, `keyHeld`. Press
kbd:[Delete]: the part goes. kbd:[C] for a condition: type
`keyPressed`, kbd:[Enter]; kbd:[Enter] on its `key`, type `SPACE`,
kbd:[Enter], kbd:[Esc]. A press rather than a hold, so what comes next
happens once. kbd:[T], `playSound`, and `jump.wav` from the list.
+
image::tutorials/03-jumpRule.png[The jump rule, 600]
. kbd:[N] for a new rule, `note` `Music`. kbd:[E], and choose
`roomStart`: it fires as a room is entered. kbd:[T], `playMusic`, and
`loop.ogg`.
+
image::tutorials/03-music.png[The music rule, 600]
. kbd:[S], kbd:[P]. The hero walks, and turns to face the way he runs
though the sheet only has him facing right; he jumps and falls on the
frames for it; the coins turn; and the music loops.
**What happened.** A `sprite` look with `frames` is a sheet of that many
columns, and the `frames` behaviour picks a column by the instance's *state*
-- a var every instance has, which the `platformer` sets to `idle`, `walk`,
`jump`, or `fall` as it goes, and a rule can set to anything else with
`setState`. The platformer also sets the var `facing`, and a sprite look is
mirrored while it faces away from its art -- the look's `faces` says which
way that is, right unless told. `spin` turns the var `angle`, and a sprite
turns with it. Sound is an action, `playSound`, with a volume of its own if
you want one, and music is another; the `sound` behaviour plays a clip per
event instead, which suits a type that is hit often. Sheets, facing, and
states are in <<_instances_vars_and_states>> and
<<_the_long_tail_sprite_sheets_tiles_midi_water_soft_bodies>>.
**Try this.**
* Show the hit frame: in the spike rule, a `setState` of `hit` on the hero,
and a `wait` of `0.5` before the `moveTo`. Actions that wait run as a
sequence.
* Make the coins face the other way: `look.faces` `left` on the coin does
nothing, since a coin has no facing, but on the hero it mirrors everything.
* Turn the door on the canvas with its yellow handle, or type its `rz`.
**The description.**
[source,lua]
----
include::../assets/Forge/tutorials/03-sprites.game[]
----
=== Tutorial 4: A shoot-em-up
image::tutorials/04-playing.png[Rocks, 600]
**What you will make.** A game from nothing: a ship on the arrow keys that
fires, rocks that fall from spawners and break up, lives, and a game over.
**What you need.** Nothing but the kit: `ship.png`, `shot.png`,
`rock.png`, `shot.wav`, and `hit.wav`.
**Steps.**
. Open *New game* and clear the starter. kbd:[1], light `hero`,
kbd:[Delete]; the same for `ground`. kbd:[2], light `hero`, kbd:[Delete],
and `ground` -- a type can only go once nothing is placed of it. kbd:[3],
and kbd:[Delete] three times: the running and jumping rules go, leaving
the readout's.
. kbd:[1], and with nothing selected the form is the game's own, then the
room's. kbd:[Enter] on `title`, type `Rocks`; on `vars`, type
`score=0, lives=3`; on `layers`, type `overlay`. The starter's `world2d`
layer goes with its gravity: nothing falls unless a behaviour says so.
+
image::tutorials/04-cleared.png[The game's form, 600]
. kbd:[2], kbd:[A], and the ship: `name` `ship`, `look` `sprite`,
`look.file` `ship.png`, `vars` `health=1`, and `behaviours`
`keys, mover, shooter, health, sound`. Then each behaviour's fields:
`keys.left` `LEFT`, `keys.right` `RIGHT`, `keys.up` `UP`, `keys.down`
`DOWN`, `keys.fire` `SPACE`; `mover.speed` `260` and `mover.clamp` `true`;
`shooter.spawns` `shot`, `shooter.rate` `0.15`, `shooter.offsetY` `-14`;
`health.max` `1` and `health.keep` `true`; `sound.hit` `hit.wav`. A file
field passed with kbd:[Enter] keeps what it has, `(none)` included.
+
image::tutorials/04-ship.png[The ship type, 600]
. Three more types. `shot`: a sprite of `shot.png`, behaviours
`projectile, sound`, `projectile.vy` `-520`, `projectile.life` `3`, and
`sound.spawn` `shot.wav`. `rock`: `rock.png`, vars `health=2`,
behaviours `drift, health, sound`, `drift.vy` `90`, `health.max` `2`,
`sound.death` `hit.wav`. `hive`: a look of `none` and a `spawner` with
`spawner.spawns` `rock`, `spawner.every` `0.6`, `spawner.max` `6`, and
`spawner.total` `12`.
+
image::tutorials/04-types.png[The types, 600]
. Place them: a `ship` at the bottom, and two `hive`s just above the top
edge, one over the ship. A `none` look is drawn on the canvas as an empty
box, so it can still be picked up.
+
image::tutorials/04-placed.png[Placed, 600]
. kbd:[3], and the rules. `A shot lands on a rock`, on `collision` with `a`
`shot` and `b` `rock`: a `destroy` (of `self`, the shot), a `damage` with
`entity` `other` and `amount` `1`, and an `addScore` of `10`. `A rock
dies`, on `death` with `type` `rock`: an `emit` with `count` `24` and a
colour of `255`, `120`, `60`, and an `addScore` of `50`.
+
image::tutorials/04-hitRule.png[The hit rules, 600]
. `A rock rams the ship`, on `collision` with `a` `rock` and `b` `ship`:
`destroy`, and `damage` of `other` by `1`. `The ship is lost`, on `death`
with `type` `ship`: `addVar` of `lives` by `-1`, a `flash` of `255`,
`60`, `60` for `0.3` seconds, a `setVar` of `health` to `1`, and a `moveTo`
back to `360`, `420` -- the ship keeps itself through its death, since
`health.keep` is set. `Out of lives`, on `frame`, when a `test` of
`lives <= 0`: `gameOver`. `A rock slips past the bottom`, on `frame`
with `each` `rock`, when a `test` of `self.y > 500`: `destroy`. And the
readout's `text` becomes `"score " .. score .. " lives " .. lives`.
+
image::tutorials/04-rules.png[The rules, 600]
. kbd:[S], kbd:[P]. Fly, fire, and the rocks break up in sparks. Let three
through onto the ship and the game is over; kbd:[Esc] brings Forge back.
**What happened.** Without a `world2d` layer there is no physics, and the
behaviours move things themselves: `keys` reads a player's keys into the
vars `dx`, `dy`, and `fire`, `mover` moves by them, `shooter` spawns a type
while `fire` is set, `drift` moves steadily, and `projectile` flies until
it leaves the picture. `collision` still fires, from the boxes overlapping.
`health` gives an instance the var `health` and raises `death` at zero;
`spawner` makes a type every so often up to a limit. A rule with `each`
runs once per instance of a type, with that instance as `self`, and a
`test` is any expression over the vars. Expressions are in
<<_expressions>>.
**Try this.**
* Give the rocks a `spin` behaviour.
* A second `shooter` on the ship, `offsetX` `-10` and `10`, for twin guns.
* A `hud` layer instead of the readout: `layers` `overlay, hud`, and a
`hud.document` of your own RmlUi page with elements named for the vars in
`hud.bind`.
**The description.**
[source,lua]
----
include::../assets/Forge/tutorials/04-shmup.game[]
----
=== Tutorial 5: A quick-time event
image::tutorials/05-playing.png[A window answered, 600]
**What you will make.** A game over video: the kit's clip plays, and at
two moments in it a prompt wants a key. In time scores; too late costs a
life. The clip loops, and the game ends when the lives are gone.
**What you need.** The kit's `clip.mkv`: twelve seconds in which a door
opens at three, a target rises at six, and a hand reaches in at nine, with a
clock along the bottom. At 24 frames a second, a moment's frame is its
second times 24.
**Steps.**
. Open *New game* and clear it as in tutorial 4: the hero and ground
entities, then their types, then the three running and jumping rules.
. kbd:[1], nothing selected, and the game's form: `title` `Moments`,
`vars` `score=0, lives=3`, and `layers` `disc, overlay`. A `disc` is the
video a game is played over, and `disc.file` is now a field: kbd:[Enter]
opens the list, take `Forge/kit/clip.mkv`. The canvas shows the clip's
first frame.
+
image::tutorials/05-disc.png[The disc layer, 600]
. Two text types. kbd:[2], kbd:[A], `name` `prompt`, `look` `text`, and
a colour of `255`, `220`, `80`; kbd:[1], kbd:[A], and drag it near the
top. Then `verdict`, `text`, `160`, `255`, `180`, placed under it.
+
image::tutorials/05-prompts.png[The prompts, 600]
. kbd:[3], and three rules for the first window, frames `72` to `120`,
when the door opens. `Window one is open`, on `frame`, when
`discBetween` with `from` `72` and `to` `120`: a `setText` of `prompt` to
`"PRESS!"`. `Window one answered in time`, on `frame`, when
`discBetween` `72` to `120`, `keyPressed` with `key` `SPACE`, and `once`
with `tag` `one`: a `setVar` of `hitone` to `true`, an `addScore` of
`250`, and a `setText` of `verdict` to `"HIT"`. `Window one closed
unanswered`, on `frameReached` with `frame` `120`, when a `test` of
`not hitone`: an `addVar` of `lives` by `-1`, `verdict` to `"MISS"`, and
`prompt` to `""`.
+
image::tutorials/05-window.png[The first window, 600]
. The same three for the second window, `150` to `200`, where the target
rises, wanting `UP` and setting `hittwo`. Then `The clip runs out:
round again`, on `frameReached` at `280`: `hitone` and `hittwo` back to
`false`, `verdict` to `""`, and a `discTo` with `frame` `1`. `Out of
lives`, on `frame`, when `lives <= 0`: `gameOver`. The readout's `text`
becomes `"score " .. score .. " lives " .. lives`.
+
image::tutorials/05-rules.png[The rules, 600]
. kbd:[S], kbd:[P]. The door opens and the prompt appears; kbd:[Space] in
time is a hit. Miss the target's window and a life goes.
**What happened.** A `disc` layer plays a video underneath everything, and
the rules read its *frame*: `discBetween` is true while the disc is inside
a window, `frameReached` fires as it passes a frame, and `discTo` sends it
elsewhere -- the whole of a Dragon's Lair is windows and jumps. `once`
keeps a rule from firing every frame the window is open, and `keyPressed`
is the frame a key goes down, where `keyHeld` is every frame it is down.
`setVar` and `test` on a var of your own are how a rule remembers what
another did. The `branching` behaviour does all of this in one track for a
game of many branches; `Forge/fmv.game` in the chooser is one. In a
released game, `games.dat` names the video, as it does for any Singe game.
**Try this.**
* A third window for the hand at `216` to `264`.
* Show the frame: the readout's text with `.. " frame " .. frame`.
* Make a miss send the disc somewhere: a `discTo` in the closed rule.
**The description.**
[source,lua]
----
include::../assets/Forge/tutorials/05-qte.game[]
----
=== Tutorial 6: A light gun game
image::tutorials/06-playing.png[Draw, 600]
**What you will make.** Mad Dog McCree in miniature: the clip plays, a
target rises, and the player shoots it with the mouse or a light gun. The
hand that reaches in is not to be shot. Ammo runs out and is reloaded by a
shot off the picture.
**What you need.** The kit's `clip.mkv`, `hit.wav`, and `shot.wav`.
**Steps.**
. Open *New game*, clear it as before, and give the game's form `title`
`Draw`, `vars` `score=0, misses=0`, `layers` `disc, overlay`, and
`disc.file` `Forge/kit/clip.mkv`.
. kbd:[2], and three types with a look of `none`. `gun`: `vars` `ammo=6`,
`behaviours` `gun`, `gun.player` `1`, `gun.ammo` `6`, `gun.reload`
`offscreen`. `target`: `vars` `health=1`, `behaviours` `hitbox, health`,
`hitbox.track` `target`, `health.max` `1`. `hand`: `behaviours`
`hitbox`, `hitbox.track` `hand`. Place one of each anywhere; a hitbox's
place comes from its track, not from where it stands.
+
image::tutorials/06-types.png[The types, 600]
. kbd:[4] for *Tracks*, and kbd:[N]: a new track, keyed by frame.
kbd:[Enter], `name` `target`, kbd:[Enter], kbd:[Esc]. Along the bottom of
the canvas is the *timeline* with its cursor: kbd:[.] and kbd:[,] step it
a frame, kbd:[\]] and kbd:[\[] leap ten, and the canvas shows the clip at
that frame. Take it to `150`, where the target has risen, and press
kbd:[K]: a key at that frame, with a box in the middle of the canvas.
kbd:[Enter] through the key's form: `x` `405`, `y` `165`, `w` `90`, `h`
`90` -- or drag the box over the target and pull its corner.
+
image::tutorials/06-firstKey.png[The first key, 600]
. Another key at `210`, the same box, so the target stays hittable while it
is up; between keys the box slides from one to the next, and outside them
there is none. kbd:[N] for a second track, `hand`, with keys at `220` and
`260` of `480`, `157`, `240`, `105`.
+
image::tutorials/06-tracks.png[Two tracks, 600]
. kbd:[3], and the rules. `The target is hit`, on `hit` with `type`
`target`: `addScore` `100`, `damage` of `self` by `1`, and `playSound`
`hit.wav`. `The hand is hit`, on `hit` with `type` `hand`: `addScore`
`-200` and a red `flash`. `A shot at nothing`, on `miss`, when a `test` of
`not event.offscreen`: `addVar` `misses` by `1`, `playSound` `shot.wav`.
`Draw!`, on `frameReached` at `150`: a `say` of `"DRAW!"` for `1` second.
The readout's `text`: `"score " .. score .. " ammo " .. gun.ammo .. " misses " .. misses`.
+
image::tutorials/06-rules.png[The rules, 600]
. kbd:[S], kbd:[P]. Shoot the target as it rises; shoot the hand and pay
for it; when the ammo is gone, shoot off the edge of the picture to
reload.
**What happened.** A `gun` behaviour turns a player's pointer -- the mouse,
or a light gun, which is a mouse to the engine -- into shots: the trigger
raises `hit` on the instance under it, with `self` the thing hit and `other`
the gun, or `miss` with `event.offscreen` saying whether the shot left the
picture. A `hitbox` behaviour takes its shape from a *track* of boxes keyed
by disc frame, which is how a thing in a video can be shot only while it is
there: the boxes are where the thing is in the picture, and the editor
shows the frame under them. Tracks are in <<_tracks>>. What a cabinet
with a light gun needs -- calibration, two guns, the recoil -- is in
<<_arcade_plumbing_branching_video_lives_credits_the_board>>.
**Try this.**
* A second player: `players` `2` in the game's form, a second `gun` with
`gun.player` `2`, and `hit` rules with `player` set.
* Shoot the door as it opens: a third track, and a type with `health.max`
`3` that takes three hits.
* Score by where the shot landed: a `target` behaviour with `zones` for a
3D model is in tutorial 9; in 2D, two hitbox types on two tracks make a
bull and an outer ring.
**The description.**
[source,lua]
----
include::../assets/Forge/tutorials/06-gun.game[]
----
=== Tutorial 7: A point-and-click adventure
image::tutorials/07-playing.png[The yard, 600]
**What you will make.** A room of the Sierra and LucasArts kind: a painted
yard, a hero who walks where you click, things to look at, take, open, and
talk to, a key that goes in the inventory, a guard with a conversation, and
a door to a second painted room.
**What you need.** The kit's `yard.png`, `hall.png`, `hero.png`, `key.png`,
`door.png`, and `door.wav`.
**Steps.**
. Open *New game*, clear it, and fill the game's form: `title`
`The Yard`, `vars` `score=0, allowed=false`, `verbs`
`walk, look, take, use, open, talk`, and `layers` `overlay`. Then the
room's own fields, further down the same form: `name` `yard`,
`depthSort` `true`, and `scaleBy` `300:0.6, 460:1.0` -- things standing
at the back of the yard are drawn at six tenths, at the front full size,
and between in proportion.
. kbd:[2], kbd:[A]: a type `backdrop`, a `sprite` of `yard.png`. kbd:[1],
kbd:[A], and it fills the canvas. Now the floor the hero may walk on.
Press kbd:[V]: a walk area is being drawn, the panel steps aside, and
every press on the canvas is a corner. Press the four corners of the
yard's floor -- left and right where the wall meets it, then the near
corners -- and kbd:[V] again to close it. A green outline shows it, and
the panel comes back.
+
image::tutorials/07-walkArea.png[The walk area, 600]
. The hero: a `sprite` of `hero.png` with `look.frames` `8` and
`look.anchor` `feet`, `behaviours` `walker, frames`, `walker.speed` `160`,
`frames.fps` `10`, and `frames.states` `idle=1-1, walk=2-5`. Place him at
the front left. A walker's state is `walk` or `idle` as it goes, and the
frames follow.
. Three hotspots. `door`: `door.png`, `look.anchor` `feet`, `behaviours`
`hotspot`, `hotspot.name` `door`, and a place to walk to first,
`hotspot.walkX` `590` and `hotspot.walkY` `330`; place it in the doorway.
`key`: `key.png`, a hotspot named `key` with a walk point of `200`,
`400`, placed on the floor. `guard`: `hero.png` again, `frames` `8`,
`look.faces` `left` so he looks toward the hero, a hotspot `guard` at
`480`, `360`, placed by the door.
+
image::tutorials/07-hotspots.png[The hotspots, 600]
. kbd:[5] for *Dialogues*, and kbd:[A]: a dialogue called `talk` with one
node, `start`. kbd:[Enter], `name` `guard`. kbd:[Down] onto the node:
`who` `Guard`, `text` `Nobody passes.` kbd:[A] on a node adds a choice:
`text` `Why not?`, `next` `why`. kbd:[A] again: `Fine.`, with no `next`,
which ends the talk. kbd:[Up] to the dialogue's own row and kbd:[A] for
a second node: `name` `why`, `who` `Guard`, `text`
`The door is locked, and I have no key.` Its choices: `I have a key.`
with `when` `has("key")` and `next` `ok`, and kbd:[T] on that choice for
an action, `setVar` of `allowed` to `true` -- kbd:[Delete] on its `entity`
empties it, so the var is the game's; then `I see.` A third node `ok`:
`Then go ahead.`
+
image::tutorials/07-dialogue.png[The dialogue, 600]
. kbd:[3], and the rules. `A click on the floor walks there`, on `pressed`
with `switch` `SWITCH_BUTTON3` and `interrupt` `true`: a `walkToPointer`
of `hero` for `player` `1`. `Look at anything`, on `verb` with `verb`
`look`: a `say` of `"It is a " .. event.target .. "."` for `1` second.
`Take the key`, on `verb` `take` with `target` `key`: `walkToHotspot` of
`hero` (its `target` is `self`, the key), `give` `key`, `addScore` `1`,
`destroy`. `Talk to the guard`, on `verb` `talk` with `target` `guard`:
`walkToHotspot`, `face` of `hero` toward `self`, and `talk` with
`dialogue` `guard`.
+
image::tutorials/07-verbRules.png[The verb rules, 600]
. `The door, while the guard objects`, on `verb` `open` with `target`
`door`, when `not allowed`: `say` `"The guard shakes his head."`.
`The door opens`, the same event with `controls` `false`, when `allowed`:
`walkToHotspot`, `playSound` `door.wav`, a `fade` of `0.5` with `out`
`true`, a `goTo` of `hero` to `room` `hall` at `120`, `420`, a `fade` back
with `out` `false`, and `addScore` `1`. `Anything else`, on `verb` with
nothing filled in: `say` `"That does not work."`. Then the verb keys:
on `pressed` with `key` `L`, a `setVerb` of `look`; `T` for `take`, `O`
for `open`, `K` for `talk`. The readout's `text`:
`(sentence or "") .. " score " .. score`.
. The hall. kbd:[1], kbd:[N] for a room, `name` `hall`, `depthSort`
`true`. Place a `backdrop` -- and, since the type's picture is the yard,
a second type `hall` from `hall.png` does better -- a walk area along its
floor, and a `readout`. kbd:[PgUp] goes back to the yard.
+
image::tutorials/07-hall.png[The hall, 600]
. kbd:[S], kbd:[P]. Click the floor and the hero walks; the sentence line
says what the pointer is over. kbd:[T] then a click on the key takes it.
kbd:[K] and the guard: with the key, the second choice opens, and the
guard relents. kbd:[O] and the door, and the yard fades to the hall.
**What happened.** A room's *walk areas* become a navigation mesh, and a
`walker` finds its way across them; `depthSort` draws what is lower on the
screen in front, and `scaleBy` makes what is further up smaller, so a flat
picture reads as a floor. A `hotspot` is anything a *verb* can be used on:
the game's `verbs` are the sentence's first word, the pointer's hotspot its
second, and a click raises `verb` with both, the most specific rule winning
-- `open` on `door` beats `open` on anything, and a rule with no `verb` at
all is the last resort. `give`, `take`, and `has` are the inventory;
`talk` runs a *dialogue*, a tree of what is said and what may be answered,
each choice with a `when` and actions of its own. Actions that take time
-- `walkToHotspot`, `say`, `fade`, `talk` -- make a rule a *sequence* that
runs to its end while the rest of the game goes on, unless `controls` is
off. All of it is in
<<_adventures_rooms_walking_hotspots_verbs_and_talk>>.
**Try this.**
* A `use` rule: `verb` `use` with `item` `key` and `target` `door`, for
when the player tries the key on the lock.
* A parser: `layers` `overlay, parser`, and rules on `said` with a `verb`
and a `noun` -- the Sierra way, typed.
* A `saveGame` on a key, and a `loadGame` on another.
**The description.**
[source,lua]
----
include::../assets/Forge/tutorials/07-adventure.game[]
----
=== Tutorial 8: Into 3D
image::tutorials/08-playing.png[The fox in the field, 600]
**What you will make.** The platformer again, in a scene: a fox on the
engine's character controller, a floor and a ledge, a prize on the ledge, and
a camera that follows.
**What you need.** The kit's `fox.glb` and `coin.wav`.
**Steps.**
. Open *New game*, clear it, and in the game's form set `title` `Field`
and `layers` `scene3d, overlay`. The canvas becomes the *viewport*: the
scene itself, built from the types' looks, under an editor camera.
`scene3d.fov` `55` narrows the view a little.
+
image::tutorials/08-scene.png[A scene, 600]
. kbd:[2], and three `mesh` types -- solid shapes with a colour. `floor`:
`look` `mesh`, `look.shape` `box`, `look.w` `30`, `look.h` `0.4`,
`look.d` `12`, a colour of `90`, `110`, `90`, and `behaviours` `solid`.
`ledge`: a box of `4`, `0.4`, `4` in `110`, `90`, `70`, also `solid`.
`prize`: `look.shape` `sphere`, `look.w` `0.6`, in `255`, `200`, `60`,
with `behaviours` `trigger`. Sizes are world units; a unit is about a
metre.
+
image::tutorials/08-meshes.png[The meshes, 600]
. The hero: `look` `model`, `look.file` `fox.glb`, `look.scale` `0.008`
(the fox is drawn in centimetres), and `look.w` `0.6`, `look.h` `0.9`,
`look.d` `0.6` for how big it counts as. `behaviours`
`keys, character, animator`: `keys.left` `LEFT` and so on for the four
arrows; `character.speed` `4`, `character.jump` `6.5`,
`character.radius` `0.3`, `character.height` `0.9`; `animator.idle`
`Survey` and `animator.walk` `Walk`, two of the fox's own clips. And a
`camera`: a look of `none`, `behaviours` `camera`, `camera.mode`
`follow`, `camera.target` `hero`, an offset of `camera.x` `0`, `camera.y`
`3`, `camera.z` `7`, `camera.lookY` `0.6`, and `camera.lag` `0.2`.
+
image::tutorials/08-hero.png[The hero and the camera, 600]
. Place them. kbd:[1], kbd:[A] puts an entity on the floor under the middle
of the viewport; its `x`, `y`, and `z` are typed, in units, to a tenth.
The `floor` at `0`, `-0.2`, `0`; the `ledge` at `6`, `1.2`, `0`; the
`prize` at `6`, `2`, `0`; the `hero` at `-4`, `0.5`, `0`; the `camera`
at `-4`, `3`, `7`. kbd:[J] and kbd:[L] orbit the editor camera round
what is selected, kbd:[Y] and kbd:[H] tilt it, kbd:[I] and kbd:[K] close
in and back off. An entity is picked by a press where it stands and
dragged across the floor; the selected one carries the *gizmo*, three
arms along the axes -- drag an arm's handle to move along that axis alone
-- and a yellow fourth that turns it.
+
image::tutorials/08-viewport.png[The viewport, 600]
. kbd:[3], and the rules. `Walking is a state the animator plays`, on
`frame` with `each` `hero`, when a `test` of
`self.dx != 0 or self.dy != 0`: a `setState` of `self` to `walk`.
`Standing still`, the same with `self.dx == 0 and self.dy == 0` and
`idle`. `Jump, with ground underfoot`, on `frame`, when `keyPressed`
`SPACE` and `onGround` of `hero`: `jump` `hero`. `The prize is taken`,
on `enter` with `a` `prize` and `b` `hero`: `destroy`, `addScore` `100`,
`playSound` `coin.wav`. `Fallen off the world`, on `frame`, when
`hero.y < -5`: a `moveTo` of `hero` back to `-4`, `0.5`, `0`.
+
image::tutorials/08-rules.png[The rules, 600]
. kbd:[S], kbd:[P]. The arrows walk the fox relative to the camera, which
follows; kbd:[Space] jumps it onto the ledge, and the prize goes.
**What happened.** A `scene3d` layer makes the room a scene: positions are
world units in three axes, physics is Jolt's, and the picture is drawn from
whichever instance has the `camera` behaviour. `mesh` looks are shapes,
`model` looks are glTF files with their own animation clips, which an
`animator` plays by state. `character` is the engine's character
controller, moved by the vars `dx` and `dy` that `keys` sets, relative to
the camera; `trigger` and `enter` work as they did on the flat. The
viewport is the same scene under an editor camera, which is why what is
placed is what is seen. Scenes are in <<_games_in_the_scene>> and the
viewport in <<_the_3d_viewport>>.
**Try this.**
* A `light` type -- `look` `light`, `look.type` `point`, a colour and an
`intensity` -- placed above the ledge.
* `camera.mode` `orbit`, or `first` for the fox's own eyes.
* A `sky`: `scene3d.sky` with a picture of your own.
**The description.**
[source,lua]
----
include::../assets/Forge/tutorials/08-scene.game[]
----
=== Tutorial 9: A rail shooter
image::tutorials/09-playing.png[The dead yard, 600]
**What you will make.** House of the Dead in a yard: the camera rides a
rail and stops; at each stop zombies come out and walk at you; the gun on
the pointer drops them, ragdolls and all; when the stop is clear the rail
moves on. The yard is the painting from tutorial 7, standing in the scene,
with its pillar as an *occluder* -- a box that is not seen but hides what
walks behind it, so a zombie passes behind a pillar that is only paint.
**What you need.** The kit's `fox.glb`, `yard.png`, and `hit.wav`.
**Steps.**
. Open *New game*, clear it, and in the game's form: `title`
`The Dead Yard`, `vars` `score=0, health=3`, `layers` `scene3d, overlay`,
`scene3d.fov` `60`.
. The room's meshes. `floor`: a `mesh` box of `40`, `0.2`, `60` in `70`,
`75`, `90`, `solid`. `painting`: a box of `24`, `16`, `0.2` in white
(`255`, `255`, `255`) with `look.texture` `yard.png` and `look.unlit`
`true`, so the picture is shown as painted and not as lit. `pillar`: a
box of `1.2`, `8`, `1.2` with `look.occluder` `true` and no colour at
all.
+
image::tutorials/09-room.png[The room's types, 600]
. The rest. `camera`: a look of `none`, `behaviours` `camera`,
`camera.mode` `rail`, `camera.track` `rail`. `gun`: as in tutorial 6.
`zombie`: `look` `model`, `look.file` `fox.glb`, `look.scale` `0.01`, a
size of `0.8`, `0.9`, `1.4`, `vars` `health=2`, and `behaviours`
`health, seek, animator, target, timer`: `health.max` `2`,
`health.ragdoll` `true`, `health.linger` `2`; `seek.target` `camera`,
`seek.speed` `1.6`, `seek.stopAt` `2.2`; `animator.idle` `Survey`,
`animator.walk` `Walk`, `animator.attack` `Run`; `timer.name` `bite`,
`timer.after` `1.5`, `timer.start` `false`.
+
image::tutorials/09-zombie.png[The zombie, 600]
. Place the `floor` at `0`, `-0.1`, `-20`; the `painting` at `0`, `6`,
`-30`, standing across the far end; the `pillar` at `0`, `4`, `-22`, in
front of the painted one; the `camera` at `0`, `1.6`, `0`; the `gun`
anywhere.
+
image::tutorials/09-placed.png[Placed, 600]
. kbd:[4] for *Tracks*, kbd:[N]: in a scene a new track is a rail, keyed
by time. `name` `rail`. kbd:[K] puts a point where the editor camera
stands, looking where it looks; its form takes the rest. Four points:
`at` `0` at `0`, `1.6`, `0` looking at `0`, `1.2`, `-10`; `at` `4` at
`0`, `1.6`, `-6` looking at `0`, `1.2`, `-16` with `stop` `gate`; `at`
`10` at `2`, `1.6`, `-14` looking at `2`, `1.2`, `-24` with `stop`
`pillar`; and `at` `14` at `2`, `1.6`, `-18` looking at `2`, `1.2`,
`-28`. The rail is drawn through the viewport with its stops marked.
+
image::tutorials/09-rail.png[The rail, 600]
. kbd:[3], and the rules. `The gate: two come out of the dark`, on
`stopped` with `name` `gate`: two `spawn`s of `zombie`, at `-2`, `0`,
`-14` and `2`, `0`, `-15`. `The pillar: three more, one from behind
it`, on `stopped` `pillar`: spawns at `-1`, `0`, `-23`; `0`, `0`,
`-26`; and `4`, `0`, `-24`. `A zombie walks the moment it is made`, on
`spawn` with `type` `zombie`: `setState` `walk`. `A shot lands`, on
`hit` with `type` `zombie`: `damage` `1`, `addScore` `100`, `playSound`
`hit.wav`.
+
image::tutorials/09-hitRule.png[The hit rules, 600]
. `It reaches the camera and rears up`, on `arrived` with `type` `zombie`:
`setState` `attack` and a `timerStart` of `bite` after `1.5`. `The bite
lands unless it was killed in time`, on `timer` with `type` `zombie` and
`name` `bite`, when `inState` `attack`: `addVar` `health` by `-1` (its
`entity` emptied with kbd:[Delete]), a red `flash`, a `shake` of `0.3`
for `0.4`, and the timer started again. `The stop is clear: move on`, on
`frame`, when `waiting` with `entity` `camera` and a `test` of
`count("zombie") == 0`: `pathNext` `camera`. `Out of health`, on
`frame`, when `health <= 0` and `once` with `tag` `over` and `scope`
`game`: a `say` of `"YOU ARE DEAD"` and `gameOver`. `The end of the
rail`, on `railEnd`: `say` `"STAGE CLEAR"`. The readout's `text`:
`"score " .. score .. " health " .. health .. " ammo " .. gun.ammo`.
+
image::tutorials/09-rules.png[The rules, 600]
. kbd:[S], kbd:[P]. The camera glides to the gate; two foxes come at you.
Shoot them: two hits each, and they fall limp. The rail moves on to the
pillar, where one comes out from behind it.
**What happened.** A `camera` in `rail` mode rides a track of points in
time, each with a place and a point to look at, and holds at a point with a
`stop`, raising `stopped` with its name; `waiting` is true while it holds,
`pathNext` sends it on, and `railEnd` fires at the last point. `seek`
walks an instance straight at a target and raises `arrived`; a `timer`
raises `timer` after its seconds, started by `timerStart`. In a scene the
gun casts a ray from the pointer, and what it meets is a `target` -- a
body the size of its look, with `zones` on named bones if a headshot should
count for more. `health.ragdoll` lets a dead model fall on its own bones
and `linger` before it goes. An `occluder` mesh writes depth and no
colour: anything behind it is hidden, and the painting behind it shows
through, which is how a 3D character walks behind a pillar that is only in
the picture -- the way a Grim Fandango room is built. Rails and the gun are
in <<_games_in_the_scene>>; painted rooms in
<<_adventures_rooms_walking_hotspots_verbs_and_talk>>.
**Try this.**
* Headshots: `target.zones` on the zombie, a list of boxes on named bones
typed as `name=head, bone=b_Head_05, w=0.5, h=0.5, d=0.5`, and a `hitPart`
condition of `head` in a second hit rule that does `2` damage.
* Fog: `scene3d.fog` `r=12, g=12, b=22, near=25, far=60`.
* A third stop, and a `light` type above it.
**The description.**
[source,lua]
----
include::../assets/Forge/tutorials/09-rail.game[]
----
=== Tutorial 10: Releasing your game
image::tutorials/10-playing.png[Coin Run, 600]
**What you will make.** The platformer from tutorial 3 as an arcade game:
lives, a coin slot, a continue, the score panel round the picture, the score
sent to the board -- and a folder with everything it needs, ready to be
packed into a `.game` and listed by the Singe menu on a machine that has
never seen Forge.
**What you need.** Tutorial 3, or `03-sprites` from the chooser.
**Steps.**
. Open it, and in the game's form: `title` `Coin Run`, `vars`
`score=0, lives=3, credits=0`, and `layers` `world2d, bezel`. The
`bezel` is the arcade's score panel round the picture, showing the vars
`score`, `lives`, and `credits` as they change.
+
image::tutorials/10-bezel.png[The bezel layer, 600]
. kbd:[3], and onto rule 6, `Spikes hurt`, kbd:[Down] past its parts to the
last, and kbd:[T]: an `addVar` of `lives` by `-1`, its `entity` emptied.
Then `Out of lives`, on `frame`, when `lives <= 0` and `once` with `tag`
`over` and `scope` `game`: a `submitScore` with `board` `default`, a
`say` of `"GAME OVER"` for `2` seconds, and `gameOver`. `A coin`, on
`pressed` with `switch` `SWITCH_COIN1`: `credit`. `Continue`, on
`pressed` with `switch` `SWITCH_START1`, when
`credits > 0 and lives <= 0`: `addVar` `credits` by `-1`, `setVar`
`lives` to `3`, and `restart`. The readout's `text` gains
`.. " lives " .. lives`.
+
image::tutorials/10-arcadeRules.png[The arcade rules, 600]
. kbd:[S], then kbd:[X]: the game is *released* into a folder named for
it in Forge's data directory -- `CoinRun` -- and the message says where.
In it: `CoinRun.singe`, the compiled game; `Author.singe`, the runtime
it needs; `CoinRun.game`, the description, so it can be opened again;
`games.dat`, so the menu lists it, naming the script as the menu will see
it, `CoinRun/CoinRun.singe`; and every file the description named, at the
same relative name -- here the kit's hero, coins, and sounds under
`Forge/kit/`.
+
image::tutorials/10-released.png[Released, 600]
. Pack it, as any Singe game is packed, from a shell in Forge's data
directory (the chooser said where it is, beside `Forge.pdf`):
+
[source,text]
----
Singe --pack CoinRun CoinRun.game
----
+
Copy the `.game` beside the other games, and the menu lists *Coin Run*
next time it starts. Nothing of Forge is in it but the runtime it
carries. A packed game left beside the descriptions is not mistaken for
one: the chooser can tell them apart.
**What happened.** A released game is an ordinary Singe game: a script,
a `games.dat`, and its files. The compiled script finds its own folder and
loads the runtime and every named file from there first, so it runs wherever
it is put, packed or loose. `submitScore` queues the score for the master
service's board for this game, sent when the machine can reach it, and the
menu shows the board. `credit` counts a coin, and the bezel's `credits`
is what a cabinet's coin door shows. Releasing is in
<<_releasing_a_game>>; the board, the bezel, and what a cabinet needs are in
<<_arcade_plumbing_branching_video_lives_credits_the_board>>.
**Try this.**
* Open `CoinRun.game` from the release folder in Forge -- drop it in the
chooser's data directory -- change something, and release again.
* Two players: `players` `2`, a second hero on the other keys, and
`score2` and `lives2` for the bezel's other side.
* A `hud` layer instead of the readout, with an RmlUi document of your own.
**The description.**
[source,lua]
----
include::../assets/Forge/tutorials/10-release.game[]
----
=== Recipes
The rest of the catalogue, a paragraph each, with a sample to open from the
chooser. A recipe names the behaviours; the tutorials above have shown how
each is typed.
*Racing* (`racing.game`). A `scene3d` room, a `vehicle` behaviour on a
mesh -- `type` `car`, `motorcycle`, `tank`, or `boat`, with its `mass`,
`thrust`, `torque`, and wheel sizes -- driven by `keys` through the vars `dy`
(throttle) and `dx` (steering), a `camera` in `follow` mode behind it, and
opponents on `racer`, which drives a vehicle round a track of points on its
own. A `trigger` across the finish line, a `lap` var, and a `timer` for the
clock.
*Tower defence* (`towerdefence.game`). Creeps on `patrol` along a list of
points to the base, a `spawner` making waves of them, towers with `turret`
-- `targets` the creep type, a `range`, a `rate`, and a `spawns` projectile
or plain `damage` -- placed by `spawnAtPointer` on a click while the gold
var allows, and `patrolEnd` costing a life.
*Pool and water* (`pool.game`). A `mesh` with the `water` behaviour is a
pool: bodies inside it float or sink by their `buoyancy`, and a `soft`
mesh is a cloth or a balloon. `body` gives anything mass, bounce, and
friction; `push` shoves it; `joint` hangs one body from another by a
hinge, a ball, or a slider.
*Bowling* (`bowling.game`). Ten `body` pins and a ball pushed along the
lane, a `trigger` behind the pins that counts what falls through, and a
`frame` rule that resets the rack with `moveTo` when the ball is past them.
*The maze* (`maze.game`). A `grid` look draws a map of tiles from a sheet
-- `columns` wide, `tile` pixels square, the `map` rows of tile numbers --
and `solid` walls are entities placed on it; `keys` and `mover` with
`clamp` walk the maze, a `timer` runs the clock, and `enter` on the exit
`trigger` ends it.
*Third person* (`thirdperson.game`). Tutorial 8's fox with an `orbit`
camera: the mouse turns the view, the arrows walk relative to it, and
`camera.eye` raises the viewpoint. `animator` with `attack` and `hit`
clips, `health` on what it fights, and `animationDone` to return to `idle`.
*Space* (`space.game`). `thrust` pushes a body along its nose by `dy` and
turns it by `dx` -- a ship without lift -- with `projectile`s that `aim`
along their flight and a `spawner` of rocks that `drift`.
*Rhythm and MIDI* (`rhythm.game`). A game that opened a MIDI port hears
`midi` events with `event.pitch` and `event.velocity`; a `timeBetween`
window round each beat, and `once` per note, scores the hit. A `music`
layer plays the track from the start.
*The 3D adventure* (`adventure3d.game`). Tutorial 7's rooms in a scene:
`walker` on the floors named in the room's `navFrom`, hotspots with a
`walkX`, `walkY`, `walkZ`, a painted backdrop on an unlit mesh, and
`occluder` boxes where the painting's furniture stands, so the hero walks
behind it. The verbs, the inventory, and the dialogues are exactly the flat
ones.
*The board and the bezel* (`fmv.game`). `bezel` round any game, `credit`
on the coin switch, `restart` on start with credits, and `submitScore` at
the end; `branching` for a Dragon's Lair of windows, moves, and jumps.
*Anything else.* The `lua` action runs a line of Singe Lua in a rule, with
`self`, `other`, and `event` in scope, and the engine's whole API with them:
the way out when the vocabulary stops, and it is meant to be used.
== Describing a Game Instead of Writing One
A game can be written as a *description* -- a table of types, rooms, 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
functions, 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 nouns
*Layers* are what the game draws through: `world2d` (physics in the XY plane,
drawn into the overlay), `scene3d` (the 3D scene, with a sun, a sky, and fog;
positions are world units), `overlay` (flat drawing), `disc` (the video the
game is played over), `hud` (an RmlUi document bound to the game's vars), and
`music`. A game lists the ones it wants.
*Types* are what things are: a `look` (`box`, `sprite`, `text`, or `none` in
2D; `model`, `mesh`, `light`, `billboard`, or `text3d` in the scene), any
number of `behaviours`, and `vars` -- the state every instance starts with.
Everything placed or spawned is an instance of a type.
*Rooms* are where things are. A room lists its `entities` -- a type, an id, a
position, and any vars that differ -- and its `tracks`. A room keeps its state
when it is left unless it says `reset = true`; vars declared on the game live
across rooms.
*Rules* are a trigger, conditions, and actions. `on` names the event -- every
frame, a collision between two types, a shot landing, a death, a timer, a disc
frame passing -- and `each` scopes the rule to every instance of a type, with
`self` bound. All of a rule's conditions must hold for its actions to run; an
`any` group inside them is an OR.
.A description, in full
[source,lua]
----
return {
title = "One rule",
layers = { { kind = "world2d", gravity = 1500 } },
vars = { score = 0 },
types = {
ground = { look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
behaviours = { { kind = "solid" } } },
hero = { look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
behaviours = { { kind = "platformer", speed = 210, jump = 620 } } }
},
rooms = {
{ name = "start",
entities = { { type = "ground", id = "ground", x = 360, y = 440 },
{ type = "hero", id = "hero", x = 120, y = 380 } } }
},
rules = {
{ note = "Run right",
on = "frame",
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/` holds the worked examples -- a platformer, a QTE over
video, a shoot-em-up, a light gun game over video, a rail shooter, a 3D
platformer, an adventure in 2D and in 3D, a racing game, a tower defence,
branching video, a first-person shooter, breakout, a maze, third-person
action, a space shooter, a rhythm game, and bowling, with two more for the
sprite, tile, MIDI, water, and soft-body pieces -- and
`testScripts/scene52.singe` onward compile and play them.
=== Instances, vars, and states
A type's `vars` are what each instance starts with: `vars = { health = 3,
ammo = 6 }`. An entity in a room may override them (`vars = { health = 5 }`),
and rules read and change them: `self.health` in an expression, `setVar` and
`addVar` as actions. `state` is a var every instance has, `"idle"` to begin
with; `setState` changes it and `inState` tests it.
Vars declared on the game -- `vars = { score = 0, lives = 3 }` -- belong to
no instance and are read by name: `score`, `lives`. `setVar` and `addVar`
with no entity change them, and `addScore` is `addVar` on `score`.
Instances are made when a room is entered, by a `spawner` or `shooter`
behaviour, or by the `spawn` action, and go with `destroy`, with a `health`
that reaches zero, or with a `projectile` that leaves the picture. An
instance made at run time has an id of its own (`raider#7`); rules reach it as
`self` or `other`, or through `each`.
=== Events
A rule's `on` says what triggers it:
[cols="1,3"]
|===
| `frame` | every frame, the default
| `pressed`, `released` | a key or a switch, once per press; `key` or `switch` narrows it
| `collision` | an instance of type `a` began touching one of type `b`; `self` is `a`, `other` is `b`
| `hit`, `miss` | a gun's shot landed on `self` (`other` is the gun), or landed on nothing; `event.offscreen` says whether a miss left the picture
| `death`, `spawn` | `self`'s health reached zero; `self` was just made
| `timer` | a timer on `self` went off; `event.name` says which
| `frameReached` | the disc passed `frame`
| `enter`, `leave` | an instance of type `b` entered or left a trigger of type `a`
| `arrived` | a seeking instance reached its target
| `stopped`, `railEnd` | a rail camera reached a stop (`event.name`), or the end of its track
| `animationDone` | a clip that does not loop ended on `self`
| `roomStart`, `roomEnd`, `gameOver` | the game's own moments
|===
The event's parameters are written on the rule itself (`on = "collision",
a = "shot", b = "raider"`). `each = "raider"` runs a frame rule once per live
raider, and narrows an event rule to that type; `room = "hall"` keeps a rule
to one room.
=== Expressions
Anywhere a number goes, an expression may go instead, as a string: numbers,
strings, `+ - * / % ^ ..`, comparisons, `and`, `or`, `not`, and the names
below. Nothing else is allowed -- an expression is checked when the game is
built -- and the `lua` action is the way past it.
[cols="1,2"]
|===
| `self.health`, `other.x`, `event.frame` | the instance the rule is about, the other one, the event's own fields
| `hero.x` | a placed entity's field or var, by id
| `score`, `lives` | the game's vars
| `time`, `frame` | seconds since the game began; the disc frame
| `count("raider")` | live instances of a type
| `random(a, b)`, `distance(a, b)`, `has("key")` | a number in a range; between two instances; whether the inventory holds an item
| `abs`, `min`, `max`, `floor` | arithmetic
|===
A `test` condition takes one: `{ "test", expr = "count('raider') == 0" }`.
=== Sequences
Some actions take time. `say` shows a line and waits for it; `wait` waits.
A rule with one of them runs as a coroutine, its actions in order, each waiting
its turn -- a cut-scene is the same shape as `addScore`. While it runs the
same rule does not start again for the same instance unless the rule says
`interrupt = true`, and `controls = false` on the rule takes the keys away for
its duration.
=== Tracks
A track is keys by disc frame (`key = "frame"`) or by time (`key = "time"`),
and a room may hold any number. A track of `boxes` is a hitbox that moves
with the picture:
[source,lua]
----
tracks = {
{ name = "bandit", key = "frame",
boxes = { { at = 120, x = 100, y = 200, w = 60, h = 100 },
{ at = 220, x = 400, y = 200, w = 60, h = 100 } } }
}
----
Between keys the box is interpolated; outside them there is none. A type with
`{ kind = "hitbox", track = "bandit" }` is hit where the track says, which is
how a light gun game over video is 2,750 rectangles no longer.
A track of `points` is a rail for a camera: positions by time, each looking
at a point (`look = { x, y, z }`) or at an entity by id, and any point may be
a `stop` that holds the camera there until a `pathNext` action sends it on --
which is what House of the Dead does at every doorway.
[source,lua]
----
{ name = "rail", key = "time",
points = { { at = 0, x = 0, y = 1.6, z = 0, look = { 0, 1.2, -10 } },
{ at = 4, x = 0, y = 1.6, z = -6, look = { 0, 1.2, -16 }, stop = "hall" } } }
----
=== Games in the scene
A game with a `scene3d` layer places its types in world units and draws them
through the engine's renderer. A `model` look is a glTF file with a scale and
a starting clip; `mesh` is a box, sphere, cylinder, or plane with a colour;
`light` and `billboard` are what they say. The 2D looks still draw on the
overlay over the scene, which is how a score readout works in both.
The behaviours that live there: `character` is the engine's controller
moving by the `keys` vars relative to the camera; `camera` draws the scene
from the instance -- `fixed`, `follow` (an offset from a target), `orbit`
and `first` (the mouse looks), or `rail`; `seek` walks toward a target and
raises `arrived`; `animator` plays a clip per state and raises
`animationDone`; `target` gives a `gun` something to hit, with `zones` on
named bones so a shot says which part it landed on (`hitPart`); `health`
with `ragdoll` lets a model fall limp on death; `solid`, `body`, and
`trigger` are the physics. A gun in the scene casts a ray through the
pointer, so the same `hit` and `miss` rules serve a light gun over video and
a rail shooter.
`testScripts/author/railshooter.game` is House of the Dead in a table: a
rail with two stops, zombies that spawn there, walk to the camera, and bite
on a timer unless they are killed, headshots through a zone on the head bone,
and the rail moving on when the stop is clear.
=== Adventures: rooms, walking, hotspots, verbs, and talk
A point-and-click adventure -- Sierra's or LucasArts' -- is rooms that
remember themselves, a character that walks where the player clicks, hotspots
a verb is used on, an inventory, dialogue, and cut-scenes. All of it is the
same nouns.
*Walking.* A 2D room declares `walk = { { x, y, x, y, ... }, ... }`, its
walkable floor as polygons in overlay coordinates; a 3D room declares
`navFrom = { "floor", "ledge" }`, the entities whose meshes are its floor.
Either is baked into a navigation mesh when the room is entered, and a type
with a `walker` behaviour walks it: `walkTo` sends it to a point (and waits),
`walkToPointer` to where the player clicked, `walkToHotspot` to a hotspot's
walk point; it raises `arrived`, and its state is `walk` or `idle` as it
goes. A painted room may say `depthSort = true`, so what is lower on the
picture is drawn over what is higher, and `scaleBy = { { y = 300, scale = 0.6
}, { y = 460, scale = 1 } }`, so a character further up the picture is drawn
smaller; looks with `anchor = "feet"` stand on their position rather than
being centred on it, which is what a walking character wants.
*3D over a painting.* A room in the scene may be a picture on a flat `mesh`
(its `texture`, `unlit`) with 3D characters in front of it. Whatever stands
in the picture -- a pillar, a doorway, a table -- that a character has to walk
behind is a `mesh` look with `occluder = true`: an invisible box that hides
what is behind it while the picture shows through it. The editor's 3D
viewport keeps occluders visible so they can be placed; the game hides them.
`testScripts/author/painted.game` and scene 79 show a character crossing a
painted room behind such a pillar.
*Hotspots.* A type with `{ kind = "hotspot", name = "door", walkX = 600,
walkY = 330 }` is something a verb can be used on: its look's box, or a
`polygon` of x,y pairs, in 2D; a box the size of its look, met by the ray
through the pointer, in 3D. The name is what the sentence line shows.
*Verbs.* A game with `verbs = { "walk", "look", "take", "use", "open", "talk"
}` builds a sentence line from the current verb, the hotspot under the
pointer, and the item in hand (`sentence`, `hover`, `verb`, and `item` are
game vars, so a text look or a HUD element can show them). A click on a
hotspot raises `verb` with `verb`, `target`, and `item`, and **the most
specific rule wins**: `verb = "use", item = "key", target = "door"` beats
`verb = "use", target = "door"` beats `verb = "use"` beats a rule that names
nothing, which is the "That doesn't work" every adventure needs. A rule with
conditions ranks above one without among the same parameters, so "open the
door while the guard objects" and "open the door" can share their parameters
and differ in a `test`. `setVerb`, `nextVerb`, and `useItem` choose; the
first verb in the list is what a click on the floor means, and the `pressed`
event carries the pointer's `x` and `y` for a `walkToPointer` rule.
*The parser.* A `parser` layer takes typing at a prompt: `words = { look =
{ "look", "examine", "l" }, door = { "door", "gate" } }` says what the game
knows, and a typed line raises `said` with its `verb`, `noun`, and `second`
as the words table knows them -- or `event.unknown`, the first word it did
not -- with the same most-specific-wins rule. A game may offer both a verb
line and a parser; the rules do not care which was used.
*Inventory.* `give` and `take` keep a list in the `inventory` var;
`has("key")` tests it in an expression and `has` as a condition.
*Dialogue.* The game's `dialogues` are trees:
[source,lua]
----
dialogues = {
guard = {
start = "hello",
nodes = {
hello = { who = "Guard", text = "Nobody passes.",
choices = { { text = "Why not?", next = "why" }, { text = "Fine." } } },
why = { who = "Guard", text = "The door is locked.",
choices = { { text = "I have a key.", when = 'has("key")',
act = { { "setVar", name = "allowed", value = true } }, next = "ok" },
{ text = "I see." } } },
ok = { who = "Guard", text = "Then go ahead." }
}
}
}
----
`talk` runs one and waits for it: each node's line is said, its choices
(those whose `when` holds, and not those already chosen once with `once`) are
offered and picked with the number keys or a click, the choice's `act` runs,
and `next` names the node after. With a HUD, the choices go in the element
called `choices`.
*Cut-scenes and rooms.* A rule with `controls = false` takes the keys away
while it runs; `fade` (out, then back with `out = false`) and `goTo` change
the picture and the room, and `goTo` with `at = "arrival"` puts the carried
entity where the entity of that id stands in the new room, so the same rule
serves a painted room and a modelled one. Sequences survive a room change.
*Saving.* `saveGame` keeps the whole game -- the room, every visited room's
instances and their vars, the game's vars, the inventory -- in a numbered
slot, and `loadGame` brings it back. A save taken while a sequence runs
records the world as it stands; the sequence does not resume. `die` is the
Sierra death: a line, a fade, and the game over from the start.
`testScripts/author/adventure2d.game` is the two-room adventure in a painted
room, and `adventure3d.game` is the same game in a modelled room from a fixed
camera -- it takes the 2D description's rules, dialogue, verbs, and parser as
they are and supplies only types and rooms. Scenes 64 and 65 play them.
=== Vehicles, crowds, turrets, and physics toys
`vehicle` puts a type on the engine's vehicle physics -- a car, a motorcycle,
a tank, or a boat -- with wheels hung at the corners its look's size gives,
driven by the `keys` vars (`dy` throttle, up is forward; `dx` steering), and
`self.speed` says how fast. `racer` drives a vehicle round a track of
`points` on its own, steering toward the next and easing off in bends, which
is every opponent in a racing game. `camera` in `follow` mode with a `lag`
is what sits behind the car.
`walker` with `follow = "hero"` keeps an instance after an entity (or the
camera) across the navigation mesh, asking again every so often -- a crowd of
enemies in a shooter, a companion. `patrol` walks a list of points in turn,
looping or stopping, and raises `patrolEnd` at the last: a creep's lane, a
guard's round. `turret` fires at the nearest instance of a type within range,
no faster than its rate, spawning a projectile aimed at it (or doing the
damage itself), and `spawnAtPointer` makes an instance where the player
clicked -- on the floor the ray meets, in 3D -- which is how turrets are
placed.
`body` is a thing physics moves (mass, bounce, friction); `thrust` pushes a
body along its nose by `dy` and turns it by `dx` (a hovercraft, a ship);
`joint` hangs a body from another's, or from the world, by a hinge, a ball,
or a slider through a point along an axis; and `push` shoves a body, or a
ragdoll's bone.
`testScripts/author/racing.game` (a car, a rival on a track, checkpoint
triggers, a hinged bar, a hovercraft) and `towerdefence.game` (creeps in
waves on a lane, turrets placed by clicks, gold and lives) are the worked
examples; scenes 66 and 67 play them.
=== Arcade plumbing: branching video, lives, credits, the board
A `branches` track is Dragon's Lair: each branch is a window of disc frames,
the `move` (a key) or `switch` it wants, and the frames the disc goes to on
`success` and on `fail`; a type with `{ kind = "branching", track = "moves" }`
watches it and raises `branchOpen` (with `event.move`), `branchTaken`, and
`branchMissed`. Lives, credits, and continues are vars and rules -- `credit`
on `SWITCH_COIN1`, a `restart` on `SWITCH_START1` when `credits > 0` -- and a
`bezel` layer shows `score`, `lives`, and `credits` (and a second player's
`score2` and `lives2` with `twin`) on the arcade panel around the picture.
`gameOver` ends the rules, keeps the best score across runs, and shows the
results card; `submitScore` sends the score to the master service's board
for the game, queued until it can go. `testScripts/author/fmv.game` is the
worked example and scene 68 plays it: one window answered, three missed, the
lives spent, the score sent, a coin, and a continue.
=== The long tail: sprite sheets, tiles, MIDI, water, soft bodies
`distance(a, b)` takes instances or ids (`distance(self, "hero")`) and
measures through the scene in 3D. A rule with `each` on an event that has no
instance of its own -- a key, a room, the disc -- runs once per instance of
the type, as a frame rule does, which is how "a swing of the sword hits every
enemy in reach" is one rule. A `solid` with `moving = true` is a body that
follows wherever the instance is moved: a paddle, a lift.
A `sprite` look turns with the var `angle` (degrees, clockwise), which an
entity's `rz` starts and which rules, a `spin` behaviour, or a `projectile`
with `aim` change -- a turning image is a whole one, not a sheet. `spin`
turns a 3D instance about Y the same way. A `particles` look is a steady
stream from the instance -- a fire, smoke, a thruster, rain -- with a colour,
a rate, a size, a speed, and a life, and the `emit` action is a burst from any
instance.
A `sprite` look with `frames` is a sheet of that many columns, and a `frames`
behaviour steps it: `states = "idle=1-1, walk=2-5"` gives each state its run
of frames, played at `fps`, so a walker's walk cycle follows the state the
`walker` (or a rule) sets. Art need only face one way: the look's `faces`
says which (`right` unless said), and while the var `facing` -- which the
`walker` and the `mover` set as they go -- is the other way, the image is
mirrored. A sheet drawn facing both ways names the other way's frames for
the state with `Left` or `Right` on the end (`walkLeft=6-9`), and those are
used instead of the mirror; `faces = "none"` never mirrors, for a top-down
sprite that turns rather than flips. A sheet entity with an `rz` (or a var
`angle`) draws its frame turned. A `grid` look draws a map of tiles from a sheet --
`columns` tiles wide, each `tile` pixels square -- from rows of tile numbers
(`map = "1 2 1; 2 1 2"`, `0` for nothing), which is a maze or a level in a
string. A game that opened a MIDI port hears `midi` events with
`event.pitch` and `event.velocity` for every note struck.
In the scene, a mesh look with `water` is filled with water: its top is the
surface, and a `body` with `buoyancy` floats in it; a `vehicle` of type
`boat` with `thrust` is pushed by its propeller while that sits under the
surface; and `soft` makes a mesh look a cloth or an inflated body.
`testScripts/author/tail.game` and `pool.game` are the worked examples;
scenes 70 and 71 play them.
=== 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 Vocabulary
Conditions, actions, behaviours, looks, layers, and events are not built into
the compiler. Each is an entry in the `AUTHOR` table in `Author.singe`
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 tables below are generated from
that manifest.
The parameter types: `number` (a number or an expression), `expression`,
`string`, `boolean`, `entity` (`"self"`, `"other"`, or an entity's id),
`type`, `scancode` (a key name from `SCANCODE`), `switch` (a `SWITCH_*` name),
`file`, `state`, `track`, `room`, and `lua`. A behaviour's parameters are
plain values in the description; key and switch names among them are resolved
when the game runs. An action or condition may also carry `defaults`, the
value a parameter takes when the description leaves it out (a `shake` of
`0.3` for `0.4` seconds), and `optional`, the parameters that mean something
by their absence (a `goTo` with no `x`); a parameter with neither, left
empty, is reported by the checker as *empty* and compiles to `nil`.
include::ForgeVocabulary.adoc[]
== 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`; 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, the samples inside `Forge.game` and 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 panel is an RmlUi document; the canvas beside it 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 panel will not reach
the canvas. The panel slides: drag the tab on its outer edge, or use the
arrow keys, so an entity that lives underneath it is never permanently out of
reach.
=== The four panels
`Tab` cycles the panel through the room's *entities*, the *types*, the
*rules*, the room's *tracks*, and the *dialogues*; the digits `1` to `5` go
straight to one. Whatever is selected is edited the same way: `Enter` walks
its fields and each is typed; `Enter` again moves to the next, keeping what
was typed and leaving alone a field passed over with nothing typed; `Esc`
puts the value back and leaves the form; `Delete` while typing empties the
field, which for an action's `entity` means the game itself. A typed number
comes back a number, because `100` and `"100"` compile to different source.
The last line of the box under the list is Forge's message: what just
happened. A value is typed in its own row: it starts selected whole, so
typing replaces it, and kbd:[Left], kbd:[Right], kbd:[Home], and kbd:[End]
put a caret into it to edit it instead; kbd:[Backspace] takes out the
character before the caret, or the whole selected value, and kbd:[Delete]
the character after it -- or, on the value still selected whole, empties
the field for good. Shifted keys type what
they say -- a capital, a quote. The game's name at the top of the panel,
or a press on an empty spot of the canvas, selects nothing, which is the
game's and the room's own form. The panel's list and its fields box each
scroll, with a bar and the wheel, when there is more than fits, and the
chosen row and the field being edited are kept in view. The mouse does the
same work: a click on a row of the list selects what it names -- an entity, a
type, a rule or one of its parts, a track or a key, a dialogue's node or
choice -- a click on a field in the box below starts typing that field, and
a click on a picker's row takes it.
*Entities*: `A` adds one of the selected type in the middle of the canvas,
`D` duplicates the selected one, `Delete` removes it, and dragging moves it.
Its fields are `id`, `type`, `x`, `y`, `z`, `scale` -- and in a 3D room
`rx`, `ry`, `rz`, its turn about each axis in degrees -- and each var its type
declares, overridable here. A scale scales the look, the body, and how big
the instance counts as for touching. Renaming an entity renames it in every rule that talks about
it. In a 3D room `A` puts the new entity on the floor under the middle of
the viewport, and positions are world units to a tenth. `PgUp` and `PgDn`
move between rooms; `N` adds a room. With nothing selected, `Enter` edits
the game and then the room: the game's `title`, `players`, `vars` (as
`score=0, lives=3`), `verbs`, and `layers` (a list of names, as `disc,
overlay`) with each layer's own parameters as `disc.file` or `scene3d.fov`
-- a table parameter is typed as text: a map such as `scene3d.ambient` as
`r=80, g=80, b=100`, a list of points such as `spawner.points` as
`100,-20; 300,-20`, a list of records such as `target.zones` as
`name=head, bone=b_Head_05, w=0.5, h=0.5, d=0.5; name=body, bone=b_Spine, w=0.8`
(one record per semicolon), a map of lists such as `parser.words` as
`look=look|examine|l, take=take|get`, and a list of words such as
`parser.ignore` as `the; a; an` -- then the room's `name`, `reset`, `depthSort`, `walk` areas (polygons of
x,y pairs, separated by semicolons, drawn on the canvas), `navFrom`, and
`scaleBy` (as `300:0.6, 460:1`). Typing a layer, or a behaviour, away and
back keeps its values.
A field whose values are a fixed set opens a *dropdown* under its own row
instead of a prompt: a file (a look's image or model, a sound, a
HUD document, a layer's disc) lists the files under the game's directory
and in the kit (`Forge/kit/`); a flag lists `true` and `false`; a key
lists the scancodes and a switch the switches; a type's `look` lists the
looks and a rule's `on` the events, each with its line of help; a word
parameter lists the words it takes, as the vocabulary tables say (`anchor`
is `feet`, a camera's `mode` is `fixed`, `follow`, `first`, `orbit`, or
`rail`); and a type, a room, an entity, a state, a track, or a dialogue's
node lists the ones the game has. The list opens on the value the field
has (a value the list does not know is offered as it is), or on `(none)`
when it has none, so `Enter` straight away keeps things as they are:
`Enter` on a row takes it and moves on to the next field, as `Enter` on a
typed value does, and a click takes it and stays. `(none)` leaves the
field empty -- the default, or the game itself for an action's entity.
Typing narrows the list to the names
that contain what was typed -- an exact name first, then names that begin
with it -- and `Backspace` widens it again; a long list shows a window of
rows around the chosen one, and says how many lie past it. A dropdown
opens upward when there is no room below its row. The pickers
that add a condition, an action, or an event (`C`, `T`, `E`) take the
panel's list over instead, since the whole manifest is being chosen from. Where the game's own names are only
suggestions -- a rule may name a type, a room, or an entity that is made
later, and a state can be any word -- what was typed is offered at the top
of the list, so `Enter` takes a new word as readily as a known one. `Esc`
on a picker leaves the field to be typed.
*Types*: `A` adds one, `D` duplicates, `Delete` removes one no room still
places. Its fields are `name`, `look` and what that look takes (`look.w`,
`look.file`, ...), `vars` typed as `health=3, ammo=6`, `behaviours` typed as a
list of names (`keys, mover, shooter`), and each behaviour's own parameters as
`mover.speed` and so on. The fields come from the manifest, so a new look or
behaviour is editable the moment it is declared; renaming a type renames it in
every entity and rule.
In a 2D room, `V` draws a polygon corner by corner on the canvas: with the
selected entity's type carrying a `hotspot` behaviour it becomes that hotspot's
outline, otherwise it is added to the room's walk areas. The panel steps
aside while the corners are pressed. `V` or `Enter` closes it, `Esc` drops
it.
*Rules*: the selected rule opens in place with its conditions and actions
under it, because a rule only means anything whole. `N` adds a rule, `E` picks
what it is on (the event's own parameters then appear as fields on the rule),
`C` adds a condition, `O` a condition in the rule's `any` group, `T` an action
-- each picked from the vocabulary with its help beside it -- and `[` and `]`
move the rule up and down the sheet, since rules run in order. `Delete`
removes the selected condition or action, or the rule itself.
*Tracks*: the room's tracks and their keys. The canvas shows the frame of the
disc layer's video under the cursor and every track's box there; `,` and `.`
step the cursor a frame, `[` and `]` leap ten. `N` adds a track, `K` a key at
the cursor -- taking the box interpolated there, or the nearest key's -- and a
key is moved by dragging its box on the canvas or by typing. `Delete` removes
the selected key, or the track. In a 3D room a new track is a rail of points,
drawn through the scene with its stops marked, and `K` puts a point where the
editor camera stands, looking where it looks: stand where the player should
and press it. `W` adds a *waves* track, whose keys are spawns: `at`, the
`type` made, `count` of them, `every` so many seconds apart, and where --
`from`, an entity or a comma-separated list of entities taken in turn, or
`x`, `y`, `z`. A waves track keyed by `stop` instead of `frame` or `time`
ties its waves to a rail camera's stops: `at` names the stop. A wave
begins once per visit to the room, and each instance it makes raises `spawn`
as any other.
*Dialogues*: an outline of every dialogue, the selected one's nodes under it,
the selected node's choices under that, and a choice's actions under that.
`A` adds at the level selected -- a dialogue, a node in it, a choice in the
node -- `T` adds an action to the selected choice from the vocabulary, and
`Delete` removes what is selected. A dialogue's `name` and `start`, a node's
`name`, `who`, `text`, `seconds`, and `next`, and a choice's `text`, `when`,
`next`, and `once` are typed like everything else.
`P` plays from the room on screen: the built game begins in it, with the
other rooms following in order. `X` releases the game into a folder named
for it in Forge's data directory, with everything it needs (see
<<_releasing_a_game>>).
On the 2D canvas the selected entity carries the yellow turn handle alone:
an arm the way its `rz` points, past its box, dragged round the entity to
turn it, and a sheet entity shows its first frame turned the same way. In
the types panel each type's picture stands beside its name -- the sprite
itself (one frame of a sheet), `Aa` for a text, or a swatch of the box's
colour -- so a type can be told from its neighbours without opening it.
`testScripts/scene82.singe` drives both.
=== The 3D viewport
A room in a game with a `scene3d` layer is shown as the scene itself, built
from the types' looks exactly as the runtime builds them, under an editor
camera that orbits whatever is selected: `J` and `L` orbit, `Y` and `H` tilt,
`I` and `K` close in and back off. Nothing steps -- no behaviours, no physics
-- so it is the description that is on screen, not a running game. An entity
is picked by where it projects and dragged across the floor it stands on, and
the selected one carries a gizmo: three arms along the world axes, red for
X, green for Y, blue for Z, each ending in a handle that drags the entity
along that axis alone -- the pointer's travel along the arm, as a fraction of
the arm, is the distance moved -- and a fourth, yellow, the way the entity
faces, whose handle dragged round the entity turns it about Y. `testScripts/scene63.singe` and `scene80`
drive it.
=== Keys
[cols="1,4"]
|===
| `Tab`, `1` to `5` | entities, types, rules, tracks, dialogues
| Up, Down | move through the list, and through the parts of the open rule or track
| Left, Right | slide the panel; while typing, move the caret (with `Home` and `End`)
| `PgUp`, `PgDn` | the previous or next room
| `Enter` | type a value for whatever is selected; again for its next value
| `Delete` | while typing, take out the character after the caret, or empty the field while its value is still selected whole
| `Esc` | put the value back and leave the form; otherwise close the description
| `A`, `D`, `Delete` | add, duplicate, delete an entity or a type
| `N` | a new rule, a new track, or (in the entities) a new room
| `W` | a new waves track (in the tracks)
| `E` | pick what the rule is on
| `C`, `O`, `T` | add a condition, an any-of condition, an action
| `[`, `]` | move the rule up or down; in the tracks, leap the cursor
| `,`, `.` | step the cursor
| `V` | draw a walk area, or the selected hotspot's outline, corner by corner
| `K` | a key at the cursor; in a 3D room, a rail point where the camera stands
| `J`, `L`, `Y`, `H`, `I`, `K` | in a 3D room: orbit, tilt, and zoom the editor camera
| `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`, `X` | save, build, play, release
|===
From a script the same work is `forgeEntityAdd`, `forgeEntityDuplicate`,
`forgeEntityDelete`, `forgeEntitySet`, `forgeTypeAdd`, `forgeTypeSet`,
`forgeRuleNew`, `forgeRuleOn`, `forgeRuleAdd("when"|"any"|"act", name)`,
`forgePartSet`, `forgePartDelete`, `forgeRuleMove`, `forgeTrackNew`,
`forgeKeyAdd`, `forgeCursorTo`, `forgeUndo`, and `forgeRedo`.
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 `testScripts/scene54.singe` asserts it.
=== Releasing a game
`X` in the editor, or `forgeExport(folder, name)` from a script, writes
everything a finished game needs into a directory of its own: the compiled
script, a `games.dat` so the menu lists it (its names begin with the folder's
own, `CoinRun/CoinRun.singe`, which is how both the menu and a packed copy
address them), the description it was built from
so it can be opened again, **a copy of the runtime**, taken out of Forge, and
**every file the description names** -- looks, sounds, music, the disc, models,
a HUD document -- at the same relative name it had, so the kit's
`Forge/kit/hero.png` lands in `Forge/kit/` under the release. The engine's
own files (`Singe/...`) and absolute paths are left where they are. The
compiled script notes its own directory in `AUTHOR_DIR`, and the runtime looks
for every named file there first, so the release runs wherever it is put.
`--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.
`forgeBuild(path)` compiles what is on screen (and puts the runtime beside it,
so the result plays); what `authorCheck` has to say about the description --
a type that is not declared, an expression that does not parse, a field a
look, a behaviour, or an event does not take (a misspelling, nine times in
ten) -- goes to the console and the panel.