1757 lines
86 KiB
Text
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.
|