Los paquetes compartidos
El editor, el runtime y tu propio código están construidos con los mismos paquetes, y varios de ellos están a tu disposición. Cuáles depende de dónde corra tu código.
| Paquete | Te da | En un script | En un plugin | En una extensión de componente |
|---|---|---|---|---|
@was/ecs | entities, componentes, el mundo | ✓ | ✓ | ✓ |
@was/engine | las clases de componente | ✓ | ✓ | ✓ |
@was/signals | el sistema de reactividad | — | ✓ | ✓ |
@was/svdt | esquemas y validación | ✓ | ✓ | — |
@was/utils | utilidades pequeñas | ✓ | ✓ | — |
@was/ui | la biblioteca de componentes del editor | — | ✓ | ✓ |
@was/icons | el juego de iconos del editor | — | ✓ | ✓ |
react | React 19 | — | ✓ | ✓ |
ctx.effectUn script no puede importar el paquete de signals directamente: ctx.effect es el mismo mecanismo
con la vida gestionada por ti, así que un efecto muere con su objeto en vez de fugarse.
@was/signals — el sistema de reactividad
Es aquello sobre lo que se construye toda la plataforma: valores que saben quién los lee, de modo que un cambio actualiza exactamente lo que dependía de él y nada más.
import { signal, computed, effect, batch, untracked } from '@was/signals';
const score = signal(0);
const doubled = computed(() => score.value * 2);
const stop = effect(() => {
render(score.value); // se vuelve a ejecutar solo cuando cambia score
});
batch(() => {
score.value += 1;
score.value += 1; // los efectos corren una vez, no dos
});
stop();
| Función | Hace |
|---|---|
signal(v) | un valor que se puede observar; se lee y escribe .value |
computed(fn) | un valor derivado, recalculado solo cuando cambia lo que lee |
effect(fn) | corre ahora, y de nuevo cuando cambia lo que leyó; devuelve un stop |
batch(fn) | agrupar escrituras para que los observadores corran una vez, al final |
untracked(fn) | leer sin convertirse en dependencia |
ref(obj) | hacer reactivo un objeto entero, hasta sus hojas |
snapshot(obj) | una copia llana, no reactiva |
raw(obj) | el objeto subyacente, sin seguimiento |
readonly(obj) | una vista que no se puede escribir |
Para paneles de React hay un compañero con useSignal, useComputed, useSignalEffect y
useLiveSignal, de modo que un componente se vuelve a renderizar desde una signal sin cableado
alguno.
Es lo que hace que el editor y el runtime se comporten como lo hacen: nada sondea, nada se rerenderiza por si acaso, y un panel se actualiza porque cambiaron los datos, no porque alguien se lo dijera. → Por qué este motor
@was/ui — los componentes del editor
Los paneles de plugins y las extensiones de componente se pueden construir con la misma biblioteca que usa el editor, así que parecen nativos en vez de una página web incrustada. Unos treinta componentes:
Accordion · Avatar · Button · Card · Checkbox · Chip · CloseButton
DimensionInput · Draggable · Dropdown · Flag · Icon · IconButton · Input
Menu · Modal · Outside · Panel · Popover · Portal · Render · Scroll
Search · Section · Segmented · Select · Skeleton · Switcher · Tabs
Toast · Toolbar · Tooltip
Más @was/icons para el juego de iconos.
Los paneles usan un conjunto fijo de clases utilitarias que se entrega con la biblioteca. Los
valores arbitrarios como text-[13px] no están; usa un style en línea para tamaños fuera de la
escala.
@was/ecs — el modelo del mundo
Entity, Component, Space. Un plugin lee y modifica una escena con exactamente la misma API
que usa el motor por dentro; no existe una «API de plugins» aparte y más débil.
Space — el mundo
| Llamada | Devuelve |
|---|---|
getEntity(id) | una entity, o nada |
hasEntity(id) | si existe |
getEntities() | todas |
queryEntities(A, B, …) | cada entity que lleve todos esos componentes |
makeQueryEntities(A, B, …) | la misma consulta, prearmada, para uso repetido |
createEntity(id?, components?) | una entity nueva, añadida al mundo |
addEntity(…e) · removeEntity(…e) | meter una o sacarla |
getSystem(S) · hasSystem(S) | llegar a un sistema |
execute() | ejecutar un fotograma |
queryEntitiesEstá respaldado por un índice, así que pedir «todo lo que tenga una luz y un transform» es barato.
getEntities() no es lo mismo: te entrega el mundo entero y te obliga a filtrar.
Entity — una cosa del mundo
| Llamada | Hace |
|---|---|
getComponent(Type) | un componente, o null |
getComponents(A, B) | varios a la vez, en ese orden |
hasComponent(A, B) | si los lleva todos |
addComponent(…c) | añadir; añadir un tipo que ya tiene se ignora |
removeComponent(…c) | quitar, por clase o por instancia |
component(fn) | ejecutar fn por cada componente, ahora y en el futuro |
clone() · clean() | copiarla, o dejarla pelada |
.id · .components | su id, y sus componentes indexados por tipo |
Component — los datos
| Llamada | Hace |
|---|---|
.x o get('x') | leer un campo; dentro de un efecto eso además lo suscribe |
$data | todo ello como objeto llano |
$rawData | el objeto guardado, sin suscribir — de solo lectura |
update({ … }) | la única forma de escribir |
updateAt(path, value) | escribir dentro de un componente grande sin revalidarlo entero |
version(field?) | un contador que avanza al cambiar algo, para depender de un campo sin leerlo |
reset(data?) | volver a los valores por defecto |
clone() | una copia |
version() es el truco detrás del rendimiento con listas grandesLeer un valor grande te suscribe a cada hoja que hay dentro. Depender en su lugar de su contador de versión significa que te enteras de que cambió sin vigilarlo entero, que es como un panel sobrevive a una lista de veinte mil elementos.
@was/engine — las clases de componente
Cada tipo de componente como clase: TransformComponent, MaterialComponent,
RigidBodyComponent y los demás. Importas la clase y se la pasas a getComponent,
hasComponent o query.
La lista completa con cada campo: referencia de componentes.
@was/svdt — esquemas
La capa de validación con la que se describen los componentes. Compilada en vez de interpretada, que es por lo que analizar no cuesta nada en el camino de la animación.
import { s, compile } from '@was/svdt';
const schema = s.object({ speed: s.f64(1), name: s.string('') });
const codec = compile(schema); // compila una vez, al declarar — nunca en cada llamada
const value = codec.parse(input);
Describir una forma
Números: s.f32 s.f64 s.i8 s.u8 s.i16 s.u16 s.i32 s.u32, cada uno con un valor por defecto.
Escalares: s.bool, s.string, s.color, s.literal, s.enum, s.unknown, s.ref.
Vectores: s.vec2, s.vec3, s.mat4.
Compuestos: s.object, s.variant, s.array, s.record, s.union, s.preprocess, s.lazy.
Encadenables sobre cualquiera de ellos: .default(v), .optional(), .nullable(), .min(n),
.max(n), .int().
Qué te da un códec compilado
| Llamada | Hace |
|---|---|
parse(input) | validar y normalizar, lanzando error con una entrada mala |
safeParse(input) | lo mismo, devolviendo éxito o error en vez de lanzar |
parseAt(path, v) | validar un campo sin tocar el resto |
equals(a, b) | comparación profunda, generada para esta forma |
diff(a, b) | qué cambió |
apply(target, p) | aplicar un diff |
invert(p) | invertir uno — la base de deshacer |
pack · unpack | hacia y desde una forma binaria compacta |
Y para inspeccionar un esquema en lugar de datos: introspect, keys, requiredKeys.
@was/utils
Cosas pequeñas que usa todo el mundo: pick, omit, assign, keys, values, entries,
capitalize, basename, extname, dispose (pegar funciones de limpieza entre sí) y las
utilidades de MIME que hay detrás de la validación de subidas.
Siguiente: Glosario