JavaScript în Engine

API-ul de script al Engine-ului: ciclul de viață, capcanele de coordonate, cele trei moduri de a crea un obiect, și cum să muți unul fără să treci prin pereți.

Într-o propoziție#

Fiecare entitate poate purta un script JavaScript. Rulează în browser, în fila ta, împotriva scenei vii, și este API-ul canonic de scriptare: dialectul BASIC, și vederile Python, C# și Rust din editor, toate se compilează spre aceeași suprafață.

Lista exhaustivă este referința de API: 538 de membri în 50 de spații de nume, câte o pagină pentru fiecare, cu o căutare. Această pagină este ce trebuie să știi înainte de a o citi.

Ciclul de viață#

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 și entity sunt globale; getComponent(type) citește o componentă de pe entitatea care deține scriptul.

Coliziunile și trigger-ele se declanșează atât în 2D cât și în 3D, iar fiecare parte primește propria normală de contact: other.contact.normal indică de la entitatea cealaltă către aceasta. Asta îi permite unui script să deosebească „am aterizat pe el" de „a aterizat el pe mine", o strivire de platformer citește other.contact.normal.y > 0.5.

Cele două capcane de coordonate#

Amândouă costă timp real de depanare, și niciuna nu eșuează zgomotos.

Mouse: pagină versus canvas#

input.mousePosition este în coordonate de pagină. scene.pick() vrea coordonate de canvas. Trecerea uneia în locul celeilalte nu ratează, ci lovește un punct decalat cu poziția viewport-ului în fereastră, așa că un click-to-move al eroului merge undeva plauzibil și greșit.

Apelează scene.pick() fără niciun argument (țintește cursorul), sau folosește input.mouseViewport când ai nevoie de numere.

Controalele virtuale sunt în pixeli de canvas#

input.addVirtualJoystick(id, x, y, size) și input.addVirtualButton(...) își plasează centrele în același spațiu ca mouseViewport. Un joystick întoarce un vector X/Y normalizat după zona lui moartă radială; un buton expune held, pressed-this-frame și released-this-frame separat. Pointer-ul lor este capturat și consumat, așa că o atingere pe control nu este raportată și ca o acțiune brută de canvas.

Joystick-ul implicit este unificat. input.getJoystickX/Y() alege un singur vector complet din stick-ul de pe ecran, un gamepad fizic, sau tastatura (WASD/săgeți), oricare dintre surse este mai puternică. Un gamepad conectat, dar inactiv, nu dezactivează deci mișcarea de la tastatură. Axa lui Y urmează spațiul de ecran 2D: sus este -1.

Trei moduri de a crea un obiect, și nu sunt interschimbabile#

Ce creeazăTrăiește pentru
scene.createEntity(def)o entitate autorizată: apare în Scene Graph și în Inspectorproiect, este salvată
game.spawn(type, pos)o primitivă goală: fără viață, fără AI, fără comportamentsesiunea de joc
game.spawnFrom(template, pos, opts)o copie a unei entități întregi: componente, corp fizic, tag-uri, layer, scală și stiva de comportament compilatăsesiunea de joc

game.spawn este potrivit pentru moloz și prototipuri și inutil pentru inamici: un spawner alimentat cu el produce bile inerte. Șablonul pe care spawnFrom îl copiază este o entitate obișnuită pe care o compui în editor și o oprești cu ochiul din ierarhie: fără un format separat, fără un al doilea editor, așa că ce vezi este ce apare.

// 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 acceptă un id, un nume sau un tag. Copiile făcute în timpul jocului sunt limitate la sesiunea de joc, așa că stop() le elimină, iar scena de editare nu este niciodată poluată.

Dezactivarea unei entități înseamnă ascunsă ȘI tăcută: nod dezactivat, script oprit, corp fizic care nu mai răspunde. Doar ascunderea ar lăsa un inamic invizibil care continuă să te lovească. game.setEnabled(id, on) o pilotează dintr-un script, ochiul din ierarhie o pilotează de acolo.

Mutarea a ceva: împinge un corp, nu teleporta#

Scrierea lui transform.position în fiecare cadru trece prin pereți, prin alți monștri și prin podea. Pilotează viteza în schimb, dar numai când există ceva de pilotat:

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 fără un corp umple un câmp pe care nimeni nu îl integrează, iar entitatea se oprește pe loc. De aceea există physics.hasBody, și de aceea fiecare comportament de mișcare din catalog pune întrebarea, în loc să presupună un răspuns. Oprirea înseamnă anularea vitezei: a nu face nimic lasă corpul să alunece mai departe.

Scrierea unei poziții chiar mișcă acum un corp fizic. Havok pilotează transformarea, niciodată invers, așa că atribuirea lui transform.position pe un corp dinamic era suprascrisă la pasul următor: fiecare teleportare eșua tăcut pe o entitate fizică. Proxy-ul de poziție resincronizează acum corpul, așa cum a făcut-o mereu proxy-ul de rotație.

Pathfinding-ul este un instantaneu#

pathfinding.createGrid() face raycast pe o grilă o singură dată, centrată acolo unde a fost apelată. Ieșirea din acea cutie întoarce null, iar majoritatea apelanților revin apoi la o linie dreaptă, prin pereți, fără niciun cuvânt.

Grila se recentrează la fiecare traseu cerut, implicit. setAutoRecenter(false) renunță la asta pe o lume fixă, unde reconstrucția costă o trecere de raycast degeaba.

console.log scrie în jurnal. Print desenează.#

Consola este console.log / warn / error, ca oriunde altundeva. Print din BASIC desenează pe ecran și este apelat în fiecare cadru, cele două sunt comenzi separate în mod deliberat, iar confundarea lor este ceea ce umple o consolă cu o buclă de cadru.

Unde rulează un script#

În editor și într-un build web, scripturile rulează în browser. Într-un build nativ, desktop sau mobil, rulează neschimbate pe un runtime QuickJS integrat, aceeași sursă, nu un port.

Scripturile sunt stocate în propria bază de date a browserului cât timp editezi, așa că un buffer nesalvat supraviețuiește unei reîncărcări; sunt salvate în proiect odată cu tot restul.

Limite și capcane frecvente#

  • scene.pick() cu un argument vrea coordonate de canvas. Vezi mai sus; este cel mai frecvent bug de loc greșit.
  • game.spawn face o primitivă, nu un personaj. Folosește spawnFrom.
  • Un corp este creat pentru entitățile născute în timpul jocului. Fizica se construia înainte doar la Play, așa că orice apărut ulterior cădea tăcut prin lume, în timp ce vecinii lui plasați manual se comportau normal.
  • Opt pași este plafonul coechipierului AI, nu al tău. O buclă de script nu are o astfel de limită; un while scăpat de sub control va bloca cadrul ca oriunde altundeva.
  • Celelalte patru vederi de limbaj sunt transpilate din aceasta. Editarea unei vederi Python sau Rust și revenirea trece prin JavaScript-ul stocat, vezi pagina BASIC pentru motiv.