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 cria | Vive por | |
|---|---|---|
scene.createEntity(def) | uma entidade autorada: aparece no Scene Graph e no Inspector | o projeto, é salva |
game.spawn(type, pos) | uma primitiva nua: sem vida, sem IA, sem comportamento | a 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 compilada | a 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.positionnum 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.spawnfaz uma primitiva, não um personagem. UsespawnFrom.- 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
whiledescontrolado 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ê.