JavaScript nell'Engine

L'API di script dell'Engine: il ciclo di vita, le trappole delle coordinate, i tre modi per creare un oggetto, e come spostarne uno senza attraversare i muri.

In breve#

Ogni entità può portare uno script JavaScript. Gira nel browser, nella tua scheda, contro la scena viva, ed è l'API di scripting canonica: il dialetto BASIC, e le viste Python, C# e Rust dell'editor, compilano tutti verso la stessa superficie.

L'elenco esaustivo è il riferimento API: 538 membri in 50 namespace, una pagina ciascuno, con una ricerca. Questa pagina è ciò che devi sapere prima di leggerlo.

Il ciclo di vita#

function start() {}                     // once, when the entity starts
function update(dt) {}                  // every frame, dt in seconds
function fixedUpdate(dt) {}             // fixed timestep, for physics
function onCollisionEnter(other) {}     // first frame of contact
function onCollisionStay(other) {}      // every frame while touching
function onCollisionExit(other) {}      // contact just ended
function onTriggerEnter(other) {}       // entered a trigger volume
function onTriggerExit(other) {}
function onMessage(msg, data, senderId) {}   // entity-to-entity messaging
function onAnimationEvent(name, data) {}
function onDestroy() {}
function main() {}                      // console-game entry, async allowed

deltaTime, time, entityId ed entity sono globali; getComponent(type) legge un componente dall'entità che possiede lo script.

Collisioni e trigger scattano sia in 2D che in 3D, e ogni lato riceve la propria normale di contatto: other.contact.normal punta dall'entità altra verso questa. È ciò che permette a uno script di distinguere "ci sono atterrato sopra" da "mi è atterrato sopra", uno stomp da platform legge other.contact.normal.y > 0.5.

Le due trappole delle coordinate#

Entrambe costano tempo reale di debug, e nessuna delle due fallisce ad alta voce.

Il mouse: pagina contro tela#

input.mousePosition è in coordinate di pagina. scene.pick() vuole coordinate di tela. Passare l'una al posto dell'altra non manca il bersaglio: colpisce un punto spostato della posizione del viewport nella finestra, così un eroe che si muove al clic cammina verso un punto plausibile e sbagliato.

Chiama scene.pick() senza argomenti (punta al cursore), oppure usa input.mouseViewport quando ti servono i numeri.

I controlli virtuali sono in pixel di tela#

input.addVirtualJoystick(id, x, y, size) e input.addVirtualButton(...) posizionano i loro centri nello stesso spazio di mouseViewport. Un joystick restituisce una X/Y normalizzata dopo la sua zona morta radiale; un pulsante espone held, pressed-this-frame e released-this-frame separatamente. Il loro puntatore viene catturato e consumato, così un tocco sul controllo non viene riportato anche come azione grezza sulla tela.

Il joystick predefinito è unificato. input.getJoystickX/Y() sceglie un unico vettore completo tra lo stick a schermo, un controller fisico, oppure la tastiera (WASD/frecce), qualunque sia la sorgente più forte. Un pad connesso ma inattivo non disabilita quindi il movimento da tastiera. Il suo asse Y segue lo spazio schermo 2D: su è -1.

Tre modi per creare un oggetto, e non sono intercambiabili#

Cosa creaVive per
scene.createEntity(def)un'entità autorata: compare nella Scene Graph e nell'Inspectoril progetto, viene salvata
game.spawn(type, pos)una primitiva nuda: nessuna salute, nessuna IA, nessun comportamentola sessione di gioco
game.spawnFrom(template, pos, opts)una copia di un'intera entità: componenti, corpo fisico, tag, layer, scala e lo stack di comportamento compilatola sessione di gioco

game.spawn è giusto per detriti e prototipi, ed è inutile per i nemici: uno spawner alimentato da esso produce palle inerti. Il template che spawnFrom copia è un'entità ordinaria che componi nell'editor e disattivi con l'occhio della gerarchia: nessun formato separato, nessun secondo editor, quindi ciò che vedi è ciò che appare.

// Once, at start: a missing template should say so, not fail every wave silently
if (!game.hasTemplate('Skeleton')) console.warn('no Skeleton template');

const mob = game.spawnFrom('Skeleton', { x, y, z }, { tag: 'Enemy' });

template accetta un id, un nome o un tag. Le copie fatte durante il gioco sono legate alla sessione, quindi stop() le rimuove e la scena di edit non viene mai contaminata.

Disattivare un'entità significa nascosta E silenziosa: nodo disabilitato, script fermato, corpo fisico che non risponde più. Nascondere soltanto lascerebbe un nemico invisibile che continua a colpirti. game.setEnabled(id, on) la pilota da uno script, l'icona a forma d'occhio la pilota dalla gerarchia.

Spostare qualcosa: spingi un corpo, non teletrasportare#

Scrivere transform.position a ogni fotogramma fa attraversare i muri, gli altri mostri e il pavimento. Pilota invece la velocità, ma solo quando c'è qualcosa da pilotare:

if (physics.hasBody) {
  const v = physics.getVelocity();
  physics.setVelocity({ x: vx, y: v.y, z: vz });   // pushes a real body
} else {
  transform.position.x += vx * dt;                 // legitimate for a flyer
}

physics.setVelocity senza un corpo riempie un campo che nessuno integra, e l'entità si ferma di colpo. Ecco perché esiste physics.hasBody, e perché ogni comportamento in movimento del catalogo fa la domanda invece di presumere una risposta. Fermarsi significa annullare la velocità: non fare nulla lascia il corpo scivolare.

Scrivere una posizione ora muove davvero un corpo fisico. Havok pilota la trasformazione, mai il contrario, quindi assegnare transform.position su un corpo dinamico veniva sovrascritto al passo successivo: ogni teletrasporto falliva in silenzio su un'entità fisica. Il proxy di posizione risincronizza ora il corpo, come ha sempre fatto il proxy di rotazione.

Il pathfinding è un'istantanea#

pathfinding.createGrid() lancia raggi su una griglia una sola volta, centrata dove è stata chiamata. Uscire da quella zona restituisce null, e la maggior parte dei chiamanti torna poi a una linea retta, attraverso i muri, senza dirlo.

La griglia si ricentra sul viaggio richiesto per impostazione predefinita. setAutoRecenter(false) esclude questo comportamento su un mondo fisso, dove ricostruire costa un passaggio di raycast per niente.

console.log registra. Print disegna.#

La console è console.log / warn / error, come ovunque altrove. Il Print del BASIC disegna sullo schermo e viene chiamato ogni fotogramma, i due sono comandi deliberatamente separati, e confonderli è ciò che riempie una console con un ciclo di fotogrammi.

Dove gira uno script#

Nell'editor e in una build web, gli script girano nel browser. In una build nativa desktop o mobile, girano invariati su un runtime QuickJS integrato, la stessa sorgente, non un porting.

Gli script sono conservati nel database del browser mentre modifichi, così un buffer non salvato sopravvive a un ricaricamento; vengono salvati nel progetto insieme a tutto il resto.

Limiti e problemi comuni#

  • scene.pick() con un argomento vuole coordinate di tela. Vedi sopra: è il bug del posto sbagliato più comune in assoluto.
  • game.spawn crea una primitiva, non un personaggio. Usa spawnFrom.
  • Un corpo viene creato per le entità nate a metà partita. La fisica veniva costruita solo al Play, quindi qualsiasi cosa generata dopo cadeva attraverso il mondo in silenzio mentre i suoi vicini posizionati a mano si comportavano normalmente.
  • Otto passi è il limite del compagno IA, non il tuo. Un ciclo di script non ha un limite del genere; un while fuori controllo bloccherà il fotogramma come ovunque altro.
  • Le altre quattro viste linguistiche sono transpilate da questa. Modificare una vista Python o Rust e tornare indietro passa attraverso il JavaScript memorizzato, vedi la pagina del BASIC per il perché.