Skip to content

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.