Orquestador de Escenas#
Una escena acá es un clip cinemático horneado a un flipbook de PNG — un mp4 importado
una vez (ffmpeg lo extrae cuadro a cuadro en el momento de importar) y reproducido como una
secuencia plana de imágenes, exactamente igual que una animación de personaje. No es lo
mismo que una cinemática (core/cutscene.js, autorada por el Editor de Cinemáticas): una
cinemática es una lista ordenada de pasos escriptados del motor — caminatas de NPC, líneas de
SAY, esperas — delimitada a una sola sala. El paso PLAYSCENE de una cinemática reproduce
una escena del Orquestador de Escenas como uno de sus pasos y bloquea hasta que termina; las dos
herramientas editan dos formas de datos distintas y no son intercambiables. No hay ningún
códec de video en el runtime — por diseño (ver la nota de cierre) — una escena es solo cuadros
PNG avanzando sobre un reloj, así que se reproduce igual en cada plataforma que el motor
soporta, incluidos los ports que no pueden depender de <video>.
Llegas a él desde el Hub (con el dev server corriendo). La barra de herramientas tiene un desplegable Escena, + Nuevo, Guardar (Ctrl+S, escribe el descriptor y lo registra), un gestor de Clips, una herramienta Replace Color (cuentagotas + tolerancia/mezcla, hornea un PNG recoloreado para un clip), Deshacer/Rehacer, y GC (barre los recursos de escena sin usar a una carpeta de basura reversible). La columna izquierda es una vista previa (con paneo/zoom y controles de transporte) sobre una línea de tiempo de pistas; el panel derecho muestra propiedades de lo que esté seleccionado — pista, keyframe o cue.
Límites: cuánto video cabe en una escena#
Una escena reproduce cuadros PNG, y el player decodifica todos los cuadros de todos los clips de la escena antes de arrancarla: eso es lo que hace que se vea igual en todas las plataformas, y también es el techo. El presupuesto que importa no es el tamaño del mp4 sino cuadros × ancho × alto × 4 bytes ya decodificados. El importador muestra esa cifra junto a la estimación de disco y la usa como compuerta: ámbar arriba de 1 GB (carga, pero lento, y una máquina modesta puede no lograrlo), rojo arriba de 3 GB (el import se rechaza: baja los fps o la escala, o recorta). El mismo límite se aplica durante el import mismo, así que no hay forma de colar un clip.
En segundos, con los tamaños de cuadro y las tasas que más vas a usar:
| Tamaño de cuadro | fps | cómodo (≤ 1 GB) | límite duro (3 GB) |
|---|---|---|---|
| 1920 × 1080 | 24 | ~5 s | ~16 s |
| 1920 × 1080 | 12 | ~11 s | ~32 s |
| 960 × 540 | 24 | ~22 s | ~65 s |
| 960 × 540 | 12 | ~43 s | ~2 min |
Las escenas son para piezas cinemáticas cortas: un logo, una title card, unos segundos de movimiento. Lo más largo va en un editor de video, recortado a las piezas que de verdad necesitas.
Cómo grabar para importar (OBS o cualquier grabador de pantalla):
- Resolución = la de tu proyecto (1920 × 1080 para un proyecto 1080p), nunca mayor. Un cuadro más grande se escala al dibujar de todos modos; solo cuesta disco, RAM y tiempo de import. Si le pasas una grabación 4K, el importador baja la escala solo para ajustar al ancho del proyecto.
- 24–30 fps. A 60 fps duplicas los cuadros sin ganancia visible en un flipbook. El importador remuestrea a los fps que elijas, así que una fuente a 60 funciona, solo decodifica el doble de cuadros.
- El bitrate casi no importa. Cada cuadro se decodifica completo al importar, así que un master de alta calidad no cuesta nada extra; lo que pesa es la cantidad y el tamaño de los PNG en que se convierte.
- Clips de segundos, no de minutos, y recorta en el importador (desde + cantidad) cuando la grabación trae más que la toma que quieres.
Anatomía de una escena#
Un descriptor de escena ({ id, duration, bg?, endMode?, keepMusic?, onEnd?, tracks: [...] })
es un objeto de datos
puro — serializado como un pequeño módulo ES y guardado en projects/<id>/scenes/<sceneId>.js.
Guardar también agrega, de forma idempotente, una entrada a core/scenes/index.js, pero ese
archivo es solo un mapa chico id→descriptor; en la práctica el runtime nunca necesita ese
registro hecho al guardar, porque _ensureScene() de shell/main.js importa dinámicamente el
descriptor de forma perezosa en el primer PLAYSCENE/REWINDSCENE y lo registra ahí — el core
queda totalmente desacoplado de las rutas del proyecto.
Una escena tiene cinco tipos de pista, todas compartiendo una sola línea de tiempo:
- Pistas de sprite (la opción por defecto, sin
kind) — un clip (unFrame[]con nombre, del registro de clips del proyecto) más una lista de keyframes ({ t, x, y, scale, rot, alpha, ease }) que maneja una curva de transformación independiente encima de la reproducción cuadro a cuadro propia del clip.in/outacotan cuándo la pista es visible en el tiempo de la escena;zordena las capas;anchory un preset opcional defx(un filtro visual por clip, resuelto a un filtro real solo en el shell) completan el cuadro. Acá corren dos relojes en paralelo: la línea de tiempo maestra (keyframes, navegable, con ease) y el reloj interno del clip (ciclo de cuadros, reusando exactamente el mismo primitivotickAnim()que usan las animaciones de personaje). El registro en sí (scenes/clips.js) es unFrame[]plano por clip ({ sprite, ms }por cuadro). Una entrada con cualquier otra forma aparece como omitida en el gestor de Clips, con su id, y se vuelve a escribir intacta cuando guardas — nunca se pierde. - Pistas de audio (
kind: 'audio') — un clip de audio posicionado y recortable (src,at,trimStart/trimEnd,vol,fadeIn/fadeOut,muted). Cuando el reloj maestro cruzaat, dispara un único token, disparado por flanco,SCENEAUDIO:<src>|<trimStart>|<dur>|<vol>|<fadeIn>|<fadeOut>; el shell lo reproduce como un one-shot en el bus de música (GameAudio.playMusic), detenido automáticamente (con el propio fade-out del clip) cuando termina la escena — salvo que la escena tengakeepMusic(ver Cómo termina una escena). - Pistas de cue (
kind: 'cue') — una lista de pares{ t, token }: tokens de efecto arbitrarios disparados en el instante en que el reloj maestro llega at. Así es como una escena dispara cualquier cosa a mitad de reproducción — una flag, un sonido, un warp de sala — sin esperar a que la escena termine. - Pistas de FX (
kind: 'fx') — un preset de filtro de pantalla completa (glitch/blur/etc.) con su propia ventanain/outyopacity, dibujado sobre todo el cuadro en vez de como un sprite posicionado. - Pistas de texto (
kind: 'text') — un rótulo o título en pantalla. Como una pista de sprite lleva una lista de keyframes (posición, escala, rotación, alpha en el tiempo, con easing por segmento) y unz, pero en vez de un clip dibuja una línea de texto con un lote completo de estilo: fuente y tamaño, color, alineación, negrita / cursiva / subrayado / tachado, un contorno (outlineWidth), una sombra, ylineHeight/letterSpacing. El texto en sí se autora monolingüe y va respaldado por unlid, así el Editor de Traducción completa los demás idiomas después — la misma regla content-i18n que las líneas habladas y los nombres de objeto. Se renderiza idéntico en el preview del editor, la exportación MP4 y en el juego (el core lleva la forma; el shell resuelve ellidy elfont).
Edición en la línea de tiempo#
Dos herramientas de puntero viven en la barra. ➤ Seleccionar (V) es la default — haz clic en una pista o clip para seleccionarlo, arrastra para moverlo por su carril. ✂ Cortar (C) parte un clip en el marcador del cabezal: haz clic en cualquier clip y se vuelve dos clips consecutivos. Cada mitad reinicia su propia reproducción desde el frame 0, así que un corte es una partición genuina en dos clips independientes, no un recorte de uno.
Las pistas se reordenan verticalmente: agarra el encabezado de una pista y arrástralo arriba o
abajo a un nuevo lugar, y el editor renumera el z según el orden resultante de arriba hacia abajo
— lo que ves apilado en la línea de tiempo es el orden de pintado en pantalla. El propio clip de
una pista de sprite se puede arrastrar verticalmente fuera de su carril con el mismo efecto.
Cuando dos clips de audio se solapan en el tiempo, aparece un helper ⤬ crossfade en las
propiedades del clip seleccionado: un clic fija el fadeOut del clip anterior y el fadeIn del
posterior al largo del solape, convirtiéndolo en un crossfade estilo DAW hecho con los campos de
fade que el runtime ya reproduce — sin tipo de pista nuevo, sin datos especiales.
El pipeline de tokens de efecto (una sola gramática, un solo despacho)#
Tanto los tokens de cue como la lista onEnd de la escena son tokens de efecto comunes del
mismo manifiesto compartido que usa cualquier otro sistema (reglas, diálogos, cinemáticas) —
autorados con el mismo widget de picker tipado, nunca texto libre para un argumento que porte un
ID. La lista de verbos que se muestra acá está curada para el contexto de escena: oculta tokens
que solo tienen sentido dentro de la lógica de sala en vivo (scroll de inventario, restaurar
hotspot, guardar/cargar, recoger ítem) y promueve arriba los verbos de warp de sala / audio /
flag / diálogo / cinemática / escena; los pickers de cue además promueven primero el cuarteto de
audio (SOUND/MUSIC/STOPMUSIC/PLAYFX), ya que un cue a mitad de escena casi siempre es un
golpe de audio.
onEnd guarda una lista, y sus filas llevan un asa ⠿ — arrastra una para cambiar el orden en
que se disparan los tokens al terminar la escena. Un cue no tiene asa a propósito: guarda una
sola ranura de token, así que no hay nada que reordenar; a un cue lo mueves en el tiempo
arrastrándolo por su pista.
WAIT y WAITFOR quedan afuera por otro motivo: una escena es una línea de tiempo, así que
sus beats se espacian moviéndolos por la pista, no pausando entre tokens. Cuando la demora
pertenece al juego en vivo —una línea hablada que necesita un momento antes de que cambie la
sala— eso es una cadena de reacción, y ahí las dos esperas sí están disponibles.
En runtime, el tickScene() puro de core/scenes/timeline.js devuelve cada cuadro un arreglo
fired con los tokens que vencieron; shell/main.js los pasa por el mismo despacho
applyEffects() de dos niveles que se usa en todo el motor — los tokens de Nivel 1
(SETFLAG, GIVEITEM, WARPTOROOM, etc.) mutan el estado directamente, y lo que sobra
(SCENEAUDIO, SAY, MUSIC) cae al despachador de acciones genérico. Un solo almacén de
reglas, una sola gramática, sin un intérprete separado solo para escenas.
Cómo termina una escena#
La ventana in/out de cada pista es inclusiva en out: una pista sin out propio hereda
scene.duration y se ve hasta el último milisegundo. Cortar a negro antes del final es decisión
del autor — para eso está el out de esa pista. (Antes, el frame que caía justo en duration
cullaba todas las pistas, así que el último frame de cualquier escena salía negro.)
Dos campos a nivel escena deciden qué pasa cuando el reloj llega a duration:
endMode—'end'(por defecto) termina la escena y devuelve el control al juego.'hold'la deja estacionada ahí: el reloj maestro sigue corriendo, así quetransformAtsostiene el último keyframe y los clips en loop siguen ciclando — la toma final queda viva en vez de congelarse en una foto. Una escena en hold termina solo cuando el jugador la saltea (el mismo gesto de mantener presionado, o Escape), yonEndse difiere a ese momento: dispararlo al cruzardurationejecutaría justo elWARPTOROOM/ avance de estructura que saca al jugador de la toma que pediste sostener. Es la escena de créditos clásica: un título volando, música de fondo, hasta que el jugador decide salir.keepMusic— por defecto la música de la escena (una pista de audio o un cueMUSIC:— las dos van al bus de música) se corta al salir. Con este campo sobrevive a lo que venga después: el menú, el siguiente bloque de la estructura, el cuarto que estaba atrás.
Ambos se editan en el panel de la escena y solo se serializan cuando difieren del default, así que una escena que termina de la forma habitual queda byte a byte igual que antes.
Reproducción: saltar y rebobinar#
El jugador puede hacer clic para saltar un paso y mantener presionado para saltar toda
una escena: mantener presionado llena una barra de progreso de ~1.1s, y al completarse el reloj
de la escena se adelanta directo a duration para que el siguiente tick cruce el final de
forma natural y dispare onEnd — el runtime nunca anula la escena activa directamente, así que
onEnd no puede quedar varado por un salto. En una escena con endMode: 'hold', el salto
completado termina la escena directamente (adelantar el reloj solo volvería a entrar al hold).
El aviso de todo esto ya no aparece encima del primer frame: sale recién con la primera tecla o clic del jugador durante la escena. Una escena en hold además lo muestra sola tras unos segundos estacionada en el final, para que nadie se quede en los créditos sin saber cómo salir.
REWINDSCENE:<sceneId>[|speed=N] (velocidad por defecto 2) reproduce una escena hacia
atrás con una estética VHS, manejada por el seekScene() puro — un render sin efectos
secundarios en un tiempo absoluto y decreciente. Esto es solo visual: seekScene no dispara
ningún token de cue, audio, u onEnd mientras rebobina, por construcción (nunca toca
sceneState ni el arreglo fired). No hay UI de autoría para esto en el propio Orquestador de
Escenas — es un token a nivel de runtime, alcanzable desde el slot de efecto raw de una
cinemática (la vía de escape del Editor de Cinemáticas) o desde cualquier otro lugar donde se
pueda despachar un token, no es una pista o tipo de paso propio acá.
Audio mientras corre una escena#
Una escena es dueña del paisaje sonoro: arrancar una agacha (duck) de inmediato el ambiente
de la sala (antes incluso de que termine la carga asíncrona del clip, así nada se cuela debajo
de una escena durante el hueco de carga), y el ambiente se reanuda apenas la escena termina —
incluidas las salidas anormales (un rebobinado que termina, o una carga fallida). Las propias
pistas de audio de una escena viajan por el bus de música compartido vía SCENEAUDIO y no se
ven afectadas por el ducking, ya que son parte intencional de la escena, no de la sala.
Importar un clip desde video#
Clips → NUEVO CLIP DESDE VIDEO elige un mp4 y lo extrae en un modo escena que se salta
los pasos exclusivos de personaje (limpieza de chroma key, anclaje al lienzo del personaje) y
escribe los cuadros completos en assets/scenes/<clip>/. Perillas: fps (remuestreo — una
cinemática estilizada suele vivir en 12–15 fps), escala (1× / 0.75× / 0.5× — preajustada
para caber en el ancho de tu proyecto cuando la fuente es más ancha), y un recorte de
entrada/salida (desde + cantidad). Tres presets (Ligero 12fps·0.5×, Equilibrado
15fps·0.75×, Calidad 24fps·1×) fijan las dos primeras de un golpe.
Preview frame 0 hace más que mostrar una miniatura: muestrea un puñado de cuadros repartidos por el clip para medir cuánto pesan de verdad los PNG (un primer cuadro oscuro, solo, puede errar por una cuarta parte), y lee el tamaño, los fps y la duración del clip. Desde ahí, la línea bajo los botones muestra los dos costos en vivo mientras mueves las perillas: disco (≈ MB, cuadros × bytes por cuadro) y RAM al reproducir la escena (la cifra decodificada de la que hablan los límites de arriba), y se pone ámbar o roja con la compuerta.
El import corre junto a su destino: los cuadros se extraen directo a una carpeta de trabajo al
lado de assets/scenes/<clip>/, en el disco del proyecto (nunca en la carpeta temporal del
sistema), la barra de progreso sigue a ffmpeg cuadro por cuadro, y al final la carpeta terminada
se pone en su lugar. Antes de arrancar, el importador compara el espacio libre de ese disco con
la estimación y se niega de entrada en vez de fallar a la mitad. Las fuentes opacas se escriben
como RGB; el alpha se conserva solo cuando el video de verdad lo trae. Al importar, el clip nuevo
se agrega al registro de clips del proyecto (persistido la próxima vez que guardes desde el
gestor de Clips). El ffmpeg que se usa es el que apuntaste en Hub Config, o el de tu PATH.
Flujo de trabajo#
- Elige una Escena existente o + Nuevo para arrancar una en blanco.
- Clips → NUEVO CLIP DESDE VIDEO para traer metraje cinemático (o reusa un clip existente), luego +🎬 / +🔊 / +✨ en la línea de tiempo para agregar una pista de sprite, audio, o FX de pantalla completa — más Agregar texto / Agregar trigger desde el panel de agregar pista para un rótulo o una pista de cue.
- Para una pista de sprite, coloca keyframes en la línea de tiempo para mover/escalar/
rotar/desvanecerla en el tiempo, con ease por segmento; arrastra sus manijas
in/outpara acotar su visibilidad. - Agrega tokens de cue en momentos específicos para golpes a mitad de escena (sonido,
flags, warps); define la lista
onEndde la escena para lo que pasa cuando termina. - Previsualiza con los controles de transporte (reproducir/pausar, rebobinar/avanzar al final, detener) y la línea de tiempo con zoom; usa Replace Color si necesitas una variante recoloreada de un PNG importado.
- Guardar — escribe
projects/<id>/scenes/<sceneId>.jsy registra el id. - Cabléalo desde un token
PLAYSCENE:<id>(oREWINDSCENE:<id>) en cualquier lugar donde se pueda disparar un token — lo más común: un pasoPLAYSCENEdel Editor de Cinemáticas, una reacción de hotspot, o un bloque scene del Diseñador de Estructura del Juego.
Los tiempos de cue y keyframe de una escena quedan horneados contra la cantidad de cuadros y el fps del clip importado en el momento de la importación. Cada
ten un cue, un keyframe, o una ventanain/outes un offset absoluto en milisegundos dentro del reloj maestro de la escena — no queda ninguna relación de vuelta al mp4 original una vez que los cuadros están extraídos. Volver a importar una versión recortada o re-codificada del mismo metraje (fps distinto, puntos de recorte distintos) produce una secuencia de cuadros nueva cuyo tiempo ya no coincide con los cues y keyframes autorados contra la anterior — van a disparar antes, después, o sobre cuadros en blanco. Trata un clip importado como definitivo antes de gastar tiempo colocando cues y keyframes contra él; si tienes que reimportar, espera tener que revisar cada cue y keyframe de las pistas que usan ese clip.