En NacatamalOn una escena es una función. Esta página cuenta qué hace el motor con ella: cuándo la ejecuta, qué pasa en cada fotograma, qué significa pausarla, cómo se cambia de una a otra y qué se limpia al irse. Cada paso está leído del código del motor, en el orden en que ocurre.
Los estados de una escena
- Registrada. Cada clave del objeto que le das al juego es el nombre de una escena. Registrada quiere decir que existe y se puede arrancar por su nombre, nada más.
- Naciendo. El motor ejecuta el cuerpo de la función una vez. Ahí se declara todo lo que hay en la escena.
- En marcha. Sus
useUpdatecorren en cada fotograma, reacciona a los clics y se dibuja. - En pausa. Se sigue dibujando, pero sus
useUpdateno corren y no reacciona a los clics. Es lo que necesita un menú de pausa encima de un nivel congelado. - Retenida. Solo le pasa a la escena que llega con una transición: ya ha nacido y está cargando lo que pidió, pero no se ve, no se actualiza y no recibe clics hasta el intercambio.
- Parada. Sale de la lista de escenas en marcha, deja de dibujarse y corren sus limpiezas,
entre ellas su
useSceneUnmount(más abajo, en «Irse»). Si se vuelve a arrancar, empieza de cero: el cuerpo corre otra vez.
Nacer: el cuerpo corre una vez
const Level = () => {
// Todo esto corre UNA vez, cuando la escena arranca.
const hero = useLoadTexture({ src: '/hero.png' });
const player = createSprite({ texture: hero, transform: { x: 160, y: 120 } });
const keys = useKeyboard();
// Esto no se ejecuta ahora: se apunta, y el motor lo llama en cada fotograma.
useUpdate((dt) => {
if (keys.isDown('ArrowRight')) player.transform.x += 120 * dt;
});
return createScene();
};Tres cosas de este momento importan:
- Es síncrono. El cuerpo no puede ser
asyncni esperar a nada. Una escena que devuelve una promesa se rechaza con un error que lo dice. - Los hooks se llaman aquí y solo aquí.
useUpdate,useKeyboardouseLoadTexturese apuntan en la escena que está naciendo. Fuera del cuerpo (en un clic, en un temporizador) no hay escena naciendo. Para crear objetos más tarde, el cuerpo prepara una fábrica conuseSpawn, y esa fábrica sí se puede llamar desde unuseUpdateo desde un clic. - Cargar no bloquea.
useLoadTextureempieza a cargar y devuelve la textura al momento, aún vacía. La escena arranca igual, y el sprite aparece en cuanto llega la imagen. Para una barra de progreso estáuseLoader.
Al terminar, createScene() cierra la escena con todo lo declarado y el motor la añade a la
lista de escenas en marcha.
La primera escena del juego es la primera clave del objeto, o la que le digas:
const game = createGame('#app', { width: 320, height: 240 });
game({ Menu, Level }); // arranca Menu
game({ Menu, Level }, 'Level'); // arranca LevelUn fotograma, en orden
Sesenta veces por segundo, en este orden:
- 1Entradatambién en pausa
Los clics y toques que han llegado desde el fotograma anterior se reparten a sus oyentes, y se lee el mando. Ocurre aquí, en un momento que elige el juego, y no cuando el navegador lo dispara.
- 2Transicióntambién en pausa
Si hay un cambio de escena con transición en marcha, avanza. No se congela nunca: la pantalla siempre acaba descubriéndose.
- 3Actualizaciones
Cada escena en marcha, en el orden en que se lanzaron, ejecuta sus useUpdate con dt. Las que están en pausa o retenidas se saltan.
- 4Cierre del tecladotambién en pausa
justPressed deja de ser cierto. Una pulsación cuenta durante un solo fotograma, y en ese fotograma cuenta para todos los que pregunten.
- 5Borradostambién en pausa
Lo que se destruyó con destroy sale ahora, cuando nadie está recorriendo las escenas. No llega a dibujarse una última vez.
- 6Sonidotambién en pausa
El oído y los sonidos colocados en el mundo siguen a lo que hay en pantalla.
- 7Dibujotambién en pausa
Se calcula dónde está cada cosa y se pinta el fotograma, escena a escena en el orden en que se lanzaron.
dt son los segundos que han pasado desde el fotograma anterior. Tiene dos ajustes que conviene
conocer:
- Tiene un tope de 0,25 segundos. Si el navegador se para (cambias de pestaña y vuelves), el juego no da un salto de varios segundos de golpe.
- Es tiempo de juego. Se multiplica por
timeScale(la cámara lenta) y vale0con el juego en pausa.
Si un useUpdate lanza un error, se pierde el resto de actualizaciones de su escena en ese
fotograma, nada más: las otras escenas siguen y el fotograma se dibuja. El juego avisa con su
evento error.
Dentro de la escena: quién va primero
Una escena es un árbol de objetos, y se recorre de arriba abajo: primero el objeto, después sus hijos, rama a rama. Así, cuando la espada se actualiza, el jugador del que cuelga ya se ha movido en este fotograma.
Dos detalles que evitan fallos raros:
- Lo que nace en un fotograma espera al siguiente. Una bala no avanza el fotograma en que se dispara, y algo que crea objetos no puede encadenarlos todos en un mismo fotograma.
- Lo que se destruye se para en el acto.
destroydeja de actualizarlo ya, aunque no salga hasta el paso de borrados. Una bala no puede anotar un segundo impacto después del que la mató.
Pausar no es parar
| En pausa | Parada | |
|---|---|---|
Sus useUpdate | no corren | no corren |
| Se dibuja | sí | no |
| Sus clics | no reacciona | no reacciona |
useSceneUnmount | no corre | corre, una vez |
| Cómo vuelve | resume(), donde estaba | arrancándola otra vez, de cero |
Un menú de pausa es justo la combinación de las dos cosas: el nivel en pausa, que se sigue viendo, y una escena de menú lanzada encima.
const Level = () => {
const scene = useScene();
const keys = useKeyboard();
useUpdate(() => {
if (keys.justPressed('Escape')) {
scene.pause(); // el nivel se queda quieto, pero se ve
scene.launch('PauseMenu'); // y el menú se dibuja encima
}
});
return createScene();
};
const PauseMenu = () => {
const scene = useScene();
const keys = useKeyboard();
useUpdate(() => {
if (keys.justPressed('Escape')) {
scene.resume('Level'); // el nivel sigue donde estaba
scene.stop(); // y el menú se va
}
});
return createScene();
};El nivel no puede quitarse la pausa a sí mismo: en pausa, sus useUpdate no corren. Por eso la
quita el menú.
Cambiar de escena
scene.change('Level2') sustituye la escena que lo llama por otra. Hay dos formas.
Reglas que conviene saber:
- Solo cuenta la primera llamada. La condición que dispara un cambio suele seguir siendo cierta durante varios fotogramas, y cada uno arrancaría otra copia. El motor ignora las siguientes.
- Un cambio a la vez. Mientras una transición está en marcha, otro
changese ignora con un aviso. - Cambiar a su propio nombre la reinicia, siempre con corte seco: para cubrirla harían falta dos copias de la misma escena a la vez.
- La transición va con su propio reloj. Ni la pausa ni
timeScalela frenan: una cámara lenta no puede alargar un fundido de 300 ms hasta tres segundos.
Varias escenas a la vez
scene.launch('Map') arranca otra escena junto a las que ya corren, sin parar ninguna. Cada
una tiene sus propias actualizaciones, su pausa y sus limpiezas, y la que se lanza después se
dibuja encima.
const Level = () => {
const scene = useScene();
const keys = useKeyboard();
let mapOpen = false;
// La M abre el mapa encima del nivel, y lo cierra.
useUpdate(() => {
if (keys.justPressed('m')) {
if (mapOpen) scene.stop('Map');
else scene.launch('Map');
mapOpen = !mapOpen;
}
});
return createScene();
};Una escena solo puede estar en marcha una vez: lanzar un nombre que ya corre es un error. Y
scene.stop() sin nombre para la escena que lo llama, como un menú que se cierra con su propio
botón.
Irse: limpiar lo que no es del motor
Cuando una escena se para (con stop, con change o porque se destruye el juego entero), el
motor recorre su árbol y ejecuta sus limpiezas, los hijos antes que el padre: así la limpieza
de una espada todavía puede llegar al jugador del que colgaba.
Lo que el motor te dio se va solo con la escena: sus sprites, sus useUpdate, sus oyentes, sus
señales. Las imágenes cargadas se quedan guardadas, para que la próxima escena que las pida no las
vuelva a descargar.
Lo que tú arrancaste fuera del motor sigue vivo si no lo paras: un setInterval, un oyente
de window, una petición que quieres cancelar. Para eso está useSceneUnmount, que corre una
sola vez al irse (y no al pausar):
const Level = () => {
const onKey = (event: KeyboardEvent) => console.log(event.key);
window.addEventListener('keydown', onKey);
// Sin esto, el oyente sigue vivo cuando el nivel acaba y salta en la escena siguiente.
useSceneUnmount(() => window.removeEventListener('keydown', onKey));
return createScene();
};El juego entero
Por encima de las escenas está el juego, con su propio principio y su propio final:
createGame('#app', { … })guarda la configuración y devuelve la función que arranca.- Al llamarla con las escenas, el motor elige con qué dibujar (WebGPU o, si no hay, WebGL2).
- Registra cada escena por su nombre.
- Arranca la escena inicial: su cuerpo corre.
- Dibuja el primer fotograma, con
dtigual a0. - Avisa con su evento
ready.
Pausar el juego no es pausar una escena. useGame().pause() congela el juego entero: ninguna
escena se actualiza y dt vale 0, pero todo se sigue dibujando y la entrada se sigue leyendo.
Las dos pausas son independientes: reanudar el juego no reanuda una escena pausada por su cuenta,
ni al revés.
Al final, destroy() para todas las escenas (con todas sus limpiezas) y el juego avisa con su
evento destroy.
En una tabla
| Qué | Cuándo corre |
|---|---|
| El cuerpo de la escena | Una vez, al arrancarla |
useUpdate | En cada fotograma, mientras la escena está en marcha |
useSceneUnmount | Una vez, al irse la escena (no al pausarla) |
scene.change | Corte: en el acto. Con transición: la vieja se va en el intercambio |
scene.launch | En el acto, junto a las demás y dibujada encima |
scene.pause / resume | En el acto; la escena sigue dibujándose |
destroy(objeto) | Deja de actualizarse ya, y sale al final del fotograma |
Evento ready del juego | Tras el primer fotograma |