635 lines
32 KiB
Text
635 lines
32 KiB
Text
= Forge
|
|
Scott Duensing <scott@kangaroopunch.com>
|
|
:revnumber: 3.00
|
|
:revdate: 2026
|
|
:doctype: book
|
|
:toc: left
|
|
:toclevels: 3
|
|
:sectnums:
|
|
:sectnumlevels: 3
|
|
:source-highlighter: rouge
|
|
:icons: font
|
|
:experimental:
|
|
|
|
[preface]
|
|
== About Forge
|
|
|
|
Forge is Singe's authoring tool: a way to make a game by describing it --
|
|
placing things, attaching behaviours, writing rules -- and having ordinary
|
|
Singe Lua written for you. It is itself a Singe game and is distributed on
|
|
its own, beside the engine rather than inside it. This document covers the
|
|
description format, the vocabulary, the compiler, and the editor. The engine
|
|
calls the generated code makes (`playerNew`, `collidePointRect`,
|
|
`onKeyPressed`, `scriptPush`, and the rest) are documented in the Singe Manual.
|
|
|
|
== Describing a Game Instead of Writing One
|
|
|
|
A game can be written as a *description* -- a table of kinds, 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.
|
|
|
|
*Kinds* 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 kind.
|
|
|
|
*Rooms* are where things are. A room lists its `entities` -- a kind, 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 kinds, a shot landing, a death, a timer, a disc
|
|
frame passing -- and `each` scopes the rule to every instance of a kind, 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 },
|
|
kinds = {
|
|
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 = { { kind = "ground", id = "ground", x = 360, y = 440 },
|
|
{ kind = "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 kind'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 kind `a` began touching one of kind `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 kind `b` entered or left a trigger of kind `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 kind; `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 kind
|
|
| `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 kind 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 kinds 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 kind
|
|
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 kind 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 kinds and rooms. Scenes 64 and 65 play them.
|
|
|
|
=== Vehicles, crowds, turrets, and physics toys
|
|
|
|
`vehicle` puts a kind 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 kind 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 kind 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 kind, 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),
|
|
`kind`, `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.
|
|
|
|
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 *kinds*, the
|
|
*rules*, the room's *tracks*, and the *dialogues*. Whatever is selected is edited the same way:
|
|
`Enter` walks its fields and each is typed; `Enter` again moves to the next,
|
|
`Esc` puts the value back. A typed number comes back a number, because `100`
|
|
and `"100"` compile to different source.
|
|
|
|
*Entities*: `A` adds one of the selected kind in the middle of the canvas,
|
|
`D` duplicates the selected one, `Delete` removes it, and dragging moves it.
|
|
Its fields are `id`, `kind`, `x`, `y`, `z`, `scale` -- and in a 3D room
|
|
`rx`, `ry`, `rz`, its turn about each axis in degrees -- and each var its kind
|
|
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. `PgUp` and `PgDn` move between rooms; `N` adds a room. With nothing
|
|
selected, `Enter` edits the room itself: its `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`).
|
|
|
|
In every picker, typing narrows the list to the names that contain what was
|
|
typed, and `Backspace` widens it again. A field that names a file -- a look's
|
|
image or model, a sound, a HUD document -- opens a picker of the files under
|
|
the game's directory instead of a prompt; `Esc` on it leaves the field to be
|
|
typed.
|
|
|
|
*Kinds*: `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 kind 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 kind carrying a `hotspot` behaviour it becomes that hotspot's
|
|
outline, otherwise it is added to the room's walk areas. `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.
|
|
|
|
*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.
|
|
|
|
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 kinds panel each kind'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 kind 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 kinds' 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` | entities, kinds, rules, tracks
|
|
| Up, Down | move through the list, and through the parts of the open rule or track
|
|
| Left, Right | slide the panel
|
|
| `PgUp`, `PgDn` | the previous or next room
|
|
| `Enter` | type a value for whatever is selected; again for its next value
|
|
| `Esc` | put the value back; otherwise close the description
|
|
| `A`, `D`, `Delete` | add, duplicate, delete an entity or a kind
|
|
| `N` | a new rule, a new track, or (in the entities) a new room
|
|
| `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` | save, build, play
|
|
|===
|
|
|
|
From a script the same work is `forgeEntityAdd`, `forgeEntityDuplicate`,
|
|
`forgeEntityDelete`, `forgeEntitySet`, `forgeKindAdd`, `forgeKindSet`,
|
|
`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
|
|
|
|
`forgeExport(folder, name)` writes everything a finished game needs into a
|
|
directory of its own: the compiled script, a `games.dat` so the menu lists it,
|
|
the description it was built from so it can be opened again, and **a copy of
|
|
the runtime**, taken out of Forge. `--pack` turns that directory into a `.game`
|
|
like any other, and it runs on a machine that has never had Forge on it.
|
|
|
|
Every built game finds its own directory to load that runtime from, using
|
|
`debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the
|
|
engine was *launched* with, so a game reached by `dofile` -- a test, a
|
|
launcher, a preview -- would otherwise look beside the caller.
|
|
|
|
`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 kind 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.
|