Aller au contenu

Les comportements

Un jeu fait dans l'App n'a besoin d'aucun Lua. Chaque modèle porte un comportement : un texte court, une règle par ligne, groupé par état, qui dit ce que font les objets de ce modèle. L'App le compile dans le .lua du niveau, sur un petit runtime qu'elle écrit dans le projet (papyrun.lua) ; la console et le firmware ne changent pas. Les objets ne se voient pas entre eux : ils partagent le pot (pot.score, pot.exit…) et reçoivent du moteur ce qui leur arrive — un contact, le sol, un mur, la manette. La fiche Game du projet ordonne les niveaux, lit le pot pour passer de l'un à l'autre, et dessine le HUD.

Tout est entier : des pixels, des pixels par trame, des trames (60 par seconde). Pas de décimale.

Un comportement

# 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

Les en-têtes, d'abord : family <nom> (ce que le hit <nom> des autres détecte ; sans lui le modèle n'est pas détecté et ne détecte rien), hits <nom>, <nom> (les familles ou modèles que celui-ci détecte au contact), solid (collisionne avec la carte), gravity (tombe ; la force est celle du niveau), prop <nom> = <défaut> (un paramètre que chaque objet posé peut régler sur sa fiche, lu par props.<nom> ; le défaut le type, un nombre ou un "texte").

Les états, ensuite : state <nom>: anim <a>, gravity off, solid off, through. Les attributs s'appliquent à l'entrée dans l'état et se défont à la sortie. Le premier état écrit est l'état de départ. Un décor est un comportement d'un état sans règle : state idle: anim idle. Un modèle sans comportement est simplement dessiné.

Les règles : when <condition>, <condition>: <action>; <action>. La virgule veut dire et. À chaque trame, les règles hors état s'évaluent d'abord, puis celles de l'état courant ; toute règle dont les conditions tiennent tire, dans l'ordre. state <nom> s'applique en fin de trame : la dernière gagne, une règle hors état gagne sur celles de l'état, et le même état changé en lui-même ne fait rien. Les règles enter et exit tirent la trame où l'objet entre dans l'état ou le quitte. # un commentaire jusqu'à la fin de la ligne.

Les conditions

Condition Vraie quand
always à chaque trame
enter / exit la trame où l'objet entre dans cet état / le quitte
not <cond> la condition ne tient pas
pad.a / pad.a.pressed / pad.a.released le bouton est tenu / vient d'être enfoncé / vient d'être relâché (left right up down a b x y l r start select) ; props.pad dit quelle manette, 1 par défaut
hit <modèle> / hit <famille> / hit any a touché un objet de ce modèle, de cette famille, ou n'importe quel objet détecté, depuis la trame d'avant
touch left (right, top, bottom) ce côté est contre un mur
on_ground / in_air un sol sous les pieds (quelques trames de grâce après un rebord comptent) / aucun. Un objet neuf n'est ni l'un ni l'autre à sa première trame
anim_done l'animation non bouclée a montré sa dernière image
offscreen entièrement hors de la carte
ladder_here / ladder_below / platform_below une case d'échelle sous le milieu des pieds / juste sous les pieds / une plateforme juste sous les pieds
after 90 exactement 90 trames passées dans cet état
every 30 toutes les 30 trames dans cet état
chance 1 in 10 une fois sur dix, au hasard (le hasard semé de la console : un rejeu donne la même chose)
x < 100 (== != < <= > >=) deux valeurs entières comparées ; une valeur seule (when me.cool:) veut dire non nulle

Les actions

Action Effet
vx = … / vy = … ma vitesse, en pixels par trame (vy négatif monte) ; le moteur me déplace et m'arrête aux murs
impulse dx, dy ajoute à ma vitesse
x = … / y = … me téléporte
anim <nom> joue cette animation (un état du modèle)
state <nom> change d'état en fin de trame
face left / face right retourne l'image (dessinez les sprites tournés vers la droite)
pot.<clé> = … / += / -= écrit le pot commun
me.<clé> = … / += / -= écrit mon carnet privé (chaque objet a le sien ; me.x0 ci-dessus)
spawn <modèle> at <x>, <y> with <prop> = <valeur>, … crée un objet de ce modèle à ces pixels, avec ses props. Le modèle n'a pas besoin d'être posé dans le niveau : la compilation l'emporte
destroy je disparais en fin de trame
sound <nom> / music <nom> loop / music stop un effet sonore, une musique du projet
camera follow la caméra me suit, bornée à la carte
climb <vitesse> / drop sur une échelle : ↑ et ↓ me déplacent, rien de tenu m'immobilise, le dernier barreau pose mes pieds au-dessus / à travers la plateforme sous mes pieds
gravity on|off, solid on|off, through on|off mes drapeaux physiques, jusqu'au prochain changement d'état

Les valeurs : x y w h (ma boîte, en pixels), vx vy (pixels par trame), facing (1 à droite, -1 à gauche), time (trames dans cet état), frame (trames depuis le début du niveau), random(a, b), props.<clé>, pot.<clé>, me.<clé> (0 si absente), + - * / % et les parenthèses ; / est une division entière.

Les contacts, les murs, le sol et anim_done sont ce que le pas de la dernière trame a produit : une règle les voit une trame plus tard, pareil pour tout le monde.

Le joueur est un comportement

Sur la fiche d'un modèle, Make this the player y pose le préréglage platformer : manette, marche et saut en props (prop pad = 1, prop speed = 2, prop jump = 5), solid, gravity, camera follow, gauche et droite en miroir, les états idle, walk, jump, fall et climb sur les états du modèle qui portent ces noms quand ils existent. C'est un comportement comme un autre : ouvrez-le, lisez-le, changez-le. Ses règles disent ce que le moteur fait d'un platformer : ↓ + A sur une plateforme la traverse, ↑ sur une échelle l'accroche, A au sol saute (quelques trames de grâce après un rebord), relâcher ↑ et ↓ au-dessus du dernier barreau pose debout sur le sommet. L'objet marqué P dans un niveau est celui dont le comportement est family player ; un objet neuf se pose juste sous lui dans l'ordre de dessin.

La gravité d'un niveau est une seule valeur pour tous les objets qui ont gravity : 0,25 px/trame² par défaut dès qu'un comportement tombe, ou le réglage propre du niveau (gravite dans sa compilation, en 1/256 px/trame²).

Où vit un comportement

Sur le modèle, pour tous les objets posés depuis lui ; chaque objet le règle par ses props (la fiche liste celles qui sont déclarées, avec leur défaut). Un objet peut à la place prendre sa propre copie (Make it this object's own sur sa fiche) : le comportement du modèle ne le concerne alors plus. Une copie est complète, jamais une surcharge partielle ; Back to the model's la retire.

Un comportement est vérifié à la frappe (le serveur le lit ; les erreurs se voient dans la gouttière, avec leur ligne) et s'enregistre par Ctrl+S. Enregistrer réécrit le .lua de chaque niveau qui pose le modèle, et quand le jeu tourne dans Play, le nouveau comportement y entre sans relance : les objets concernés repartent de leur premier état. Un comportement qui nomme un état que le modèle n'a plus est refusé, comme retirer d'un modèle un état que son comportement nomme.

Les props déclarées en nombre refusent un texte sur l'objet. Au plus 64 règles par comportement, 32 états, 16 props ; 32 familles par niveau (les couches du moteur) ; 256 objets vivants à la fois (spawn ne fait alors rien).

Le pot

Le pot est une seule table partagée par tous les objets et par le jeu : pot.score, pot.coins, pot.exit, pot.dead, la clé que vous voulez. Une clé absente se lit 0. pot.score, quand c'est un entier, est le score que la console enregistre pour un rejeu. Le panneau Pot (à côté d'Output) montre le pot pendant que le jeu tourne, l'état du jeu, et pour l'objet choisi dans un niveau son état courant, ses trames et son carnet ; son champ écrit une clé (lives = 1).

La fiche Game

Sur la fiche du projet, Game ouvre la configuration du jeu — le même langage, dans le même éditeur — et en écrit main.lua :

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 est l'écran titre (majuscules et chiffres) ; levels les niveaux compilés dans ce projet, dans l'ordre ; start le pot au début d'une partie — ces clés survivent d'un niveau à l'autre, toute autre clé est effacée quand un niveau se charge (c'est là que vivent pot.exit et pot.dead) ; music title et music level les musiques (des .son du projet). Les règles lisent le pot, la manette, after et every, et font next level (après le dernier, la partie est gagnée), restart level, game over, win, pot.k = …, music, sound. hud "LABEL" <valeur> at x, y [size 1|2] affiche une ligne à chaque trame d'un niveau (gfx.print : une entrée de display list par caractère, 256 en tout avec les sprites).

Le main.lua généré a un écran titre, les écrans de game over et de victoire, et START (ou A) pour jouer. Il porte sa configuration dans son en-tête et est en lecture seule dans l'éditeur ; un main.lua écrit à la main n'est jamais remplacé sans demander. Le jeu expose une globale JEU (etat : title, level, over, won ; niveau) que le panneau Pot lit.

Les limites, et Lua toujours

Pas de or, pas de boucle, pas de vue sur un autre objet : c'est voulu. Ce que le langage ne dit pas s'écrit en Lua, comme toujours : un main.lua à la main, qui appelle niveau.update() et niveau.dessiner() de chaque niveau, lit pot et niveau.acteurs (un record par objet : .a l'acteur, .o l'objet, .etat son état, .me son carnet) ; voir le fichier <niveau>.lua et phy.