JavaScript en el Engine

La API de scripting del Engine: el ciclo de vida, las trampas de coordenadas, las tres formas de crear un objeto, y cómo mover uno sin atravesar paredes.

En una frase#

Cada entidad puede llevar un script en JavaScript. Se ejecuta en el navegador, en tu propia pestaña, contra la escena en vivo, y es la API de scripting canónica: el dialecto BASIC, y las vistas Python, C# y Rust del editor, todas compilan hacia la misma superficie.

La lista exhaustiva es la referencia de la API: 538 miembros en 50 espacios de nombres, una página cada uno, con buscador. Esta página es lo que necesitas saber antes de leer aquella.

El ciclo de vida#

function start() {}                     // una vez, cuando arranca la entidad
function update(dt) {}                  // cada fotograma, dt en segundos
function fixedUpdate(dt) {}             // paso fijo, para física
function onCollisionEnter(other) {}     // primer fotograma de contacto
function onCollisionStay(other) {}      // cada fotograma mientras se tocan
function onCollisionExit(other) {}      // el contacto acaba de terminar
function onTriggerEnter(other) {}       // entró en un volumen trigger
function onTriggerExit(other) {}
function onMessage(msg, data, senderId) {}   // mensajería entidad a entidad
function onAnimationEvent(name, data) {}
function onDestroy() {}
function main() {}                      // entrada para juego de consola, async permitido

deltaTime, time, entityId y entity son globales; getComponent(type) lee un componente de la entidad que posee el script.

Las colisiones y los triggers se disparan tanto en 2D como en 3D, y cada lado recibe su propia normal de contacto: other.contact.normal apunta desde la otra entidad hacia esta. Eso es lo que le permite a un script distinguir "aterricé sobre él" de "él aterrizó sobre mí", un stomp de plataformas lee other.contact.normal.y > 0.5.

Las dos trampas de coordenadas#

Ambas cuestan tiempo real de depuración, y ninguna falla de forma evidente.

El ratón: página frente a lienzo#

input.mousePosition está en coordenadas de página. scene.pick() quiere coordenadas de canvas. Pasar una en lugar de la otra no falla del todo, golpea un punto desplazado por la posición del viewport en la ventana, así que un click-to-move del héroe camina hacia algún sitio verosímil y equivocado.

Llama a scene.pick() sin argumento (apunta al cursor), o usa input.mouseViewport cuando necesites los números.

Los controles virtuales están en píxeles de canvas#

input.addVirtualJoystick(id, x, y, size) y input.addVirtualButton(...) colocan sus centros en el mismo espacio que mouseViewport. Un joystick devuelve un vector X/Y normalizado tras su zona muerta radial; un botón expone held, pressed-this-frame y released-this-frame por separado. Su puntero queda capturado y consumido, así que un toque sobre el control no se reporta también como una acción de canvas en bruto.

El joystick por defecto está unificado. input.getJoystickX/Y() elige un vector completo de la fuente más fuerte entre el stick en pantalla, un mando físico, o el teclado (WASD/flechas). Un mando conectado pero inactivo no desactiva por tanto el movimiento por teclado. Su eje Y sigue el espacio de pantalla 2D: arriba es -1.

Tres formas de crear un objeto, y no son intercambiables#

Qué creaVive mientras
scene.createEntity(def)una entidad autorada: aparece en el Scene Graph y en el Inspectordure el proyecto, se guarda
game.spawn(type, pos)una primitiva desnuda: sin salud, sin IA, sin comportamientodure la sesión de juego
game.spawnFrom(template, pos, opts)una copia de una entidad completa: componentes, cuerpo físico, tags, layer, escala y la pila de comportamiento compiladadure la sesión de juego

game.spawn es correcto para escombros y prototipos y inútil para enemigos: un spawner alimentado con él produce bolas inertes. El template que copia spawnFrom es una entidad ordinaria que compones en el editor y apagas con el ojo de la jerarquía: sin formato aparte, sin segundo editor, así que lo que ves es lo que aparece.

// Una vez, al inicio: a un template que falta hay que avisar, no fallar cada oleada en silencio
if (!game.hasTemplate('Skeleton')) console.warn('no Skeleton template');

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

template acepta un id, un nombre o un tag. Las copias hechas durante el juego son de alcance de sesión, así que stop() las elimina y la escena de edición nunca se contamina.

Apagar una entidad significa oculta Y silenciosa: nodo desactivado, script detenido, cuerpo físico que ya no responde. Solo ocultarla dejaría un enemigo invisible que sigue golpeándote. game.setEnabled(id, on) lo maneja desde un script, el icono del ojo lo maneja desde la jerarquía.

Mover algo: empuja un cuerpo, no lo teletransportes#

Escribir transform.position cada fotograma atraviesa paredes, otros monstruos y el suelo. Maneja la velocity en su lugar, pero solo cuando hay algo que manejar:

if (physics.hasBody) {
  const v = physics.getVelocity();
  physics.setVelocity({ x: vx, y: v.y, z: vz });   // empuja un cuerpo real
} else {
  transform.position.x += vx * dt;                 // legítimo para algo que vuela
}

physics.setVelocity sin un cuerpo rellena un campo que nadie integra, y la entidad se detiene en seco. Por eso existe physics.hasBody, y por eso todo comportamiento de movimiento del catálogo hace la pregunta en lugar de asumir una respuesta. Detenerse significa cancelar la velocity: no hacer nada deja al cuerpo deslizándose.

Escribir una posición mueve un cuerpo físico, ahora. Havok maneja el transform, nunca al revés, así que asignar transform.position sobre un cuerpo dinámico se sobrescribía en el siguiente paso: cada teletransporte fallaba en silencio sobre una entidad física. El proxy de posición resincroniza el cuerpo, como siempre lo hizo el proxy de rotación.

El pathfinding es una instantánea#

pathfinding.createGrid() lanza rayos sobre una cuadrícula una vez, centrada donde se llamó. Salir de esa caja devuelve null, y la mayoría de quienes la llaman entonces recurren a una línea recta, atravesando paredes, sin avisar.

La cuadrícula se recentra en el siguiente viaje solicitado por defecto. setAutoRecenter(false) permite salir de eso en un mundo fijo, donde reconstruirla cuesta una pasada de rayos para nada.

console.log registra. Print dibuja.#

La consola es console.log / warn / error, como en cualquier otro sitio. El Print de BASIC dibuja en la pantalla y se llama cada fotograma, los dos son comandos deliberadamente separados, y confundirlos es lo que llena una consola con un bucle de fotogramas.

Dónde se ejecuta un script#

En el editor y en una build web, los scripts se ejecutan en el navegador. En una build de escritorio o móvil nativa, se ejecutan sin cambios sobre un runtime QuickJS embebido, la misma fuente, no un port.

Los scripts se guardan en la propia base de datos del navegador mientras editas, así que un buffer sin guardar sobrevive a una recarga; se guardan en el proyecto junto con todo lo demás.

Límites y problemas frecuentes#

  • scene.pick() con un argumento quiere coordenadas de canvas. Ver más arriba; es el bug de lugar equivocado más común de todos.
  • game.spawn crea una primitiva, no un personaje. Usa spawnFrom.
  • Se crea un cuerpo para las entidades nacidas a mitad de partida. La física solía construirse solo al pulsar Play, así que cualquier cosa generada después caía en silencio a través del mundo mientras sus vecinos colocados a mano se comportaban con normalidad.
  • Ocho pasos es el techo del compañero de IA, no el tuyo. Un bucle de script no tiene ese límite; un while descontrolado va a colgar el fotograma como en cualquier otro sitio.
  • Las otras cuatro vistas de lenguaje se transpilan desde esta. Editar una vista Python o Rust y volver atrás pasa por el JavaScript guardado, ver la página de BASIC para saber por qué.