Documentación · cómo funciona

El ciclo de vida de una escena

Todo lo que le pasa a una escena desde que arranca hasta que se va, en el orden exacto en que lo hace el motor.

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

arranque · launch · changecreateScene()change con transiciónen el intercambiopause()resume()stop · change · destroystop · change · destroyse puede volver a arrancar, desde ceroRegistradaun nombre, sin arrancarNaciendosu cuerpo corre una vezRetenidacarga, pero ni se ve ni correEn marchauseUpdate cada fotogramaEn pausase dibuja, no se actualizaParadacorre useSceneUnmount
Una escena pasa por estos estados. Solo «En marcha» ejecuta sus useUpdate; «En marcha» y «En pausa» son los que se ven.

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:

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 Level

Un fotograma, en orden

Sesenta veces por segundo, en este orden:

  1. 1
    Entradatambié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.

  2. 2
    Transició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.

  3. 3
    Actualizaciones

    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.

  4. 4
    Cierre 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.

  5. 5
    Borradostambién en pausa

    Lo que se destruyó con destroy sale ahora, cuando nadie está recorriendo las escenas. No llega a dibujarse una última vez.

  6. 6
    Sonidotambién en pausa

    El oído y los sonidos colocados en el mundo siguen a lo que hay en pantalla.

  7. 7
    Dibujotambié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.

Con el juego en pausa solo se salta el paso 3: todo lo demás sigue. Por eso un menú de pausa se ve y se puede pulsar.

dt son los segundos que han pasado desde el fotograma anterior. Tiene dos ajustes que conviene conocer:

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

Escena1Jugador2Espada3Enemigo4Balanace en este fotograma: empieza en el siguiente
Primero los useUpdate del padre, en el orden en que se apuntaron, y después cada hijo con toda su rama. Lo que nace durante un fotograma no se mueve hasta el siguiente.

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:

Pausar no es parar

En pausaParada
Sus useUpdateno correnno corren
Se dibujasíno
Sus clicsno reaccionano reacciona
useSceneUnmountno correcorre, una vez
Cómo vuelveresume(), donde estabaarrancá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.

scene.change('Level2')
Este fotogramaEl siguienteMenuSe para al momento: no se dibuja más y corre su useSceneUnmountLevel2Nace (su cuerpo corre) y ya se dibujaSu primer useUpdate
El corte seco: la nueva nace primero y la vieja se para después, en el mismo fotograma. Así, si el nombre no existe, el error salta antes de que se pare nada.
scene.change('Level2', { transition: fade(300) })
CubrirIntercambioDescubrirMenuSe ve y sigue actualizándoseSe para: corre su useSceneUnmountLevel2Retenida: ya nació y está cargando, pero ni se ve ni se actualizaSe suelta: se actualiza y se dibuja, aún bajo la cortinaSe ve
Con transición, el intercambio espera a lo más tarde de dos cosas: que la pantalla esté cubierta y que haya llegado todo lo que la escena nueva pidió cargar. La transición tapa el corte y, además, la carga.

Reglas que conviene saber:

Varias escenas a la vez

Maplanzada después, encimaLevellanzada primero
Las escenas en marcha se dibujan en el orden en que se lanzaron: la última, encima.

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:

  1. createGame('#app', { … }) guarda la configuración y devuelve la función que arranca.
  2. Al llamarla con las escenas, el motor elige con qué dibujar (WebGPU o, si no hay, WebGL2).
  3. Registra cada escena por su nombre.
  4. Arranca la escena inicial: su cuerpo corre.
  5. Dibuja el primer fotograma, con dt igual a 0.
  6. 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 escenaUna vez, al arrancarla
useUpdateEn cada fotograma, mientras la escena está en marcha
useSceneUnmountUna vez, al irse la escena (no al pausarla)
scene.changeCorte: en el acto. Con transición: la vieja se va en el intercambio
scene.launchEn el acto, junto a las demás y dibujada encima
scene.pause / resumeEn el acto; la escena sigue dibujándose
destroy(objeto)Deja de actualizarse ya, y sale al final del fotograma
Evento ready del juegoTras el primer fotograma