Un fichero .scene es una escena escrita como JSON. Esta página es para quien hace una herramienta
que lee o produce una: un editor, un importador, un generador de niveles, un paso de compilación.
Está en nacatamalon: format: "nacatamalon-scene", version: 1.
0. Qué es un documento de escena, y qué no
Un documento de escena describe una escena: un árbol de cajas, qué es cada una y dónde está.
No describe el juego. El tamaño de la ventana, su color de fondo, el mapa de controles y el
aspecto de la pantalla son de project.json, porque «este juego parece de Mega Drive» no es algo de
la sala que esté abierta en ese momento.
Tampoco describe tus ficheros. Un shader, un atlas, las celdas de un mapa y los números de un efecto de partículas se quedan cada uno en su fichero y aquí solo aparecen por su nombre.
Tres funciones son todo lo que hay. Leer es cosa del juego (nacatamalon) y escribir, de una
herramienta (nacatamalon/authoring):
import { createGame } from 'nacatamalon';
import { parseSceneDoc, sceneFromDoc } from 'nacatamalon/files';
import { serializeScene } from 'nacatamalon/authoring';
const doc = parseSceneDoc(await (await fetch('/scenes/level1.scene')).json(), '/scenes/level1.scene');
createGame('#app', options)({ Level1: sceneFromDoc(doc) }); // ya es una escena como otra cualquiera
const written = serializeScene(root); // y una que corre vuelve a ser JSONO la descarga el motor, en el cuerpo de una escena, como carga un dibujo:
const cave = useLoadScene({ src: 'scenes/cave.scene' }); // se añade al llegar, como cave.name: el nombre que trae dentroLo que el motor no hace nunca es buscar un fichero de escena por su nombre: una página no puede
listar una carpeta, así que qué ficheros tiene un juego lo dice quien lo aloja (el editor los guarda
en src/scenes.ts).
sceneFromDoc devuelve una función de escena normal. A partir de ahí nada en el motor sabe que
la escena salió de un fichero: arrancarla, cambiar a ella, tapar ese cambio con una transición,
pausarla y destruirla funcionan porque no tiene nada de especial.
1. La forma
{
"format": "nacatamalon-scene",
"version": 1,
"name": "Level1",
"assets": [ /* todo lo que hay que descargar, por su nombre */ ],
"root": { /* el árbol */ }
}| Campo | Qué es |
|---|---|
format | Siempre "nacatamalon-scene". Un fichero que no lo dice se rechaza, no se lee a medias. |
version | La versión del formato. Un número mayor se lee igualmente, con un aviso. |
name | Cómo se llama la escena y con qué se arranca. |
assets | Todo lo que hay que descargar, por el nombre con que lo llama el árbol. |
root | La caja de la propia escena. Sus hijas son lo que contiene la escena. |
Por qué name es un campo. En este motor una escena se llama por la clave con que se registró, y
createScene() no recibe nombre a propósito. Sin este campo el documento solo se podría reconocer
por el nombre del fichero, y eso deja de servir en cuanto se mueve o se mete en un paquete.
2. Las cinco reglas
Todo lo demás sale de estas. Merece la pena leerlas una vez; después las tablas se explican solas.
Un campo que falta es un campo con su valor por defecto
Hay una sola convención. Sin visible es visible; sin flipX no está volteado. Un valor por defecto
nunca se escribe, porque escribirlo es una decisión que nadie tomó, y la versión que cambie ese valor
no podría distinguir una cosa de la otra.
Un fichero se nombra, nunca se copia dentro
El código de un shader, las tablas de un atlas, las celdas de un mapa, los parámetros de un efecto: todo se queda en su fichero y la escena lo nombra. Un documento con una copia lleva una copia que se queda vieja en cuanto alguien edita el fichero, y un diff que nadie puede leer.
A un dibujable lo coloca su caja; una cámara y una luz se colocan solas
Dónde está un dibujo es cosa de su caja, así que un sprite y la sombra que tiene debajo se mueven juntos diciéndolo una sola vez.
Una cámara y una luz son la excepción, por un motivo: la composición no les llega, a propósito. Nada mueve una cámara, porque es contra lo que se mide todo lo demás. Una luz sí la mueve el árbol, pero nunca la gira, para que un farol en la mano de alguien que da vueltas no barra la habitación. Lo que el árbol no puede decidir, lo lleva el componente.
Lo desconocido se guarda, nunca se borra
Un componente de un tipo que esta versión no conoce, un campo que es de otra herramienta: los dos sobreviven a la lectura y se vuelven a escribir donde estaban. Borrarlos reescribiría en silencio el fichero de alguien la primera vez que se abriera y se guardara.
Nada lanza un error
parseSceneDoc siempre devuelve algo. Un campo que falta, con la forma equivocada o sin sentido
vuelve a su valor por defecto y el nivel se abre igual. serializeScene también: lo que no puede
escribir se deja fuera con un aviso en vez de tirar el documento entero.
3. assets: qué descargar
Se recorre antes de construir una sola caja. Cada cargador apunta su sitio en cuanto se le pide y
lo llena después, así que cuando un sprite dice «mi textura es hero», hero ya está ahí, quizá
todavía llegando.
key es como lo llama el árbol; src es dónde está. Suelen ser la misma cadena, y cuando solo se
escribe key, src toma su valor.
{ "type": "texture", "key": "hero", "src": "/assets/hero.png" }
{ "type": "atlas", "key": "player","src": "/atlases/player.atlas" }
{ "type": "shader", "key": "crt", "src": "/shaders/crt.wgsl" }
{ "type": "particles", "key": "fire", "src": "/effects/fire.particles" }
{ "type": "tilemap", "key": "level", "src": "/maps/level.tilemap" }
{ "type": "audio", "key": "cheer", "src": "/audio/cheer.ogg" }
{ "type": "song", "key": "theme", "src": "/music/theme.song" }
{ "type": "motion", "key": "intro", "src": "/motions/intro.motion" }
{ "type": "bitmap-font", "key": "pac", "json": "/fonts/Pac-Man.json", "atlas": "/fonts/Pac-Man.png" }
{ "type": "font", "key": "inter", "src": "/fonts/Inter.ttf", "size": 64 }
{ "type": "geometry", "key": "cube:1:1:1", "source": { "kind": "cube" } }Una fuente bitmap son dos ficheros, a propósito: las medidas y la hoja. Adivinar uno a partir del otro es el tipo de regla que funciona hasta que alguien pone un nombre raro. Su hoja no se registra además como textura propia.
Una fuente vectorial es un fichero (.ttf o .woff). Su atlas se dibuja en el juego, letra a
letra, así que nunca aparece en la lista; size (píxeles por em en ese atlas) solo se escribe cuando
no es el de por defecto, 48. Un texto nombra cualquiera de las dos por su clave, y quien lee busca en
las dos.
Una forma es una receta, no sus esquinas. Un cubo son unos pocos números de descripción o unos cientos de datos de vértices, y la descripción es lo que una persona edita, lo que enseña un diff y lo que sobrevive a que el motor cambie cómo construye un cubo.
kind | Campos (todos opcionales) |
|---|---|
cube | width, height, depth |
plane | width, depth, widthSegments, depthSegments |
circle | radius, segments |
uvSphere | radius, segments, rings |
icoSphere | radius, subdivisions |
cylinder | radius, height, segments |
cone | radius, height, segments |
torus | radius, tube, radialSegments, tubularSegments |
lathe | profile (obligatorio: una lista de { "x", "y" }, de abajo arriba, al menos dos, x no menor que 0), segments |
gltf | src (obligatorio), node (el nombre de la pieza, "" para una que el fichero no nombró), occurrence (cuál de las piezas con ese nombre, desde 0, contadas en el orden en que se recorre el fichero) |
Una forma hecha con datos de vértices sueltos no tiene receta. No se puede escribir, así que un modelo que la usa se deja fuera del documento con un aviso.
4. root y el árbol
{
"id": "player",
"name": "Player",
"transform": { "x": 40, "y": 120, "z": 0, "rotation": 0, "rotationX": 0, "rotationY": 0,
"scaleX": 1, "scaleY": 1, "scaleZ": 1 },
"components": [ /* lo que esta caja ES */ ],
"children": [ /* otras cajas colocadas respecto a ella */ ],
"data": [ { "value": 3 } ], // los valores de useData, en el orden en que se declararon; falta si no hay
"tags": ["enemy"], // lo que dice useTag que es; falta si no hay
"visible": false, // si falta, es visible
"screenSpace": true, // si falta, vive en el mundo
"worldOffScreen": true, // solo en la raíz: la pantalla enseña lo fijado a ella, y el mundo solo por vistas
"extra": { } // lo que esta versión no usa
}id es estable y tiene que seguir siéndolo. Es la única dirección que tiene todo lo que está
fuera del motor: una selección recordada, un enlace entre dos cajas, un paso de deshacer. Generarlo
de nuevo al cargar los rompe todos, sin avisar.
transform: null quiere decir «en ningún sitio en concreto», que es lo que debe costar una caja
vacía que solo agrupa: nada. Escribe null en vez de una transformación identidad, o cada caja de
agrupar pagará una colocación que no tiene.
Una transformación a medias se completa con la identidad, así que { "x": 40 } es una caja a
cuarenta de ancho y por lo demás normal.
Una transformación puede llevar "pivot": { "x": 0, "y": -1, "z": 0 }: el punto, en sus propias
unidades, sobre el que gira y crece en tres dimensiones. Si falta es el origen, y solo se escribe
cuando está puesto. No mueve nada por estar ahí; solo un giro o un crecimiento van alrededor de él.
Lo que lleva no son hijas. Un componente de components es algo que la caja es (su dibujo, su
forma, su cámara). Una caja de children es otro objeto colocado respecto a ella.
data va por posición. Cada entrada es un valor de useData, en el orden en que los declara el
código de la caja, así que el código que los lee de vuelta tiene que declararlos en el mismo orden.
extra
Lo que el fichero decía de una caja y esta versión no entiende, guardado tal cual y vuelto a escribir sin tocar. Aquí caen dos cosas:
- un campo de una versión más nueva del formato;
- un campo que es de otro: el enlace de una herramienta a una caja reutilizable, una nota que dejó un importador.
Un componente de un tipo desconocido cae en extra.unsupportedComponents, y vuelve a components
cuando la escena se escribe otra vez.
5. components: qué es una caja
Los tipos, agrupados abajo por lo que son. Todos llevan type e id.
| Grupo | Tipos |
|---|---|
| Se dibujan | sprite, nine-slice, text, shapes, mesh, tilemap, particles, particles-3d |
| Cómo se ve la escena | camera-2d, camera-3d, fog, light, sprite-texture |
| Dónde se dibuja | mask, filter |
| Comportamiento y estado | script, store, motion |
| Sonido | sound, music, song, audio-listener |
| Física | physics-body-2d, physics-body-3d, physics-world-2d, physics-world-3d |
| Partículas que chocan | particle-collider-2d, particle-collider-3d |
Cada tipo que se dibuja puede llevar su propia transform, medida sobre su caja y omitida cuando
es la esquina u origen de la caja (los planos llevan la transformación 2D de cinco campos; mesh y
particles-3d, la 3D de nueve). createSprite({ transform }) escribe ahí, que es como se escriben
casi todas las escenas en código.
sprite
{ "type": "sprite", "id": "art", "texture": "hero", "tint": { "r": 1, "g": 1, "b": 1, "a": 1 },
"width": 32, "height": 32, "anchor": { "x": 0.5, "y": 0.5 }, "zIndex": 3 }| Campo | Notas |
|---|---|
texture | Una clave de asset, o null. |
atlas, frame | Una hoja cortada en cuadrícula, y qué celda. |
width, height | Si faltan, toma el tamaño de la textura, decidido cuando llega. Escribir los números de hoy congela el sprite al tamaño del dibujo que tenía al guardarse. |
tint | Cuatro canales de 0 a 1. |
anchor | Qué punto del sprite cae sobre su transformación, de 0 a 1 de su propio tamaño. |
uvOffset, uvScale | Qué parte de la hoja, cuando no la corta un atlas. |
animation | { autoplay, speed }: con qué carrera del atlas empieza (null se queda en frame) y a qué velocidad (1 son los fps de la carrera). Las carreras son del fichero .atlas y nunca se copian aquí. Necesita atlas. |
pivot | El punto sobre el que gira y crece, de 0 a 1 de su propio tamaño. Si falta, es el anchor. |
blend | 'additive' para sumar su luz a lo que hay detrás; si falta, lo tapa. |
parallax | { x, y }: cuánto se mueve con la cámara (1 con el mundo, 0 se queda quieto). Si falta, 1. |
tile, tileOffset | El dibujo repetido a lo largo de width × height en vez de estirado, y dónde empieza el patrón. |
flipX, flipY, smooth, visible, zIndex | |
material, uniforms | Ver §6. |
nine-slice
Un panel: un dibujo cuyas esquinas se mantienen y cuyos bordes y centro se estiran a su tamaño, para un botón o un cuadro de diálogo.
{ "type": "nine-slice", "id": "box", "texture": "ui/panel", "tint": { "r": 1, "g": 1, "b": 1, "a": 1 },
"width": 120, "height": 48, "slice": { "left": 8, "top": 8, "right": 8, "bottom": 8 } }| Campo | Notas |
|---|---|
texture | Una clave de asset. Un panel sin dibujo se avisa y se deja fuera de la escena que se construye. |
atlas, frame | Una hoja cortada en cuadrícula, y qué celda. |
uvOffset, uvScale | Qué parte del dibujo, cuando no la corta un fichero de atlas: una celda de una hoja hecha en código se escribe así. |
width, height | Siempre se escriben: un panel siempre se estira a un tamaño. |
slice | Los cuatro bordes, en píxeles del dibujo (de su frame, con una hoja). |
mode | Cómo se llenan los bordes y el centro: 'tile', 'tile-fit', o { x, y } uno por dirección. Si falta, 'stretch'. |
anchor, pivot, smooth, blend, parallax, visible, zIndex | Como en un sprite. |
material, uniforms | Ver §6. |
shapes
Formas que la tarjeta dibuja cada fotograma (createShapes), en el orden de la lista, cada una
primero rellena y luego con su contorno.
{ "type": "shapes", "id": "hud", "tint": { "r": 1, "g": 1, "b": 1, "a": 1 }, "shapes": [
{ "type": "rect", "x": 0, "y": 0, "width": 60, "height": 6, "radius": 2,
"fill": { "r": 0.88, "g": 0.33, "b": 0.33, "a": 1 },
"stroke": { "width": 1, "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "align": "inside" } },
{ "type": "line", "points": [{ "x": 0, "y": 20 }, { "x": 40, "y": 30 }],
"stroke": { "width": 3, "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "cap": "round" } }
] }| Campo | Notas |
|---|---|
shapes | Cada una es rect (x, y, width, height, radius un número o cuatro, rotation), circle (x, y, radius), ellipse (x, y, radiusX, radiusY, rotation), estas dos con start, end y segments; polygon (points) o line (points, closed, y un stroke, que necesita). |
fill, stroke | Un color; { width, color, join, cap, align }, cada uno omitido si es el de por defecto. Los colores se escriben como { r, g, b, a }, los escribiera como los escribiera el juego. |
visible | En una forma, solo se escribe como false. |
transform, tint, parallax, visible, zIndex | Como en un sprite. |
Una forma que no es ninguna de estas, o a la que le falta algo sin lo que no se puede dibujar (el tamaño de un rectángulo, el radio de un círculo, los puntos o el contorno de una línea), se avisa y se deja fuera al leer la escena; las demás se quedan.
text
text, font (una clave de asset), style (fontSize, align, letterSpacing, lineSpacing,
maxWidth), tint, anchor, pivot, smooth, blend, parallax, visible, zIndex,
material, uniforms.
mesh
geometry (una clave de asset), material (un material de modelo, ver §6), uniforms,
visible, zIndex, y castShadow, que solo se escribe como false (el suelo, una calcomanía): un
campo con su valor por defecto se omite. alwaysDrawn: true lo dibuja aunque no haya nada de él en
pantalla; boundsMargin es hasta dónde llega más allá de su forma, en sus propias unidades, antes de
contar como fuera de pantalla (lo que tiene que decir un material que mueve sus esquinas en el shader
para que se lo salte).
Aquí no hay esqueleto. Un rig es del fichero de modelo del que salió y se toma de ahí cuando el fichero llega; escribirlo en una escena pondría una copia del trabajo del artista en un sitio donde nunca lo va a buscar.
tilemap
Una entrada por mapa, no por capa. createTilemap lee el fichero y pone todas las capas en la
caja de una vez, así que una entrada por capa describiría algo que ninguna llamada puede hacer.
{ "type": "tilemap", "id": "level", "map": "level",
"transform": { "x": 0, "y": 0, "rotation": 0, "scaleX": 1, "scaleY": 1 },
"layers": [ { "name": "treetops", "visible": false } ] }layers lleva solo lo que esta escena cambió de cada capa, por el nombre que les dio el fichero:
tint, visible, zIndex, material, uniforms. Un mapa puesto y dejado tal cual no escribe
ninguna. El mapa en sí puede llevar tint (sobre todas las capas) y smooth.
transform va aquí y no en la caja porque un mapa está en caché y compartido: dos escenas que
enseñan un nivel miran la misma colocación. Escribirla como la de la caja diría que cada escena tiene
la suya.
particles
{ "type": "particles", "id": "torch", "name": "torch", "effect": "fire",
"tint": { "r": 1, "g": 1, "b": 1, "a": 1 }, "alpha": 1, "autoplay": true, "seed": null,
"overrides": { "sizeScale": 0.5, "speedScale": 1.6 } }Aquí no hay nada de cómo se comporta el efecto: el ritmo, las vidas y las curvas están en el
fichero .particles, que es lo que hace que tres antorchas sean un documento y un dibujo.
overrides es en qué se diferencia esta.
autoplay es lo que se quiso al hacerla. Si está emitiendo justo ahora no se escribe, para que
una escena guardada mientras una explosión estaba a medias no vuelva a medias para siempre.
seed: null toma la semilla del reloj; un número da siempre la misma nube. smooth, visible y
zIndex como en un sprite.
particles-3d
Los mismos campos, con una transform 3D. Es un tipo aparte y no una opción del plano, porque el
fichero que nombra es otra clase de cosa, y un componente que pudiera nombrar cualquiera de los dos
no podría avisar si se le apunta al equivocado.
camera-2d, camera-3d
Las dos llevan transform (la suya, ver §2) y zoom. camera-3d añade projection
("perspective" u "orthographic"), fov, near, far y, solo ortográfica, follow2d: true:
los modelos colocados en el mundo 2D, vistos por la cámara 2D.
fog
{ "type": "fog", "id": "mist", "color": { "r": 0.6, "g": 0.7, "b": 0.8, "a": 1 }, "near": 10, "far": 40,
"height": { "top": 2, "bottom": -1, "enabled": true } }La niebla a través de la que se ven los modelos. Es de la vista, como la cámara, así que se escribe
en la caja que es la cámara: la raíz de la escena, o una caja que se dibuja en un dibujo. near y
far son dónde empieza y dónde es completa; distance: false apaga eso, y height es una niebla
por altura (dónde empieza y dónde es completa), omitida si está apagada con sus alturas por defecto.
enabled: false solo cuando está apagada.
light
{ "type": "light", "id": "lamp", "kind": "point",
"color": { "r": 1, "g": 0.9, "b": 0.7, "a": 1 }, "intensity": 2, "ambient": 0.35, "range": 7,
"transform": { "x": 4, "y": 5, "z": 6, "rotationX": 0.25, "rotationY": -0.5 } }kind es directional, point, spot, rect o ambient. range está en point, spot y rect;
angle y penumbra en spot; width y height (el tamaño del panel) en rect. Una luz ambiente es
toda la habitación a la vez: sin sitio, sin dirección, sin alcance.
Su transform tiene cinco campos y no nueve, porque una luz no tiene escala ni alabeo.
Sombras: castShadow (si falta, no; en una escena solo una luz hace sombra), shadowBias (cuánto
se empuja la prueba hacia la luz, en unidades del mundo), shadowStrength (de 0 a 1), y en una
luz direccional shadowArea (hasta dónde llegan las sombras delante de la cámara) y
shadowDistance (cuánto se aleja la luz para mirarlo).
sprite-texture
{ "type": "sprite-texture", "id": "monitor", "key": "monitor-screen", "width": 160, "height": 120,
"background": { "r": 0, "g": 0, "b": 0, "a": 1 }, "sees": "3d" }
{ "type": "sprite-texture", "id": "minimap", "key": "minimap", "width": 64, "height": 64,
"background": { "r": 0, "g": 0, "b": 0, "a": 1 }, "sees": "world", "hide": { "tags": ["hud"] } }Un dibujo en el que se pinta la caja y todo lo que tiene debajo, en vez de la pantalla. Un modelo
lo enseña pidiendo key como pide una imagen cargada. Se hace antes que cualquier caja de la escena,
así que un modelo que va antes en el fichero que la pantalla que enseña la encuentra igual.
sees dice qué mira de fuera de sí mismo, y solo se escribe cuando mira algo: si falta, solo lo que
está bajo su caja (la pantalla de una consola portátil); "3d", los modelos del mundo de alrededor
por su propia cámara (un monitor de seguridad); "world", el mundo entero de su escena por sus
propias cámaras (un minimapa, un retrovisor, la mitad de una pantalla partida). Una vista del mundo
puede llevar hide y only, cada uno { tags, kinds }: lo que deja fuera, y lo único que mete. Un
documento que dice "scene", el nombre antiguo de "3d", se lee como "3d", y al guardarlo se
escribe el nombre nuevo.
script
{ "type": "script", "id": "s1", "ref": "patrol", "props": { "speed": 2 } }Un nombre y no código: ref es el nombre con que se registró el comportamiento (en un proyecto,
el nombre del fichero). props es cómo se dejó puesto en esta caja, y falta cuando el
comportamiento no declara ajustes. Solo se escriben los valores; qué claves existen lo dice el
comportamiento, así que un ajuste que gane más tarde se llena con su propio valor por defecto. Un
ref que nadie registró se guarda, con sus props, y se avisa en vez de tirarlo. Los scripts se
construyen los últimos en su caja, ocupen el sitio que ocupen en la lista, para que self esté
completo cuando corren.
store
{ "type": "store", "id": "player:store:save", "ref": "save" }Esta caja usa el store llamado ref. El store no está en la caja ni puede estarlo: dura más que
cualquier escena. El id sale de la caja y del nombre, la única identidad del formato que no se
guarda, así que un enlace sobrevive igual a ida y vuelta.
motion
{ "type": "motion", "id": "intro", "motion": "intro", "autoplay": true, "loop": false, "speed": 1.5 }Un fichero .motion que reproduce esta caja: valores movidos en el tiempo por claves. Como una
canción, se escribe cómo se pidió que sonara, nunca por dónde iba. motion es la clave de asset;
loop falta si es lo que dice el fichero, y speed si es 1.
sound, music, song, audio-listener
Se escribe cómo se pidió que sonara, nunca lo que está sonando. Una escena guardada a mitad de la música se abre lista para empezarla. Los mandos van al revés: un volumen bajado se escribe bajado.
sound:audio(una clave de asset),volume,loop,rate(también cambia el tono),pan(de-1izquierda a1derecha, falta si está en el centro),channel(el grupo de volumen, para que una pantalla de opciones mueva toda la música a la vez),autoplayy, para un sonido colocado en el mundo,spatial,refDistance,maxDistance,cone(más fuerte delante que detrás) ozone(llena una zona en vez de salir de un punto; gana aspatial).loopStartyloopEnd(segundos dentro del fichero; lo que hay antes deloopStartes una intro que suena una vez), yduck({ channel, to, fade?, release? }: otro canal bajado mientras este suena). Un sonido hecho en código a partir de un preset ({ sfx }) no tiene fichero y no se escribe.music:layers({ name, audio, volume? }, la misma melodía en varios ficheros que suenan juntos, cada uno movido por su nombre),volume,channel(falta si es'music'),autoplay,fadeInyfadeOut(segundos para subir desde el silencio conautoplayy para bajar a él cuando la caja se va; faltan si son 0),loopStart,loopEnd, ybpm,beatsPerBar(falta si es 4),firstBeat(falta si es 0) para lo que siga su ritmo.song:song(la clave de asset de un fichero.song),volume,channel(falta si es'music'),autoplay,fadeInyfadeOut(como enmusic),voices(el volumen de cada voz, solo las que no están a 1),pan(dónde suena cada voz, solo las que cambian respecto al fichero),tempo(falta si es lo que dice el fichero) ytranspose(semitonos, falta si es 0). Una canción escrita en código no tiene fichero y no se escribe.audio-listener: los oídos del juego en esta caja en vez de en la cámara.enabled, falta si está encendido.
mask
{ "type": "mask", "id": "m1", "by": "b7", "channel": "luminance", "soft": true }
{ "type": "mask", "id": "m2", "rect": { "x": 0, "y": 0, "width": 200, "height": 120 } }La caja, y todo lo que tiene debajo, solo se dibuja dentro de un rectángulo o de la forma de otra
caja (useMask). Se escribe en la caja que enmascara; una máscara que el juego puso a varias cajas
se escribe en cada una, con el mismo id. Una por caja.
rect: en el espacio de la propia caja, girado y escalado con ella.by: elidde la caja a cuya forma se ciñe, buscada cuando ya está construida toda la escena (puede venir después en el fichero). Gana arect, y esa caja no se dibuja salvo conshowShape. Una copia de una caja reutilizable que tiene su propia forma se ciñe a la de la copia.channel: falta si es'alpha';'luminance'para un recorte en blanco y negro.soft,invert,showShape: faltan si sonfalse.enabled: falta si está encendida.
filter
{ "type": "filter", "id": "f1", "effects": [{ "builtin": "outline", "uniforms": { "color": [1, 1, 1], "thickness": 1 } }, { "builtin": "glow", "enabled": false }] }
{ "type": "filter", "id": "f2", "effects": [{ "builtin": "crt" }], "rect": { "x": -48, "y": -36, "width": 96, "height": 72 } }Efectos que corren sobre lo que dibuja la caja y todo lo que tiene debajo (useFilter). Se escribe
en la caja que filtra; un filtro que el juego puso a varias cajas se escribe en cada una, con el
mismo id. Uno por caja, dentro de la máscara de la caja si tiene.
effects: en orden, cada uno escrito como una entrada de la cadenapostdel proyecto:builtincon la clave del efecto del motor, oeffectcon la ruta de un.shader, yuniforms,options,paletteolut,enabled(falta si está encendido),name. Una entrada que no nombra nada que pueda correr se descarta, y un filtro que se queda sin efectos se guarda tal como estaba escrito, sin leer. Un efecto escrito en código no tiene cómo escribirse y se deja fuera, con un aviso.rect: la parte de la pantalla en la que trabajan los efectos, en el espacio de la caja. Si falta, es lo que dibuja la caja, con margen alrededor.padding: ese margen en píxeles, cuando no es el que piden los efectos.enabled: falta si está encendido.
physics-body-2d, physics-body-3d
{ "type": "physics-body-2d", "id": "body", "name": "body", "body": "dynamic",
"collider": { "shape": "rect", "width": 16, "height": 24 },
"bounce": 0, "friction": 0.5, "density": 1, "sensor": false, "layer": 0, "collidesWith": 65535 }Un colisionador y un cuerpo por caja, un componente y no una hija: es algo que la caja es. Su sitio es el de la caja, así que mover la caja lo mueve.
body:static,dynamicokinematic(lo mueve el juego, empuja y bloquea, nunca lo empujan).collideren 2D:rect,circleopolygon. En 3D:box,sphere,capsule, ohull/meshnombrando un assetgeometry.physics-body-3dañade unoffsetrespecto a la caja.- La superficie:
bounce,friction,density,sensor(avisa de solapes, no bloquea nada),layer(una capa, de0a15: qué es esta cosa) ycollidesWith(una máscara de bits de las capas con las que choca; dos cuerpos chocan solo si la máscara de cada uno incluye la capa del otro).
El motor guarda, escribe y lee esto sin nada instalado para simularlo: una escena con física se
abre y se guarda sin perder nada, y una herramienta puede dibujar el contorno de un colisionador sin
un motor de física. Simular es cosa de nacatamalon/physics2d o nacatamalon/physics3d, que instala
el juego.
physics-world-2d, physics-world-3d
Los ajustes de la simulación de la escena: gravity. Viven en la raíz, porque la gravedad es de
la escena y la raíz es una caja como otra cualquiera; solo se lee la de la raíz. La plana mide en
píxeles con +y hacia abajo, y la 3D en metros con +y hacia arriba.
particle-collider-2d, particle-collider-3d
Algo sólido para las partículas, y para nada más. shape: en 2D rect, circle o plane; en
3D box, sphere o plane, medido sobre la caja que lo coloca. enabled solo se escribe cuando
está apagado. Qué hace un emisor al chocar con uno (rebotar, pararse, morir) está en su fichero
.particles.
6. Materiales
Un material es un gancho y sus mandos. Hay dos maneras de decir dónde está el gancho, y la primera es la que hay que usar:
"material": { "name": "crt", "effect": "crt",
"fragment": null, "fragmentGlsl": null, "vertex": null, "vertexGlsl": null,
"uniforms": { "curve": 0.3 } }effect nombra un asset .wgsl, y entonces los cuatro ganchos son null. El código en sí solo se
escribe para un material hecho en código con su shader escrito en la misma llamada, que no tiene
fichero que nombrar y se perdería. Una copia del código de un shader dentro de una escena está tan
mal como una copia del comportamiento compilado de un script.
maps son dibujos extra que su shader lee por nombre (sampleMap_<name>(uv)), hasta cuatro: cada
uno { "texture": "<clave de asset>" }, con wrap y smooth solo cuando no son los de por defecto.
Falta si no hay ninguno. El material de un sprite, de un texto y de una capa de mapa puede tenerlos,
igual que el de un modelo.
No hay campo de firma. Qué tipo es cada mando se deduce de los valores, así que escribirlo sería decir lo mismo dos veces, y dos versiones de un mismo dato pueden no coincidir.
Un material de modelo lleva además una superficie: texture (una clave de asset), tint,
emissive, specular, shininess, alpha, smooth, y wrap: qué hace el dibujo más allá de su
borde, 'repeat', 'clamp' o 'mirror', o { u, v } para cada dirección por separado. Repetir es
lo de por defecto y nunca se escribe; un modelo de un fichero hace lo que dice el sampler del fichero
salvo que wrap esté ahí.
emissiveTexture (una clave de asset) es el dibujo a través del que sale el brillo, multiplicado por
emissive: solo se escribe cuando hay uno. occlusion es dónde se aparta de los pliegues del modelo
la luz que viene de todas partes: { "texture": "<clave de asset>" }, con "strength" (de 0 a 1)
solo cuando es menos que todo, "uv": "uv2" solo cuando lo coloca la segunda coordenada de dibujo, y
"wrap" (como arriba) solo cuando tiene bordes propios, si no toma los del dibujo de color; solo se
escribe cuando hay uno. Una pieza de un fichero de modelo no escribe ninguno de estos dibujos
(texture es null; emissiveTexture y occlusion faltan): el fichero los trae de vuelta cuando
se abre la escena, y un dibujo guardado dentro de un .glb no tiene fichero propio que nombrar.
lighting es cómo se ilumina, escrito como el look:
{ "look": "toon", "bands": 2, "shadow": 0.4, "spot": 0.9 } o { "look": "unlit" }; steps es lo
que se le hace una vez iluminado, en orden (mangaDots, rim). Un material con looks omite su
fragment: los looks son la receta, y el shader se vuelve a hacer con ellos al abrir la escena. Sus
mandos, como estén puestos, van en uniforms.
vertexSnap pone las esquinas del modelo en una rejilla gruesa de la pantalla, el temblor de la
PlayStation: true para las filas del propio juego, o un número de filas (120 es el aspecto
clásico). Solo se escribe cuando está encendido. affine: true estira el dibujo recto por la
pantalla sin corregir la profundidad, las texturas que nadan de la PlayStation. Solo se escribe
cuando está encendido.
cull es la cara de sus triángulos que el material descarta: "front" (una forma vista desde
dentro, iluminada desde dentro) o "none" (se dibujan las dos caras, cada una iluminada como la cara
que es). Se escribe siempre que el material lo dice, "back" (el de por defecto) incluido, para que
una pieza de modelo a la que se le dijo "back" lo mantenga frente a un fichero que dibuja las dos
caras; cualquier otra palabra se lee como no dicha.
alphaCutoff recorta el dibujo en vez de mezclarlo: un píxel con alfa por debajo no se dibuja, el
resto es sólido (una valla, una hoja). Un número de 0 a 1, solo escrito cuando está puesto; cualquier
otra cosa se omite. Una pieza de modelo cuyo fichero la recorta (MASK) se recorta como dice el
fichero salvo que el documento diga otra cosa.
transparent dice si se ve a través sea cual sea su alfa: true para un dibujo con partes
transparentes o un shader que calcula la suya (cristal, agua, humo), false para sólido con
cualquier alfa. Se escribe siempre que el material lo dice, en un sentido o en otro; si falta, solo
se ve a través por debajo del alfa completo. Una pieza de modelo cuyo fichero la mezcla (BLEND) se
ve a través salvo que el documento diga false.
Un material de sprite no lleva nada de eso, y esa asimetría es a propósito. En el plano, el color y la hoja son del objeto: dos sprites de un personaje teñidos distinto son dos sprites, no dos materiales. En tres dimensiones son de la superficie, porque es lo que pregunta una luz.
7. Lo que no está en el formato
| Qué | Dónde está en su lugar |
|---|---|
| Tamaño de la ventana, fondo, MSAA, mapa de controles, cadena de post | project.json |
| Transiciones de escena | Un argumento de change, no algo de una escena |
| Esqueletos y clips de animación | El fichero de modelo del que salieron |
| Carreras de animación de sprites | El fichero .atlas; un sprite solo dice qué carrera y a qué velocidad |
| Los valores de un store | Su fichero .store y la partida guardada del jugador; una caja solo lo nombra |
| Paletas y tablas de color | Las nombra un efecto de post en project.json |
| Si una escena está pausada, debajo de otra o detrás de una transición | En ningún sitio, a propósito: es cómo llegó el juego hasta ahí (scene.pause(), launch, una transición), no lo que es la escena. Al leerla de nuevo, una escena empieza corriendo, tal como se hizo |
| Cajas nacidas después de construir la escena | En ningún sitio, a propósito: guardar un nivel tres segundos después de empezar a jugarlo no debe escribir en él cada bala que esté en el aire |
8. Un ejemplo completo
{
"format": "nacatamalon-scene",
"version": 1,
"name": "Level1",
"assets": [
{ "type": "texture", "key": "hero", "src": "/assets/hero.png" },
{ "type": "geometry", "key": "cube:1:1:1", "source": { "kind": "cube" } }
],
"root": {
"id": "level1-root",
"name": "Level1",
"transform": null,
"components": [
{ "type": "camera-2d", "id": "cam", "transform": { "x": 0, "y": 0, "rotation": 0 }, "zoom": 1 }
],
"children": [
{
"id": "player",
"name": "Player",
"transform": { "x": 40, "y": 120, "z": 0, "rotation": 0, "rotationX": 0, "rotationY": 0,
"scaleX": 1, "scaleY": 1, "scaleZ": 1 },
"components": [
{ "type": "sprite", "id": "player-art", "texture": "hero",
"tint": { "r": 1, "g": 1, "b": 1, "a": 1 }, "anchor": { "x": 0.5, "y": 1 } }
],
"children": []
}
]
}
}