Saltar al contenido principal

La API de ctx

ctx es tu asa sobre la escena en marcha: se pasa a init y todo lo de abajo cuelga de él.

Es deliberadamente pequeño. Si buscas algo y no lo encuentras aquí, hay buenas probabilidades de que la respuesta sea un paso integrado y no una API.

Dónde estás

ctx.entityel objeto al que está adjunto este script
ctx.scenela escena a la que pertenece
ctx.spaceel mundo

Ciclo de vida y eventos

ctx.tick((dt, t) => {});          // en cada fotograma; dt y t en segundos
ctx.effect(() => {}); // se reejecuta cuando cambia lo que lee; puede devolver una limpieza
ctx.on(trigger, (payload) => {}); // suscribirse a un disparador
ctx.emit(trigger, payload); // lanzar uno desde este objeto

Los disparadores usan los mismos nombres que los eventos y los patches. Los que traen información útil:

DisparadorRecibes
on-keydown · on-keyup{ code, ctrl, shift, alt, meta }
on-state-active · on-state-inactive{ stateId }
on-collide{ other } — contra qué chocaste
on-divkit-action{ id, … } — qué botón
on-game-control{ state } — quieto, moviéndose, corriendo o saltando
on-drag · on-pinch · on-rotate{ dx, dy } · { scale } · { angle }
on-vps-localizeddónde resultó estar el visitante

on-launch llega en el momento en que se crea tu instancia, así que no puedes perdértelo por arrancar tarde.

Encontrar objetos

ctx.get(id);
ctx.findByName('Door');
ctx.find(LightComponent); // el primer objeto con estos componentes
ctx.query(LightComponent, TagsComponent); // todos ellos
ctx.all();

Crear y eliminar

ctx.create({ name: 'Bullet', parent, components: [] });
ctx.spawn(props.bulletModel, { parent });
ctx.destroy(entity);

spawn es el cómodo: dale un recurso y monta los componentes adecuados. Un modelo se convierte en un objeto de modelo, una imagen en un plano texturizado, un sonido en una fuente de audio.

Ejecutar comportamiento integrado

ctx.step('play_animation', { presetId }, { targets: [enemy] });

ctx.startTransition({ durationMs: 400, easing: 'ease-out' }, () => {
// los cambios hechos aquí se interpolan en vez de saltar
});
Este es el atajo que casi todo el mundo se pierde

ctx.step ejecuta cualquiera de los pasos integrados, los mismos que usan tus eventos. Animación, cambio de state, transiciones de escena y transiciones están a una llamada, así que casi nunca necesitas reimplementarlos.

ctx.openScene(sceneOrId);
await ctx.openSpace(spaceRefOrId);
ctx.scenes();

Entrada

Dos capas, para dos trabajos distintos.

Las acciones con nombre leen las asignaciones de teclas del proyecto, así que a un visitante que se remapee las teclas se le respeta:

ctx.input.pressed('jump');
ctx.input.justPressed('fire');
ctx.input.axis('moveX');

Las teclas en crudo leen el teclado directamente. Una pulsación dura exactamente un fotograma, así que léelas dentro de ctx.tick:

ctx.keyboard.down('KeyW');
ctx.keyboard.press('Space');
ctx.keyboard.press('ArrowLeft', { every: 200 }); // repetición automática, en ms
ctx.keyboard.axis('KeyA', 'KeyD'); // -1, 0 o 1
Las dos tratan los modificadores de forma distinta, a propósito

Una asignación con nombre ignora los modificadores que no menciona: esprintar con Shift pulsado no debe cancelar «adelante». Un disparador de tecla es lo contrario: un modificador que no marcaste significa «no debe estar pulsado».

Perder el foco de la ventana suelta las teclas pulsadas, así que nada se queda atascado.

Cámara

ctx.camera.entity();           // el objeto de la cámara activa
ctx.camera.setActive(target); // cambiar de cámara; null restaura la de por defecto
ctx.camera.pose(); // { position, rotation, forward } en coordenadas de mundo

El transform de la cámara es la fuente de verdad en todos los modos de control: escribe en él para mover la cámara, léelo para ver dónde la han puesto los controles. En los modos orbit y primera persona los controles son dueños de la orientación, así que una rotación que escribas será sobrescrita; la posición sí se respeta.

Raycasting

await ctx.raycast();                                       // desde el centro de la cámara
await ctx.raycast({ screen: { x: 0.5, y: 0 }, all: true }); // un punto de pantalla, todos los impactos
await ctx.raycast({ origin, direction }); // el rayo que quieras
await ctx.raycast({ from: entity }); // desde un objeto, a lo largo de su frente

Cada impacto te dice el objeto, la distancia, y el punto y la normal de la superficie en coordenadas de mundo, ordenados de más cercano a más lejano, un impacto por objeto.

Devuelve una promesa, y no es un error

El rayo se lanza contra geometría real en el lado del renderizado, así que la respuesta llega al siguiente fotograma. Los objetos invisibles y los marcados para ignorarse (retículas, gizmos) se saltan, de modo que responde lo que hay detrás en vez de que el rayo informe de un fallo.

Física

ctx.physics.applyImpulse(target, { x: 0, y: 5, z: 0 });
ctx.physics.applyForce(target, vec, point);
ctx.physics.setVelocity(target, vec);
ctx.physics.teleport(target, position, { rotation, keepVelocity });
ctx.physics.setGravity(vec);

ctx.physics.getSpeed(target);
ctx.physics.isSleeping(target);

await ctx.physics.raycast(from, to, { skip: [ctx.entity] });
Fijar el transform de un cuerpo dinámico no hace nada

La física es dueña de su posición. Usa teleport para colocarlo e impulsos o fuerzas para moverlo.

Y pasa skip cuando lances un rayo desde dentro de tu propio cuerpo, o te darás a ti mismo todas y cada una de las veces.

Las lecturas vienen del último estado sincronizado y van con un fotograma de retraso: bien para «¿me estoy moviendo?», mal para matemáticas instantáneas exactas.

Audio

ctx.audio.play(props.hitSound, { at: enemy, volume: 0.6, positional: true });

Cada llamada arranca un sonido independiente, que es exactamente lo que necesitan los pasos, los impactos y los disparos. El componente de audio es una sola voz y se corta a sí mismo: no lo uses para efectos.

Guardar cosas

Tres almacenes, que se diferencian por su alcance:

// 1. los valores propios de este script
const store = ctx.store('game', { score: { type: 'number', default: 0 } });
store.set('score', (v) => v + 1);
store.subscribe('score', (v) => {});

// 2. globals: compartidos con todos los scripts, patches y eventos
ctx.setGlobal('level', 3);
ctx.getGlobal('level');
ctx.subscribeGlobal('level', (v) => {});

// 3. mensajes entre scripts
ctx.postMessage('enemy-died', { id });
ctx.handleMessage('enemy-died', ({ id }) => {});
Sobrevive a un cambio de escenaSobrevive al paso entre spacesSobrevive a una recarga
storenono
globalsno
globals de spaceno — aislados a propósitono
Nada de esto sobrevive a cerrar la pestaña

Ninguno de los tres se guarda entre visitas. Si algo debe persistir, mándalo tú a algún sitio mientras todavía tengas conexión.

Interfaz

const ui = ctx.getDivKit(entity);

ui.get('score');
ui.set('score', (v) => v + 1);
ui.subscribe('lives', (v) => {});
ui.onAction('restart', () => {});

Consulta Tarjetas de interfaz.


Siguiente: Lo que vas a construir de verdad — scripts completos para copiar.