Aller au contenu

phy : le moteur de jeu

Un acteur est une boîte qui se déplace et bute. Il ne sait pas se dessiner : c'est le jeu qui appelle gfx.sprite, l'ordre de peinture lui appartenant.

Le moteur est le même code sur la carte et dans le simulateur, et il donne les mêmes positions au bit près. C'est ce qui rend une trace rejouable (voir les règles).

Les identifiants sont 1-based. Un slot libéré laisse un trou, et l'allocation reprend le plus petit index libre : le résultat d'un pas ne dépend donc pas de l'ordre dans lequel le jeu a créé ses acteurs.

Toutes ces fonctions refusent les flottants. phy.PX vaut 65536 : écrivez 3 * phy.PX pour trois pixels, et // là où vous auriez écrit / (voir entiers et flottants).

Le motif de collision

phy.map(nom | nil) → w, h

Charge le motif de collision du niveau — sa géométrie : ce sur quoi on marche, ce contre quoi on bute. Rend ses dimensions en pixels. phy.map(nil) retire toute géométrie.

Deux formes, et la première est celle qu'on écrit. nom.mask est un dessin ASCII, un caractère par case de 8 × 8 px — 1:1 avec les tuiles du fond : soixante lignes de quatre-vingts signes pour un écran, # pour la matière, tout le reste est de l'air. ~4,9 Kio, qui s'éditent dans l'éditeur du simulateur comme un script, se relisent dans un diff — et que le Map editor de l'App génère depuis les tuiles taguées « solide ».

Les dimensions se lisent dans le texte — le nombre de lignes, la plus longue ligne — avec l'écran pour plancher : un monde plus grand que l'écran (jusqu'à 256 × 128 tuiles) écrit simplement un fichier plus grand. nom.msk est le format binaire, un bit par pixel, produit par un outil — pour ce qui se génère, et le jour où un motif demandera la précision du pixel. Le .mask est essayé d'abord.

Deux signes, pas trois : # est solide, tout le reste est vide. Les lignes manquantes sont vides, les caractères manquants aussi. Le dehors est solide — un acteur ne sort pas du monde. Changer de motif vide le monde.

phy.solid(x, y) → booléen

Le motif à ce pixel. C'est ce qui permet à un jeu de dessiner son niveau depuis sa géométrie au lieu d'en tenir une seconde copie en Lua. Le jeu de plateforme de Votre premier jeu construit sa liste de blocs comme ça, une fois au chargement. Hors carte, elle rend ce que le motif dit du dehors. Sans phy.map(), c'est une erreur.

Le monde et les acteurs

phy.gravity(gx, gy)

Le vecteur de gravité du monde, en pixels par trame au carré. Seuls les acteurs portant phy.GRAVITY le subissent.

phy.new(w, h) → id / phy.free(id)

Crée un acteur dont la boîte de collision fait w × h pixels ; sans drapeau, il ne percute rien et ne tombe pas. La boîte ne dérive pas du bitmap : une frame qui change de parité ferait vibrer l'acteur contre le sol. Le lien entre la boîte et le dessin est l'ancre. phy.free libère le slot, rien n'est déplacé.

phy.pos(id [, x, y]) → x, y / phy.vel(id [, vx, vy]) → vx, vy

Lisent, ou posent puis rendent. En Q16.16 : phy.pos(a, 320 * phy.PX, 0).

phy.vel écrit les deux composantes

Un axe piloté par la physique doit être relu avant d'être reposé, sinon la gravité est annulée à chaque trame :

local vx = 0
local _, vy = phy.vel(joueur)   -- on GARDE la vitesse verticale acquise
if p & pad.LEFT ~= 0 then vx = -MARCHE end
phy.vel(joueur, vx, vy)

phy.px(id) → x, y

La position de la boîte en pixels entiers, au plancher.

phy.box(id [, w, h]) → w, h / phy.anchor(id [, ax, ay]) → ax, ay

L'ancre est le décalage entre le coin de la boîte et le coin du bitmap. phy.anchor(a, W // 2, H) fait que la position de l'acteur désigne le milieu du bas du sprite — son pied.

phy.flags(id [, drapeaux]) → drapeaux

phy.MASK : bute contre le motif de collision. phy.GRAVITY : subit la gravité du monde. Un drapeau ne crée rien : il dit à quoi l'acteur est sensible.

phy.layer(id [, couche, masque]) → couche, masque

couche est un numéro de 0 à 31 ; masque est l'ensemble des couches que cet acteur percute — 1 << autre_couche. La détection est un OU : deux acteurs se signalent si l'un des deux le demande.

Une trame de monde

phy.step() → franchissements, saturés

Une trame de monde, appelée explicitement depuis _update(). Dans l'ordre : gravité, écrêtage de vitesse, déplacement en x contre le motif puis en y, sonde de sol, détection des recouvrements. Rend le nombre de franchissements de pixels consommés — à comparer aux 4 096 du budget — et le nombre d'acteurs déplacés sans collision parce que ce budget était épuisé. Le déplacement visite chaque pixel franchi : un tir à 100 px/trame ne traverse pas un mur.

phy.contacts() → n, perdus / phy.contact(k) → a, b

Les recouvrements détectés au dernier phy.step(), à parcourir de 1 à n. a < b toujours, et l'ordre est stable d'une exécution à l'autre. Les contacts se drainent, ils ne rappellent pas. Le test porte sur la boîte balayée — l'union du départ et de l'arrivée — sinon une balle rapide passerait au travers d'un ennemi lent.

phy.ground(id) → trames, au_sol / phy.touch(id) → masque

phy.ground rend le nombre de trames depuis le dernier contact avec le sol et le verdict du coyote time : vrai jusqu'à quatre trames après avoir quitté une corniche. phy.touch rend ce que l'acteur a heurté pendant cette trame : phy.LEFT, phy.RIGHT, phy.UP, phy.DOWN.

Les animations

phy.anim_load(prefixe [, cadence, boucle]) → id, n

Charge prefixe0, prefixe1, … dans l'atlas du bundle ('game/flamme'game/flamme0, game/flamme1…) jusqu'à ce qu'il n'y en ait plus — et si rien ne vient, réessaie avec un soulignement : 'game/marche' trouve game/marche_0, game/marche_1…, la forme que l'éditeur de bundles produit. Le séparateur n'est jamais à écrire. Rend l'identifiant de l'animation et son nombre d'images. cadence est en trames par image (6 par défaut) ; boucle vaut true par défaut, à false l'animation se fige sur sa dernière image.

phy.anim(id [, anim]) → anim / phy.anim_done(id) → booléen / phy.sprite(id) → sprite, x, y

Changer d'animation la reprend au début. phy.anim_done n'est vrai que pour une animation à coup unique terminée. phy.sprite rend les trois valeurs qui alimentent exactement gfx.sprite :

gfx.sprite(phy.sprite(a))

Chaque acteur a son propre compteur, remis à zéro à sa création : six torches partageant une animation ne battent pas ensemble.