Designing a kind
A thing with rules, and things acting on each other.
A kind is one sort of thing with rules, such as a coin, a door, a score or a monster. Its rule is <kind>.sim.ts and its drawing <kind>.client.ts, both in games/<game>/.
Where each value goes
| The value | Goes in |
|---|---|
| Changes while the game runs | A field of the row, starting from the kind's prefab, prefabs.<kind> in the scene file. |
| Fixed, the kind's own (speed, size, colour, reach, worth) | Its definition, the kind's entry in assets/<game>.json. |
| Fixed, the scene's own | The scene's world. |
| Large and fixed (a table, a level) | A data file of its own in assets/, listed in the scene's catalogue and named by a field of the definition. |
| Used only by the page (volume, sound on or off) | A key of the store. |
A thing whose fixed numbers differ from another's is another kind with its own definition. No number is written in the rule or the drawing.
Making the kind
- Decide whose value it is. What a seat owns, its health, ammo, lives or score, is a field of its
playerrow (Designing a seat). A value no seat owns, such as a team's total or a match clock, is a kind of its own. A new system is a new kind, never a field added to an existing kind. - Name it with a lower-case identifier that is not the game's name or a plugin's name.
- Write its definition in
assets/<game>.json. - Write its prefab in the scene file with every changing field at its starting value, or
{}. A thing that stands somewhere haspos. - Write its rule,
games/<game>/<kind>.sim.ts, exporting<kind>: Kind. - Import the rule in
<scene>.sim.tsand list it insim, including kinds that are only spawned. - Place its rows: one
actorsentry for a single thing in the scene, an entry per thing placed, or rows made byw.spawnwhile the game runs. - Write its drawing,
<kind>.client.ts, if it is drawn.
The editor's Kinds panel, New kind, writes a blank kind or a copy of another kind in one undoable change.
The rule
- A rule runs in
before,tickandafter, each(me, dt, w), and in itsacts. Multiply rates bydt, the seconds the step covers. - Each step answers the seats' messages, then runs every row's
before, then everytick, then everyafter. A rule that reads what another row did this step reads it inafter. - A seat's message runs an act only on the row it possesses, or an act listed in the kind's
open. Check arguments that came from a seat. - Anything a kind remembers between steps is a field on its row. Write array fields whole (
me.data.pos = [x, y]) and spawn with copies of another row's arrays.
The drawing
<kind>.client.ts exports setup(row, plugins), tick(row, plugins) and remove(plugins). It draws through the scene's renderer, reads changing values off row.data and numbers off the definition, and sets its variables in setup. Files such as pictures come through the loader. It is set up for every row of its kind, so a drawing that wants the seat's own row finds it with possessed(plugins.wire.rows).
Kinds acting on each other
- One kind never imports another's file, and never reads another kind's definition.
- Find rows with
w.find(kind),w.all(kind)orw.get(id). Usew.all(kind)for a row that may have ended. - Change another row by calling its act,
w.call(row, act, seat, ...args), so each field is written in one file. A hook passes-1as the seat. - An act on the
playerrow that other kinds call returns at once when a seat sent it,if (seat >= 0) return, so a seat cannot send it to itself. - Sounds and effects the page plays once are raised on audio's or a particle plugin's ledger, never as a field set for one step.
A coin with definition { "reach": 24, "worth": 5, "size": 16, "colour": "#f2a93b" } that gives its worth to the seat that touches it and counts itself on a tally:
// coin.sim.ts
export const coin: Kind = {
after(me, _dt, w) {
const { reach, worth } = def(w, me.kind)
const [x, y] = me.data.pos
const by = w.all('player').find(p => math.hypot(p.data.pos[0] - x, p.data.pos[1] - y) <= reach)
if (!by) return
w.call(by, 'award', -1, worth)
w.call(w.find('tally'), 'add', -1, 1)
w.end(me)
},
}
// player.sim.ts, among its acts
award(me, _w, seat, worth: number) {
if (seat >= 0) return
me.data.score += worth
},
Shared code
Code several kinds share, or a system of the game's such as its level or HUD, is a plugin of the game's own in games/<game>/plugins/<name>/. Its <name>.sim.ts holds functions a rule imports; its <name>.plugin.ts draws every row of the kinds that share a drawing and is loaded by shipped. It reads numbers off the definition of each row it is handed. See Building a plugin.
Random numbers and maths
w.die() rolls from 0 up to 1 from the scene's world.seed, so a replay rolls the same numbers. A rule never calls Math.random. For sin, cos, pow, hypot and the other functions whose results differ between JavaScript engines, a rule uses math; sqrt, floor, min, max and the like stay on Math.
Checking it
- Write
<kind>.sim.test.tsbeside the rule, booting the game's manager, joining a seat and reading what the kind wrote off the rows. Run it withyarn vitest run games/<game>/<kind>.sim.test.ts. - Run
yarn editor <game>and openlocalhost:5173/games/<game>/index.html. The thing is drawn where its row says, acts on what it should, and leaves nothing drawn when it ends.