Primeros pasos
Construye una pantalla de tienda desde una carpeta vacía, conéctala a los datos del juego y cárgala en Unity. Unos veinte minutos, y no se instala nada en ningún motor hasta el último paso.
La pantalla de abajo es lo que vas a construir: una tienda de gremio, con un contador de oro que el juego mantiene al día y una lista de artículos cuyos botones de compra el juego escucha. Aquí la construyes entera, paso a paso, y el último paso la mete dentro de un proyecto de Unity en marcha.

Todo hasta el paso 6 corre en el navegador: sin motor, sin cuenta, sin clave. El resto de estas docs son la referencia; esta página es la que construye algo, y enlaza fuera cada vez que un tema tiene página propia.
Antes de empezar
- Node 22 o más nuevo, y pnpm
- Un proyecto de Unity (2022.3 LTS o más nuevo), solo si quieres terminar el paso 6
- Sin cuenta, sin clave, sin cuota
1 Crear el proyecto y arrancarlo
Un comando crea el proyecto, otro arranca el preview:
npx create-zabloo-app my-game-ui
cd my-game-ui
pnpm install
pnpm dev
npx ejecuta el generador de proyectos una vez sin instalarlo; a partir de ahí
el proyecto es tuyo y quien lo mueve es pnpm. Vale cualquier gestor de
paquetes — las docs dicen pnpm en todas partes para que solo haya una cosa
que copiar.
Abre http://localhost:5078. Estás mirando la
view main-menu que trae el
proyecto, dibujada por @zabloo/renderer-web. Ese es el mismo código de dibujo
que corre el SDK dentro del juego: mide el texto, calcula dónde va cada caja y
pinta el resultado él mismo. Ninguna parte del navegador está haciendo el
layout, y dentro de ese canvas no hay HTML — así que lo que ves es lo que el
juego va a dibujar.
Tres partes del preview se ganan el sueldo desde el primer minuto:
- El selector de views. Cada
.tsxdesrc/views/es una view, y el nombre del fichero es el id con el que el SDK la carga. - El panel de bindings. Descubre todos los paths bindeados del envelope y te da un campo tipado por cada path. Eres tú, haciendo de juego.
- La consola de actions. Todas las named actions que dispara la UI, tal y como las recibiría el juego.
El proyecto en sí son cuatro carpetas:
my-game-ui/
├── src/
│ ├── views/ un .tsx por view — el nombre del fichero es el id
│ ├── components/ tus componentes React (nunca llegan a la IR)
│ ├── assets/ imágenes; la exportación las incrusta en el envelope
│ └── theme.ts tokens, variantes y motion
├── zabloo.config.ts
└── package.json dev · dev:unity · build
Lo que tienes ahora: un proyecto que funciona, y un preview que lo dibuja con el mismo código que usará el juego.
2 Tu primera pantalla
Abre src/views/main-menu.tsx y sustitúyelo por algo lo bastante pequeño como
para que cada línea sea tuya:
import { Column, Text } from "@zabloo/react";
export default function MainMenu() {
return (
<Column layout={{ grow: 1, justify: "center", align: "center", gap: 16 }}>
<Text style={{ color: "#eceff4", fontSize: 28 }}>Guild shop</Text>
</Column>
);
}
Guarda. El preview vuelve a exportar y se recarga solo: eso es pnpm dev
vigilando.
Es JSX, y los elementos son nodos del formato. Debajo no hay DOM, ni tampoco widgets del motor.
Tu .tsx se ejecuta una vez, al escribirlo, y lo que produce son datos.
zabloo export ejecuta tus componentes en tu máquina y escribe un
envelope: un fichero JSON que
describe un árbol de nodos, sus estilos y las conexiones que declaran. El
juego nunca ejecuta React, nunca ejecuta JavaScript y nunca ve MainMenu.
Como el JSON es el final del camino, las costumbres de React que dependen de
una app en marcha no se trasladan. No hay useState y no se vuelve a renderizar
nada una vez el juego está corriendo. onClick recibe un nombre en vez de
una función, porque una función no se puede escribir en un fichero JSON. Y una
condición como gold > 0 && se responde una sola vez, al exportar, y ahí se
queda congelada. Todo lo que pasa antes de que el JSON exista —.map(), las
props, las funciones auxiliares, partir las cosas en componentes— funciona
exactamente como esperas.
Así que una UI de zabloo cambia a través de dos conexiones declaradas, y los dos pasos siguientes son esas dos conexiones.
Lo que tienes ahora: tu propia pantalla en el canvas, y la única regla sobre la que se apoya el resto del tutorial — tu código se ejecuta al compilar, y lo que se publica son datos.
3 Datos: bindings
Los datos son del juego. La UI solo declara dónde leerlos. Un
binding es un path dentro de esos
datos, como player.gold: la UI nunca guarda el número, lee lo que el juego
tenga ahí, y se remaqueta cuando ese número se mueve.
Añade un contador de oro: un <Text> con bind en vez de hijos.
<Row layout={{ justify: "space-between", align: "center" }}>
<Text style={{ color: "#eceff4", fontSize: 28 }}>Guild shop</Text>
<Text bind="player.gold" style={{ color: "#facc15", fontSize: 20 }} />
</Row>
Guarda y mira el panel de bindings del preview: player.gold ha aparecido
solo, con un campo numérico al lado. Escribe 1250. El texto se rellena y la
fila se remaqueta alrededor de su nuevo ancho.
bind en <Text> es un atajo. Las demás props bindeables usan la forma de
objeto — un valor literal o { bind: "path" }:
<Text visible={{ bind: "shop.thanked" }} style={{ color: "#4ade80" }}>
Thanks for your purchase
</Text>
Un path es una dirección separada por puntos dentro de los datos del juego,
donde un segmento numérico indexa un array: player.gold,
shop.items.3.name. Leer nunca lanza un error: un path que no lleva a ninguna
parte no dibuja nada en vez de romper el frame. El mismo canal tiene tres
puertas: el panel de bindings del preview, zabloo.setData() en la consola del
navegador, y SetData desde el juego en el paso 6.
Vale la pena interiorizar ahora dos límites, porque son deliberados:
- Sin expresiones. Ni aritmética, ni formateo, ni condicionales. Un valor se enseña tal cual; todo lo que haya que decidir lo decide el juego, que luego mueve un valor al que la UI está bindeada.
styleno es bindeable. Una barra que se pone roja cuando baja se hace con el juego moviendo un token, no con la UI calculando un color.
Lo que tienes ahora: una pantalla que lee números en vivo del juego. Ahora, la misma conexión en el otro sentido.
4 Actions, y una lista
Una named action es una cadena que ha elegido el juego. El envelope declara que la conexión existe; qué pasa no está nunca en el JSON — que es justo lo que permite sustituir la pantalla sin tocar la build.
El caso interesante no es un botón. Es un botón dentro de una lista guiada por
datos, donde el mismo "buy" tiene que decir qué fila se ha pulsado.
<List> emite su plantilla de item una sola vez y el SDK la instancia por
cada elemento del array al que está bindeada:
<List
items="shop.items"
as="it"
keyPath="id"
layout={{ gap: 8, align: "stretch" }}
empty={<Text style={{ color: "#8a8a93" }}>Nothing in stock yet</Text>}
>
{(it) => (
<Row layout={{ height: 56, padding: 8, gap: 12, align: "center" }}>
<Column layout={{ grow: 1, gap: 2 }}>
<Text bind={it("name")} style={{ color: "#eceff4", fontSize: 13 }} />
<Text bind={it("detail")} style={{ color: "#8a8a93", fontSize: 11 }} />
</Column>
<Text bind={it("price")} style={{ color: "#facc15", fontSize: 13 }} />
<Button variant="primary" onClick="buy" layout={{ width: 72, height: 32 }}>
<Text style={{ color: "#ffffff", fontSize: 13 }}>Buy</Text>
</Button>
</Row>
)}
</List>
Cuatro cosas de ese fragmento:
as="it"pone nombre al alias del item. Dentro de la plantilla,it("name")es el pathit.nameresuelto contra el elemento actual; un path que no cuelga de ningún alias sigue siendo absoluto, que es como una fila puede seguir bindeandoplayer.gold.keyPath="id"es la identidad estable del item. Mantiene con su item el estado de runtime de cada uno —el anillo de foco, una transición en curso— cuando el juego reordena el array. EskeyPathy nokeyporquekeyes de React.emptyes un slot, no una condición. La IR no tiene expresiones, así que “aquí todavía no hay nada” es un nodo que el SDK enseña cuando el array está vacío.- La plantilla es un único nodo, porque el primer hijo del primitivo es la plantilla.
Aquí tienes esa pantalla, corriendo. No es una foto de la UI: pulsa Ejecutar y el mismo renderer monta en tu navegador el mismo envelope que exporta este proyecto:

Alimenta la lista igual que hará el juego, desde la consola del navegador:
zabloo.setData("shop.items", [
{ id: "sword", name: "Iron sword", detail: "Damage 12", price: "120" },
{ id: "potion", name: "Healing potion", detail: "Restores 40 HP", price: "25" },
]);
Pulsa Buy en una fila y mira la consola de actions del preview:
buy → shop.items.0 (#0)
Ese sufijo es el action context: una action disparada desde dentro de un item repetido lleva el path absoluto del item, su índice, y su clave cuando la lista declara una. Como el path incrusta todos los índices que lo envuelven, las listas anidadas funcionan solo con el item más interno.
Lo que tienes ahora: la pantalla de tienda del principio de esta página, funcionando en los dos sentidos — el juego alimenta la lista, y la lista le dice al juego qué fila se ha pulsado.
5 Tema y variantes
La pantalla funciona y está llena de códigos hex. Su sitio es src/theme.ts,
que ya trae todos los tokens que esta pantalla necesita:
export const tokens = {
"color.primary": "#7c3aed",
"color.surface": "#0e1016",
"color.text": "#ffffff",
"color.muted": "#a1a1aa",
"color.gold": "#fcd34d",
"radius.md": 10,
"space.2": 8,
// Motion is a token like any other.
"motion.fast": 120,
};
Un token es un valor con nombre que
comparte toda la UI, y una referencia a token es cómo un estilo apunta a
uno: una cadena entre llaves, "{color.gold}" en lugar de "#fcd34d",
"{space.2}" en lugar de 8. Los estilos no hornean valores: el SDK resuelve
las referencias nodo a nodo al dibujar, contra el diccionario plano del
envelope, y por eso cambiar ese diccionario retematiza la UI entera sin
volver a emitir el árbol. Pon motion.fast a 0 y la UI deja de animar; no
cambia nada más.
Una variante es un conjunto de estilos con nombre y con sus propios estados de interacción:
export const variants: ThemeVariants = {
Button: {
primary: {
style: { background: "{color.primary}", radius: "{radius.md}" },
states: {
hover: { style: { background: "#8b5cf6" } },
pressed: { style: { background: "#6d28d9" } },
focused: { style: { borderWidth: 2, borderColor: "#8b5cf6" } },
disabled: { style: { opacity: 0.45 } },
},
},
},
};
Pásale el ratón por encima, llega a él con el tabulador — los estados son del SDK, indexados por tipo de nodo, y sin una línea de código del juego:

A diferencia de un token, una variante nunca llega a la IR:
@zabloo/react la resuelve al exportar, así que el Button emitido lleva el
estilo y los estados ya aplanados y la palabra primary no aparece por ninguna
parte del envelope. Las variantes son una comodidad al escribir; los tokens son
una indirección en runtime. Por eso también van indexadas por primitivo —
<Checkbox> y <Switch> miran los dos bajo Toggle, porque es en lo que se
convierten.
Lo que tienes ahora: la misma pantalla sin un solo código hex, y un tema que el juego puede cambiar entero sin que tú vuelvas a exportar nada.
6 Exportar y cargarlo en el juego
pnpm build
Sale un solo fichero, dist/zabloo.ir.json, y es todo el entregable:
{
"v": 1, // IR major version
"tokens": { "color.gold": "#fcd34d" }, // the flat dictionary
"views": { "main-menu": { "type": "Container" } },
"assets": { "logo.png": { "hash": "…", "data": "iVBOR…" } }
}
A partir de aquí, ese fichero es la UI. Publícalo dentro de la build o descárgalo en runtime — el camino de carga es el mismo. Un SDK rechaza una versión mayor que no implementa, y degrada cualquier cosa más nueva dentro de una que sí.
Unity
El SDK se distribuye como paquete UPM, com.zabloo.sdk. Añádelo a
Packages/manifest.json y luego, en la escena:
- Añade un
ZablooDocumenta un GameObject. Requiere unUIDocument, y lo añade él mismo. - Suelta
dist/zabloo.ir.jsonenAssets/y asigna elTextAssetimportado al campo Envelope del documento. - Pon View al id de la view que quieras —
main-menu.
El juego habla con la UI a través del documento, que es el handle estable: la view es desechable y se sustituye en cada recarga, mientras que las suscripciones sobreviven y los datos empujados se vuelven a aplicar.
using UnityEngine;
using Zabloo;
[RequireComponent(typeof(ZablooDocument))]
public sealed class ShopDriver : MonoBehaviour
{
[SerializeField] int _gold = 1250;
ZablooDocument _doc;
// Start, not OnEnable: it runs after ZablooDocument has built the view.
void Start()
{
_doc = GetComponent<ZablooDocument>();
_doc.OnAction += OnZablooAction;
_doc.SetData("player.gold", _gold);
}
void OnDestroy()
{
if (_doc != null) _doc.OnAction -= OnZablooAction;
}
void OnZablooAction(string action)
{
if (action != "buy") return;
_gold -= 100;
_doc.SetData("player.gold", _gold); // the bound Text re-lays out
_doc.SetData("shop.thanked", true); // `visible` reveals the row
}
}
SetData queda cacheado en el documento, así que un valor empujado antes de
que exista la view —o antes de que exista el nodo bindeado— se aplica en cuanto
exista.
El paquete todavía no está publicado en ningún registro: añádelo por ruta
local o por URL de git. Y OnAction es un Action<string> — entrega solo el
nombre de la action, así que el action context del paso 4 aún no está en
C#. Hasta que llegue, un juego que necesite saber qué fila se ha pulsado lee
la selección de su propio estado.
No hace falta reexportar y reimportar a mano mientras trabajas. Activa
Zabloo → Dev Mode en el editor de Unity y ejecuta pnpm dev:unity: cada
guardado sustituye en caliente la view que está corriendo en el editor, modo
Play incluido, por el mismo camino de carga que usa un
hot-update de producción.
Lo que tienes ahora: la pantalla terminada corriendo dentro del juego, movida por el estado del juego — y un fichero que puedes sustituir más adelante sin publicar una build nueva.