JavaScript w Engine

API skryptowe Engine, cykl życia, pułapki związane ze współrzędnymi, trzy sposoby tworzenia obiektu i jak poruszać nim, nie przechodząc przez ściany.

W jednym zdaniu#

Każda encja może nosić skrypt JavaScript. Działa w przeglądarce, w twojej karcie, na żywej scenie, i jest kanonicznym API skryptowym: dialekt BASIC oraz widoki Python, C# i Rust w edytorze wszystkie kompilują się do tej samej powierzchni.

Wyczerpująca lista to dokumentacja API: 538 elementów w 50 przestrzeniach nazw, po jednej stronie na każdą, z wyszukiwarką. Ta strona to to, co warto wiedzieć zanim się do niej zajrzy.

Cykl życia#

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 to zmienne globalne; getComponent(type) odczytuje komponent z encji, do której należy skrypt.

Kolizje i triggery odpalają się zarówno w 2D, jak i w 3D, a każda strona dostaje własną normalną kontaktu: other.contact.normal wskazuje od tamtej encji ku tej. To właśnie pozwala skryptowi odróżnić "wylądowałem na nim" od "on wylądował na mnie": tupnięcie w platformówce odczytuje other.contact.normal.y > 0.5.

Dwie pułapki związane ze współrzędnymi#

Obie kosztują realny czas debugowania, i żadna nie zawodzi głośno.

Mysz: page kontra canvas#

input.mousePosition jest we współrzędnych page. scene.pick() chce współrzędnych canvas. Podanie jednych zamiast drugich nie chybia całkiem, trafia w punkt przesunięty o pozycję viewportu w oknie, więc bohater poruszany kliknięciem idzie w miejsce, które wygląda wiarygodnie, ale jest złe.

Wywołaj scene.pick() bez argumentu (celuje wtedy w kursor), albo użyj input.mouseViewport, gdy potrzebujesz samych liczb.

Wirtualne sterowanie jest w pikselach canvas#

input.addVirtualJoystick(id, x, y, size) i input.addVirtualButton(...) umieszczają swoje środki w tej samej przestrzeni co mouseViewport. Joystick zwraca znormalizowane X/Y po uwzględnieniu swojej promienistej martwej strefy; przycisk osobno udostępnia held, pressed-this-frame i released-this-frame. Ich wskaźnik jest przechwytywany i zużywany, więc dotknięcie kontrolki nie jest jednocześnie zgłaszane jako surowa akcja canvas.

Domyślny joystick jest ujednolicony. input.getJoystickX/Y() wybiera jeden kompletny wektor z gałki na ekranie, fizycznego gamepada albo klawiatury (WASD/strzałki), zależnie od tego, które źródło jest najsilniejsze. Bezczynny, podłączony pad nie wyłącza więc ruchu z klawiatury. Jego oś Y podąża za przestrzenią ekranu 2D: góra to -1.

Trzy sposoby na stworzenie obiektu, i nie są wymienne#

Co tworzyJak długo żyje
scene.createEntity(def)encję autorską: pojawia się w Scene Graph i w Inspectorprojekt, jest zapisywana
game.spawn(type, pos)goły prymityw: bez zdrowia, bez AI, bez zachowaniasesję gry
game.spawnFrom(template, pos, opts)kopię całej encji: komponenty, ciało fizyczne, tagi, warstwę, skalę i skompilowany stos zachowańsesję gry

game.spawn sprawdza się do gruzu i prototypów, i jest bezużyteczny dla przeciwników: spawner nim zasilany produkuje bezwładne kulki. Szablon, który kopiuje spawnFrom, to zwykła encja, którą komponujesz w edytorze i wyłączasz okiem w hierarchii: bez osobnego formatu, bez drugiego edytora, więc to, co widzisz, jest tym, co się pojawia.

// 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 przyjmuje id, nazwę albo tag. Kopie zrobione podczas gry są ograniczone do sesji gry, więc stop() je usuwa, a scena edycji nigdy nie zostaje zaśmiecona.

Wyłączenie encji oznacza ukrytą I wyciszoną: węzeł wyłączony, skrypt zatrzymany, ciało fizyczne przestaje reagować. Samo ukrycie zostawiłoby niewidzialnego przeciwnika, który wciąż by cię trafiał. game.setEnabled(id, on) steruje tym ze skryptu, ikona oka steruje tym z hierarchii.

Poruszanie czymś: pchnij ciało, nie teleportuj#

Zapisywanie transform.position co klatkę przechodzi przez ściany, przez inne potwory i przez podłogę. Zamiast tego steruj prędkością, ale tylko wtedy, gdy jest czym sterować:

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 bez ciała wypełnia pole, którego nikt nie całkuje, i encja zatrzymuje się na amen. Dlatego istnieje physics.hasBody, i dlatego każde zachowanie ruchu w katalogu zadaje to pytanie, zamiast zakładać odpowiedź. Zatrzymanie oznacza wyzerowanie prędkości: nicnierobienie zostawia ciało w ruchu bezwładnym.

Zapisanie pozycji teraz naprawdę porusza ciałem fizycznym. Havok steruje transformacją, nigdy odwrotnie, więc przypisanie transform.position na dynamicznym ciele było kiedyś nadpisywane w kolejnym kroku: każdy teleport na fizycznej encji zawodził po cichu. Proxy pozycji resynchronizuje teraz ciało, tak jak zawsze robiło to proxy rotacji.

Pathfinding to migawka#

pathfinding.createGrid() wykonuje raycast siatki raz, wyśrodkowanej tam, gdzie została wywołana. Opuszczenie tego pudełka zwraca null, a większość wywołujących wtedy po cichu przechodzi na linię prostą, przez ściany, bez słowa.

Siatka domyślnie centruje się ponownie przy każdej żądanej podróży. setAutoRecenter(false) pozwala z tego zrezygnować w świecie o stałym układzie, gdzie przebudowa kosztuje przebieg raycastów na darmo.

console.log loguje. Print rysuje.#

Konsola to console.log / warn / error, tak jak wszędzie indziej. BASIC-owy Print rysuje na ekranie i jest wywoływany co klatkę; te dwa są celowo osobnymi poleceniami, a pomylenie ich to sposób na zapchanie konsoli pętlą klatek.

Gdzie działa skrypt#

W edytorze i w buildzie webowym skrypty działają w przeglądarce. W buildzie natywnym, desktopowym lub mobilnym, działają bez zmian na wbudowanym runtime QuickJS, to to samo źródło, nie port.

Skrypty są przechowywane we własnej bazie danych przeglądarki w trakcie edycji, więc niezapisany bufor przetrwa przeładowanie; zapisują się w projekcie razem z resztą.

Ograniczenia i częste problemy#

  • scene.pick() z argumentem chce współrzędnych canvas. Patrz wyżej; to najczęstszy błąd złego miejsca.
  • game.spawn tworzy prymityw, nie postać. Użyj spawnFrom.
  • Ciało jest teraz tworzone dla encji narodzonych w trakcie gry. Fizyka kiedyś była budowana tylko przy Play, więc wszystko, co pojawiło się później, po cichu przepadało przez świat, podczas gdy jego ręcznie rozmieszczeni sąsiedzi zachowywali się normalnie.
  • Osiem kroków to sufit współpracownika AI, nie twój. Pętla skryptu nie ma takiego limitu; rozpędzone while zawiesi klatkę tak samo jak wszędzie indziej.
  • Pozostałe cztery widoki językowe są transpilowane z tego jednego. Edycja widoku Python albo Rust i powrót przechodzi przez zapisany JavaScript, zobacz stronę o BASIC, dlaczego.