JavaScript no Engine

A API de script do Engine: o ciclo de vida, as armadilhas de coordenadas, as três formas de criar um objeto, e como mover um sem atravessar paredes.

Em uma frase#

Toda entidade pode carregar um script em JavaScript. Ele roda no navegador, na sua aba, contra a cena viva, e é a API de scripting canônica: o dialeto BASIC, e as visões Python, C# e Rust no editor, todos compilam para a mesma superfície.

A lista exaustiva é a referência de API: 538 membros em 50 namespaces, uma página cada, com busca. Esta página é o que você precisa saber antes de ler aquela.

O ciclo de vida#

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 e entity são globais; getComponent(type) lê um componente da entidade dona do script.

Colisões e triggers disparam em 2D e em 3D, e cada lado recebe sua própria normal de contato: other.contact.normal aponta da outra entidade em direção a esta. É isso que permite a um script distinguir "eu pousei nele" de "ele pousou em mim": um pisão de plataforma lê other.contact.normal.y > 0.5.

As duas armadilhas de coordenadas#

As duas custam tempo de debug de verdade, e nenhuma falha em voz alta.

Mouse: página versus canvas#

input.mousePosition está em coordenadas de página. scene.pick() quer coordenadas de canvas. Passar uma no lugar da outra não erra por pouco: ela acerta um ponto deslocado pela posição do viewport na janela, então um herói que se move ao clique caminha para algum lugar plausível e errado.

Chame scene.pick() sem argumento (ele mira no cursor), ou use input.mouseViewport quando você precisa dos números.

Controles virtuais estão em pixels de canvas#

input.addVirtualJoystick(id, x, y, size) e input.addVirtualButton(...) posicionam seus centros no mesmo espaço de mouseViewport. Um joystick retorna um X/Y normalizado depois de sua zona morta radial; um botão expõe held, pressed-this-frame e released-this-frame separadamente. O ponteiro deles é capturado e consumido, então um toque no controle não é também relatado como uma ação crua de canvas.

O joystick padrão é unificado. input.getJoystickX/Y() escolhe um vetor completo a partir do stick na tela, de um gamepad físico, ou do teclado (WASD/setas), o que for mais forte. Um gamepad conectado e parado não desativa, portanto, o movimento por teclado. Seu eixo Y segue o espaço de tela 2D: para cima é -1.

Três formas de criar um objeto, e elas não são intercambiáveis#

O que criaVive por
scene.createEntity(def)uma entidade autorada: aparece no Scene Graph e no Inspectoro projeto, é salva
game.spawn(type, pos)uma primitiva nua: sem vida, sem IA, sem comportamentoa sessão de jogo
game.spawnFrom(template, pos, opts)uma cópia de uma entidade inteira: componentes, corpo físico, tags, camada, escala e a pilha de comportamento compiladaa sessão de jogo

game.spawn é certo para destroços e protótipos e inútil para inimigos: um spawner alimentado por ele produz bolas inertes. O template que spawnFrom copia é uma entidade comum que você compõe no editor e desliga com o olho da hierarquia: sem formato separado, sem segundo editor, então o que você vê é o que aparece.

// 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 aceita um id, um nome ou uma tag. Cópias feitas durante o jogo têm escopo de sessão, então stop() as remove e a cena de edição nunca é poluída.

Desligar uma entidade significa oculta E silenciosa: nó desativado, script parado, corpo físico sem mais responder. Só esconder deixaria um inimigo invisível ainda te acertando. game.setEnabled(id, on) faz isso a partir de um script, o ícone de olho faz isso a partir da hierarquia.

Movendo algo: empurre um corpo, não teletransporte#

Escrever transform.position todo quadro atravessa paredes, outros monstros e o chão. Em vez disso, conduza a velocidade, mas só quando houver algo para conduzir:

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 sem um corpo preenche um campo que ninguém integra, e a entidade para no lugar. É por isso que physics.hasBody existe, e por que todo comportamento de movimento no catálogo faz a pergunta em vez de presumir a resposta. Parar significa cancelar a velocidade: não fazer nada deixa o corpo deslizando.

Escrever uma posição agora move um corpo físico. O Havok conduz o transform, nunca o contrário, então atribuir transform.position num corpo dinâmico costumava ser sobrescrito no próximo passo: todo teletransporte falhava em silêncio numa entidade física. O proxy de posição ressincroniza o corpo, do jeito que o proxy de rotação sempre fez.

Pathfinding é um instantâneo#

pathfinding.createGrid() faz raycast numa grade uma vez, centrada onde foi chamada. Sair dessa caixa retorna null, e a maioria dos chamadores então recorre a uma linha reta, através de paredes, sem avisar.

A grade se recentraliza na viagem solicitada por padrão. setAutoRecenter(false) desativa isso num mundo fixo, onde reconstruir custa uma passada de raycast à toa.

console.log registra. Print desenha.#

O console é console.log / warn / error, como em qualquer outro lugar. O Print do BASIC desenha na tela e é chamado todo quadro, os dois são comandos deliberadamente separados, e confundi-los é o que enche um console com um loop de quadro.

Onde um script roda#

No editor e num build web, os scripts rodam no navegador. Num build nativo de desktop ou mobile, eles rodam sem mudanças num runtime QuickJS embutido, o mesmo código-fonte, não uma portabilidade.

Os scripts ficam guardados no próprio banco de dados do navegador enquanto você edita, então um buffer não salvo sobrevive a um recarregamento; eles são salvos no projeto junto com tudo o mais.

Limites e problemas comuns#

  • scene.pick() com um argumento quer coordenadas de canvas. Veja acima; é o bug de lugar errado mais comum de todos.
  • game.spawn faz uma primitiva, não um personagem. Use spawnFrom.
  • Um corpo é criado para entidades nascidas no meio do jogo. A física costumava ser construída só no Play, então qualquer coisa criada depois caía pelo mundo em silêncio, enquanto suas vizinhas posicionadas à mão se comportavam normalmente.
  • Oito passos é o teto do teammate de IA, não o seu. Um loop de script não tem esse limite; um while descontrolado vai travar o quadro como em qualquer outro lugar.
  • As outras quatro visões de linguagem são transpiladas a partir desta. Editar uma visão em Python ou Rust e voltar passa pelo JavaScript armazenado, veja a página do BASIC para entender por quê.