Le JavaScript dans l'Engine
L'API de script de l'Engine : le cycle de vie, les pièges de coordonnées, les trois façons de créer un objet, et comment en déplacer un.
En un mot#
Chaque entité peut porter un script JavaScript. Il s'exécute dans le navigateur, dans votre onglet, contre la scène vivante, et c'est l'API de script canonique : le dialecte BASIC, et les vues Python, C# et Rust de l'éditeur, compilent tous vers la même surface.
La liste exhaustive est la référence de l'API : 538 membres en 50 espaces de noms, une page chacun, avec une recherche. Cette page-ci est ce qu'il faut savoir avant de la lire.
Le cycle de vie#
function start() {} // une fois, au démarrage de l'entité
function update(dt) {} // à chaque image, dt en secondes
function fixedUpdate(dt) {} // pas fixe, pour la physique
function onCollisionEnter(other) {} // première image de contact
function onCollisionStay(other) {} // à chaque image de contact
function onCollisionExit(other) {} // le contact vient de finir
function onTriggerEnter(other) {} // entrée dans un volume déclencheur
function onTriggerExit(other) {}
function onMessage(msg, data, senderId) {} // messagerie entre entités
function onAnimationEvent(name, data) {}
function onDestroy() {}
function main() {} // entrée du jeu console, async permis
deltaTime, time, entityId et entity sont des variables globales ;
getComponent(type) lit un composant sur l'entité qui porte le script.
Collisions et déclencheurs se produisent en 2D comme en 3D, et chaque côté
reçoit sa propre normale de contact : other.contact.normal pointe de l'autre
entité vers celle-ci. C'est ce qui permet à un script de distinguer « j'ai
atterri dessus » de « il m'est tombé dessus », un saut sur la tête se lit
other.contact.normal.y > 0.5.
Les deux pièges de coordonnées#
Les deux coûtent de vraies heures de débogage, et aucun n'échoue bruyamment.
Souris : page contre canevas#
input.mousePosition est en coordonnées page. scene.pick() veut des
coordonnées canevas. Passer l'une pour l'autre ne rate pas la cible, cela
touche un point décalé de la position de la vue dans la fenêtre, si bien qu'un
héros commandé au clic marche quelque part de plausible et de faux.
Appelez scene.pick() sans argument (il vise le curseur), ou utilisez
input.mouseViewport quand il vous faut les nombres.
Les commandes virtuelles sont en pixels canevas#
input.addVirtualJoystick(id, x, y, size) et input.addVirtualButton(...)
placent leur centre dans le même espace que mouseViewport. Un joystick rend un
X/Y normalisé après sa zone morte radiale ; un bouton expose séparément tenu,
pressé cette image et relâché cette image. Leur pointeur est capturé et
consommé, donc un appui sur la commande n'est pas aussi rapporté comme une
action brute sur le canevas.
Le joystick par défaut est unifié.
input.getJoystickX/Y()choisit un vecteur complet parmi le stick à l'écran, une manette physique ou le clavier (WASD/flèches), selon la source la plus active. Une manette branchée mais immobile ne désactive donc plus le clavier. Son axe Y suit l'espace écran 2D : le haut vaut-1.
Trois façons de créer un objet, et elles ne sont pas interchangeables#
| Ce que ça crée | Durée de vie | |
|---|---|---|
scene.createEntity(def) | une entité d'auteur : elle apparaît dans le Scene Graph et l'Inspecteur | le projet, elle est enregistrée |
game.spawn(type, pos) | une primitive nue : ni vie, ni IA, ni comportement | la session de jeu |
game.spawnFrom(template, pos, opts) | une copie d'entité entière : composants, corps physique, tags, calque, échelle et la pile de comportements compilée | la session de jeu |
game.spawn convient aux débris et aux prototypes, et ne sert à rien pour un
ennemi, un générateur nourri par lui produit des boules inertes. Le modèle que
copie spawnFrom est une entité ordinaire que vous composez dans l'éditeur puis
éteignez avec l'œil de la hiérarchie : pas de format à part, pas de second
éditeur, donc ce que vous voyez est ce qui apparaîtra.
// Une fois, au démarrage : un modèle absent doit le dire, pas rater chaque vague en silence
if (!game.hasTemplate('Skeleton')) console.warn('no Skeleton template');
const mob = game.spawnFrom('Skeleton', { x, y, z }, { tag: 'Enemy' });
template accepte un identifiant, un nom ou un tag. Les copies faites pendant le
jeu sont limitées à la session : stop() les retire, et la scène d'édition n'est
jamais polluée.
Éteindre une entité veut dire invisible ET muette : nœud désactivé, script arrêté, corps physique qui ne répond plus. La cacher seulement laisserait un ennemi invisible continuer à vous frapper.
game.setEnabled(id, on)le pilote depuis un script, l'icône en œil depuis la hiérarchie.
Déplacer quelque chose : pousser un corps, pas téléporter#
Écrire transform.position à chaque image traverse les murs, les autres
monstres et le sol. Pilotez la vitesse : mais seulement s'il y a quelque
chose à piloter :
if (physics.hasBody) {
const v = physics.getVelocity();
physics.setVelocity({ x: vx, y: v.y, z: vz }); // pousse un vrai corps
} else {
transform.position.x += vx * dt; // légitime pour un volant
}
physics.setVelocity sans corps remplit un champ que personne n'intègre, et
l'entité s'arrête net. C'est pour cela que physics.hasBody existe, et pourquoi
tous les comportements mobiles du catalogue posent la question au lieu de
supposer la réponse. S'arrêter, c'est annuler la vitesse : ne rien faire
laisse le corps en roue libre.
Écrire une position déplace bien un corps physique, désormais. Havok pilote la transformation, jamais l'inverse : affecter
transform.positionsur un corps dynamique était écrasé au pas suivant, donc toute téléportation échouait en silence sur une entité physique. Le proxy de position resynchronise maintenant le corps, comme le proxy de rotation l'a toujours fait.
Le pathfinding est un instantané#
pathfinding.createGrid() lance une grille de rayons une fois, centrée là où
il a été appelé. Sortir de cette boîte rend null, et la plupart des appelants
retombent alors sur une ligne droite, à travers les murs, sans un mot.
La grille se recentre par défaut sur le trajet demandé.
setAutoRecenter(false) permet de s'en passer sur un monde fixe, où la
reconstruction coûte une passe de rayons pour rien.
console.log journalise. Print dessine.#
La console, c'est console.log / warn / error, comme partout. Le Print du
BASIC dessine à l'écran et s'appelle à chaque image, ce sont
deux commandes délibérément séparées, et les confondre
est ce qui remplit une console avec une boucle d'image.
Où s'exécute un script#
Dans l'éditeur et dans une compilation web, les scripts tournent dans le navigateur. Dans une compilation native, bureau ou mobile, ils tournent inchangés sur un moteur QuickJS embarqué, la même source, pas un portage.
Les scripts sont conservés dans la base du navigateur pendant l'édition, donc un tampon non enregistré survit à un rechargement ; ils sont enregistrés dans le projet avec le reste.
Limites et pannes courantes#
scene.pick()avec un argument veut des coordonnées canevas. Voir plus haut ; c'est le bug de mauvais repère le plus fréquent.game.spawnfabrique une primitive, pas un personnage. UtilisezspawnFrom.- Un corps est créé pour les entités nées en cours de partie. La physique n'était construite qu'au lancement, si bien que tout ce qui apparaissait après traversait le monde en silence pendant que ses voisins posés à la main se comportaient normalement.
- Les huit étapes sont le plafond du coéquipier IA, pas le vôtre. Une boucle
de script n'a pas cette limite ; un
whileemballé gèle l'image comme partout ailleurs. - Les quatre autres vues de langage sont transpilées depuis celle-ci. Modifier une vue Python ou Rust puis revenir repasse par le JavaScript conservé, voir la page BASIC pour la raison.