Documentación · referencia

El formato de escena

Una escena escrita como JSON, campo por campo: la referencia para quien hace una herramienta que lee o escribe una.

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 JSON

O 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 dentro

Lo 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 */ }
}
CampoQué es
formatSiempre "nacatamalon-scene". Un fichero que no lo dice se rechaza, no se lee a medias.
versionLa versión del formato. Un número mayor se lee igualmente, con un aviso.
nameCómo se llama la escena y con qué se arranca.
assetsTodo lo que hay que descargar, por el nombre con que lo llama el árbol.
rootLa 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.

kindCampos (todos opcionales)
cubewidth, height, depth
planewidth, depth, widthSegments, depthSegments
circleradius, segments
uvSphereradius, segments, rings
icoSphereradius, subdivisions
cylinderradius, height, segments
coneradius, height, segments
torusradius, tube, radialSegments, tubularSegments
latheprofile (obligatorio: una lista de { "x", "y" }, de abajo arriba, al menos dos, x no menor que 0), segments
gltfsrc (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 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.

GrupoTipos
Se dibujansprite, nine-slice, text, shapes, mesh, tilemap, particles, particles-3d
Cómo se ve la escenacamera-2d, camera-3d, fog, light, sprite-texture
Dónde se dibujamask, filter
Comportamiento y estadoscript, store, motion
Sonidosound, music, song, audio-listener
Físicaphysics-body-2d, physics-body-3d, physics-world-2d, physics-world-3d
Partículas que chocanparticle-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 }
CampoNotas
textureUna clave de asset, o null.
atlas, frameUna hoja cortada en cuadrícula, y qué celda.
width, heightSi 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.
tintCuatro canales de 0 a 1.
anchorQué punto del sprite cae sobre su transformación, de 0 a 1 de su propio tamaño.
uvOffset, uvScaleQué 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.
pivotEl 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, tileOffsetEl 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, uniformsVer §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 } }
CampoNotas
textureUna clave de asset. Un panel sin dibujo se avisa y se deja fuera de la escena que se construye.
atlas, frameUna hoja cortada en cuadrícula, y qué celda.
uvOffset, uvScaleQué parte del dibujo, cuando no la corta un fichero de atlas: una celda de una hoja hecha en código se escribe así.
width, heightSiempre se escriben: un panel siempre se estira a un tamaño.
sliceLos cuatro bordes, en píxeles del dibujo (de su frame, con una hoja).
modeCó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, zIndexComo en un sprite.
material, uniformsVer §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" } }
] }
CampoNotas
shapesCada 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, strokeUn 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.
visibleEn una forma, solo se escribe como false.
transform, tint, parallax, visible, zIndexComo 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.

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.

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.

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.

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 postproject.json
Transiciones de escenaUn argumento de change, no algo de una escena
Esqueletos y clips de animaciónEl fichero de modelo del que salieron
Carreras de animación de spritesEl fichero .atlas; un sprite solo dice qué carrera y a qué velocidad
Los valores de un storeSu fichero .store y la partida guardada del jugador; una caja solo lo nombra
Paletas y tablas de colorLas nombra un efecto de post en project.json
Si una escena está pausada, debajo de otra o detrás de una transiciónEn 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 escenaEn 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": []
      }
    ]
  }
}