Behaviours¶
A game made in the App needs no Lua. Each model carries a behaviour: a short text, one rule per line, grouped by state, that says what the objects of that model do. The App compiles it into the level's .lua, on a small runtime it writes into the project (papyrun.lua); the console and the firmware do not change. Objects never see each other: they share the pot (pot.score, pot.exit…) and receive from the engine what happened to them — a contact, the ground, a wall, the pad. The Game sheet of the project orders the levels, reads the pot to go from one to the next, and draws the HUD.
Everything is an integer: pixels, pixels per frame, frames (60 per second). There is no decimal.
A behaviour¶
# A patrolling enemy: walks back and forth around where it was placed,
# turns at walls, dies when a shot touches it.
family enemy
hits player, shot
solid
gravity
prop speed = 1
prop range = 48
state walk: anim walk
when enter: me.x0 = x; vx = -props.speed; face left
when x < me.x0 - props.range: vx = props.speed; face right
when x > me.x0 + props.range: vx = -props.speed; face left
when touch left: vx = props.speed; face right
when touch right: vx = -props.speed; face left
when hit shot: pot.score += 10; sound pop; state die
state die: anim die, solid off
when enter: vx = 0; vy = -2
when anim_done: destroy
Headers, first: family <name> (what hit <name> of the others detects; without it the model is not detected and detects nothing), hits <name>, <name> (the families or models this one detects on contact), solid (collides with the map), gravity (falls; the strength is the level's), prop <name> = <default> (a parameter each placed object can set on its sheet, read as props.<name>; the default types it, a number or a "text").
States, then: state <name>: anim <a>, gravity off, solid off, through. The attributes apply on entering the state and revert on leaving it. The first state written is the starting one. A decoration is a behaviour of one state and no rule: state idle: anim idle. A model without a behaviour is simply drawn.
Rules: when <condition>, <condition>: <action>; <action>. A comma means and. Every frame, the rules outside the states run first, then those of the current state; every rule whose conditions hold fires, in order. state <name> applies at the end of the frame: the last one wins, a rule outside the states wins over the state's own, and the same state again changes nothing. enter and exit rules fire the frame the object enters or leaves the state. # a comment to the end of the line.
Conditions¶
| Condition | True when |
|---|---|
always |
every frame |
enter / exit |
the frame the object enters this state / leaves it |
not <cond> |
the condition does not hold |
pad.a / pad.a.pressed / pad.a.released |
the button is held / was just pressed / was just released (left right up down a b x y l r start select); props.pad says which gamepad, 1 by default |
hit <model> / hit <family> / hit any |
touched an object of that model, of that family, or any detected object, since the previous frame |
touch left (right, top, bottom) |
that side is against a wall |
on_ground / in_air |
a floor under the feet (a few frames of grace after a ledge count) / none. A new object is neither on its first frame |
anim_done |
the non-looping animation has shown its last image |
offscreen |
entirely outside the map |
ladder_here / ladder_below / platform_below |
a ladder cell under the middle of the feet / just below the feet / a platform just below the feet |
after 90 |
exactly 90 frames spent in this state |
every 30 |
every 30 frames in this state |
chance 1 in 10 |
once in ten, at random (the console's seeded random: a replay gives the same) |
x < 100 (== != < <= > >=) |
two integer values compared; a value alone (when me.cool:) means not zero |
Actions¶
| Action | Effect |
|---|---|
vx = … / vy = … |
my speed, pixels per frame (negative vy goes up); the engine moves me and stops me at walls |
impulse dx, dy |
adds to my speed |
x = … / y = … |
teleports me |
anim <name> |
plays that animation (a state of the model) |
state <name> |
changes state at the end of the frame |
face left / face right |
mirrors the image (draw the sprites facing right) |
pot.<key> = … / += / -= |
writes the shared pot |
me.<key> = … / += / -= |
writes my private notebook (each object has its own; me.x0 above) |
spawn <model> at <x>, <y> with <prop> = <value>, … |
creates an object of that model at those pixels, with its props. The model need not be placed in the level: the compilation brings it in |
destroy |
I disappear at the end of the frame |
sound <name> / music <name> loop / music stop |
a sound effect, a music of the project |
camera follow |
the camera follows me, clamped to the map |
climb <speed> / drop |
on a ladder: ↑ and ↓ move me, nothing held keeps me still, the top rung lays my feet above / through the platform under my feet |
gravity on|off, solid on|off, through on|off |
my physics flags, until the next state change |
Values: x y w h (my box, pixels), vx vy (pixels per frame), facing (1 right, -1 left), time (frames in this state), frame (frames since the level started), random(a, b), props.<key>, pot.<key>, me.<key> (0 when unset), + - * / % and parentheses; / is an integer division.
Contacts, walls, the ground and anim_done are what the last frame's step produced: a rule sees them one frame later, the same for everyone.
The player is a behaviour¶
On a model's sheet, Make this the player puts the platformer preset on it: pad, walk and jump as props (prop pad = 1, prop speed = 2, prop jump = 5), solid, gravity, camera follow, left and right mirrored, the states idle, walk, jump, fall and climb using the model's states of the same name when they exist. It is a behaviour like any other: open it, read it, change it. Its rules say what the engine does for a platformer: ↓ + A on a platform drops through it, ↑ on a ladder grabs it, A on the ground jumps (a few frames of grace after a ledge), releasing ↑ and ↓ above the last rung stands on the top of the ladder. The object marked P in a level is the one whose behaviour is family player; a new object lands just below it in the draw order.
The gravity of a level is one value for every object that has gravity: 0.25 px/frame² by default when a behaviour falls, or the level's own setting (gravite in its compilation, in 1/256 px/frame²).
Where a behaviour lives¶
On the model, for every object placed from it; each object tunes it with its props (the sheet lists the declared ones, with their default). An object can instead take its own copy (Make it this object's own on its sheet): the model's behaviour then no longer affects it. A copy is complete, never a partial override; Back to the model's drops it.
A behaviour is checked as you type (the server reads it; errors show in the gutter with their line) and saved with Ctrl+S. Saving rewrites the .lua of every level that places the model, and when the game is running in Play, the new behaviour enters it without a restart: the objects concerned start again from their first state. A behaviour that names a state the model no longer has is refused, as is removing from a model a state its behaviour names.
Props declared as a number refuse a text on the object. Up to 64 rules per behaviour, 32 states, 16 props; 32 families per level (the engine's layers); 256 objects alive at once (spawn then does nothing).
The pot¶
The pot is one table shared by every object and by the game: pot.score, pot.coins, pot.exit, pot.dead, any key you like. An unset key reads as 0. pot.score, when it is an integer, is the score the console records for a replay. The Pot panel (next to Output) shows the pot while the game runs, the state of the game, and for the object chosen in a level its current state, its frames and its notebook; its field writes a key (lives = 1).
The Game sheet¶
On the project's sheet, Game opens the configuration of the game — the same language, in the same editor — and writes main.lua from it:
title CRAB CASTLE
levels castle1, castle2
start lives = 3, score = 0
music title theme
music level stage
when pot.exit: next level
when pot.dead: pot.lives -= 1; restart level
when pot.lives <= 0: game over
hud "SCORE" pot.score at 8, 8 size 2
hud "LIVES" pot.lives at 536, 8
title is the title screen (uppercase letters and digits); levels the levels compiled into this project, in order; start the pot at the start of a game — these keys survive from level to level, every other key is cleared when a level loads (that is where pot.exit and pot.dead live); music title and music level the musics (.son of the project). The rules read the pot, the pad, after and every, and do next level (after the last one the game is won), restart level, game over, win, pot.k = …, music, sound. hud "LABEL" <value> at x, y [size 1|2] prints a line every frame of a level (gfx.print: one display-list entry per character, 256 in all with the sprites).
The generated main.lua has a title screen, the game over and the win screens, and START (or A) to play. It carries its configuration in its header and is read-only in the editor; a main.lua written by hand is never replaced without asking. The game exposes a global JEU (etat: title, level, over, won; niveau) that the Pot panel reads.
Limits, and Lua still¶
No or, no loop, no view on another object: that is the design. What the language does not say is written in Lua, the way it always was: main.lua by hand, calling level.update() and level.dessiner() of each level, reading pot and level.acteurs (one record per object: .a the actor, .o the object, .etat its state, .me its notebook); see the <level>.lua file and phy.