Crear un plugin
Crea tu propio plugin de Studio: prepara la carpeta, escribe un panel, trabaja con la escena y publícalo.
Un plugin es una carpeta en tu ordenador. La editas en tu propio editor de código, Studio la ejecuta sin ningún paso de compilación y la publicas cuando está lista.
Para el código que se ejecuta en el proyecto publicado, consulta Código de ejecución.
Crea un plugin
- Abre el panel Plugins.
- Pulsa Create plugin.
- Elige una carpeta. Studio escribe en ella una plantilla inicial:
my-plugin/
meta.json el manifiesto
main.html el marcado de tu panel
main.js tu código; también sirven .ts, .tsx y .jsx
plugin.d.ts tipos generados para toda la API
tsconfig.json para que tu IDE encuentre esos tipos
.wasignore lo que no se sube al publicar
Para ver un cambio, guarda tus archivos y pulsa Reload. No hay nada que empaquetar ni instalar.
Puedes escribir archivos .ts, .tsx y .jsx directamente. El plugin.d.ts generado le da a
tu editor de código autocompletado para toda la API.
El manifiesto
meta.json describe tu plugin:
{
"id": "acme.shape-spawner",
"name": "Shape Spawner",
"version": "1.0.0",
"entry": "main.tsx",
"permissions": ["scene:read", "scene:write"],
"panels": [{ "id": "main", "title": "Shapes", "entry": "main.tsx" }],
"icon": "data:image/webp;base64,…"
}
El icon es una URL de datos, no la ruta de un archivo. Así, una carpeta local y un plugin
publicado muestran el mismo icono en todas partes: en la barra de herramientas, en la cabecera del
panel y en la tarjeta de la biblioteca. El formulario de publicación puede crearlo por ti a partir
de cualquier imagen.
El manifiesto puede añadir más cosas:
| Campo | Qué añade |
|---|---|
components | un componente para los objetos → Componentes personalizados |
runtime | código para el proyecto publicado → Código de ejecución |
triggers, steps | entradas en el panel de eventos → Disparadores y pasos |
settings | una sección en Project settings → Ajustes del proyecto |
locales | traducciones → Traduce tu plugin |
actions | elementos en el menú de clic derecho de las capas y los recursos → Elementos de menú y atajos |
commands | comandos que pueden llamar otros plugins y el asistente → Comandos |
skills | guías que el asistente lee en los proyectos que tienen el plugin; cada una es un archivo .md de la carpeta |
Tu panel
El panel más sencillo es un archivo HTML con un script:
<div id="root">Loading…</div>
<script type="module">
init(async () => {
// aquí editor, world y meta ya están listos
document.getElementById('root').textContent = `${meta.name} — ${world.getEntities().length} entities`;
});
</script>
También puedes prescindir del HTML y apuntar entry a un archivo .tsx. Studio monta su export
por defecto, y puedes construir el panel con los propios componentes del editor:
// main.tsx
import { useState } from 'react';
import { Button } from '@was/ui';
export default function Panel() {
const [clicks, setClicks] = useState(0);
return <Button onClick={() => setClicks(clicks + 1)}>{clicks}</Button>;
}
init() espera a que todo esté listoinit() se ejecuta cuando el editor y la escena están listos. No tienes que comprobar ninguna de
las dos cosas.
Qué puede usar tu código
Tienes disponibles estas globales:
| Global | Qué es |
|---|---|
editor | la API del editor |
world | la escena en directo, con entidades y componentes; null salvo que tu manifiesto incluya scene:read, y siempre null en una capa |
tags | las etiquetas propias de tu plugin en los objetos (consulta más abajo); null cuando world lo es |
meta | tu manifiesto |
init | ejecuta tu código cuando el editor y la escena están listos |
Puedes importar @was/ecs, @was/signals, @was/engine, @was/editor-api, @was/ui,
@was/icons, react y react-dom/client. Cualquier otro import falla con un error, así que te
das cuenta enseguida.
Los paneles hechos con @was/ui usan una hoja de estilos ya preparada, así que solo funcionan sus
clases. Los valores personalizados como text-[13px] no. Para un tamaño fuera de la escala, usa
un style en línea.
Trabaja con la escena
Puedes crear entidades a mano con world. Para escenas y espacios, es mejor pedírselo al editor:
obtienes los mismos valores por defecto, la misma numeración y las mismas comprobaciones que al
trabajar en Studio.
await editor.scenes.list();
await editor.scenes.create({ name: 'Chapter 2', trigger: { type: 'image', imageId } });
await editor.scenes.setTrigger(sceneId, { type: 'surface', bindingType: 'wall' });
await editor.scenes.delete(sceneId);
await editor.spaces.create({ name: 'Lobby' });
El trigger son los datos de un anchor. Se valida con el mismo esquema que usa el editor, así que
los tipos nuevos de disparador funcionan en tu plugin sin cambios en la API de plugins. En un
disparador de imagen, si no indicas un tamaño físico, se calcula a partir de la imagen.
Encuentra después tus propios objetos
Un plugin que genera escenas necesita volver a encontrarlas. Usa tags para eso:
tags.set(entity, { kind: 'scene', card: '2' });
tags.find({ kind: 'scene' });
tags.remove(entity, 'card');
Las etiquetas se guardan por separado para cada plugin, así que dos plugins nunca se pisan las etiquetas. El editor no las muestra ni las cambia.
La sala del proyecto
Hay estado que pertenece a la sesión actual, no al proyecto: un temporizador en marcha, una votación abierta, una mano levantada. No debe deshacerse, publicarse ni guardarse con el proyecto.
La sala del proyecto es un pequeño almacén compartido de claves y valores. Lo ve todo el que está en el proyecto en ese momento.
const { now, entries } = await editor.room.get();
await editor.room.set('timer', { running: true, endsAt: now + 60_000 });
editor.on('room.changed', (state) => render(state.entries.timer));
await editor.room.delete('timer');
- Sobrevive a una recarga de la página.
- Caduca 12 horas después de la última escritura.
- Admite hasta 64 claves. El nombre de una clave tiene hasta 128 caracteres, y un valor hasta 8 KB como JSON. Una escritura que supera un límite se ignora.
- Todo el que está en el proyecto puede leerla. Solo quien puede editar el proyecto puede escribir en ella.
Los relojes de dos ordenadores pueden diferir en minutos, así que now te da la hora del
servidor. Guarda la hora de fin a partir de now y deja que el editor de cada persona haga la
cuenta atrás por su cuenta. No guardes «segundos restantes»: el temporizador escribiría en la sala
cada segundo, para todos.
El servidor registra quién escribió cada clave, y un plugin no puede falsificarlo. Eso permite
votaciones limpias, pero tu plugin tiene que comprobarlo: el servidor deja que cualquiera que
pueda editar escriba cualquier clave. El ejemplo Session voting solo cuenta la clave
vote/<userId> si la escribió ese mismo usuario.
La capa sobre el visor
Un panel es privado y puedes cerrarlo. Para algo que deban ver todos, declara un panel con
"surface": "hud". Se dibuja como una pequeña capa encima de la escena:
editor.hud.set({ visible: true, width: 240, height: 96 });
editor.panels.open('main'); // una capa puede abrir su propio panel
La capa empieza oculta. Muéstrala solo cuando tenga algo que enseñar: incluso una capa vacía captura los clics destinados a la escena. El tamaño máximo es 640×400.
En una capa, world es null. Pon en la sala del proyecto, desde tu panel, todo lo que necesite
mostrar.
Traduce tu plugin
Guarda un diccionario por idioma en una carpeta e indica esa carpeta en el manifiesto:
"name": "${meta.name}",
"locales": "locale",
"panels": [{ "id": "main", "title": "${panel.title}", "entry": "main.tsx" }]
my-plugin/
locale/
en.json { "meta": { "name": "Shape Spawner" }, "panel": { "title": "Shapes" } }
de.json { "meta": { "name": "Formen-Werkzeug" }, "panel": { "title": "Formen" } }
name,descriptiony los títulos de los paneles y los componentes pueden ser plantillas${…}, que se leen del diccionario.- En un panel,
useEditorLocale()de@was/editor-api, oeditor.i18n(), te dat(key, vars)en el idioma del editor. Se actualiza cuando la persona cambia de idioma. - Si falta un idioma, se usa el inglés y, si tampoco está, cualquier idioma que tenga el plugin.
- Al publicar, la biblioteca muestra el nombre y la descripción en cada idioma.
Guarda ajustes
Un panel no tiene localStorage. Para recordar algo de la persona entre una visita y otra, usa
editor.storage:
await editor.storage.set('lastShape', 'sphere');
const shape = await editor.storage.get('lastShape');
editor.storage.subscribe((values) => render(values));
También tienes getAll(), delete(key) y clear(). Los valores se guardan como JSON, solo para
esa persona y ese plugin: hasta 256 KB por plugin y 1 MB entre todos los plugins de una persona.
Para valores que deba ver todo el que está en el proyecto, usa la
sala del proyecto.
Notificaciones y preguntas
editor.notify('Scene generated');
editor.notify({ message: 'Could not load the model', kind: 'error' });
const ok = await editor.confirm({ title: 'Delete 12 objects?', danger: true });
- Una notificación también puede llevar un
detail, unkind(success,warningoerror) y unaduration. confirmrespondetrueofalse. Un clic fuera cuenta como un no.- Las dos muestran el nombre de tu plugin. Puede haber hasta tres notificaciones en pantalla a la vez, y solo una pregunta esperando respuesta.
- Úsalas en lugar de
alert()yconfirm(): un panel no puede mostrarlos.
La cámara
const view = await editor.viewport.camera.get();
await editor.viewport.camera.set({ position: [0, 2, 5], target: [0, 0, 0] });
await editor.viewport.camera.focus([entityId]);
setse desliza hasta una vista cercana y salta directamente a una lejana. Pasaanimate: falsepara que salte siempre.focusencuadra uno o varios objetos. Respondefalsecuando no hay nada que mostrar.- En 2D,
setsolo desplaza la vista y hace zoom. - Necesita el permiso
editor:viewport.
Deshacer
Los cambios que tu plugin hace en la escena se pueden deshacer, igual que los de la propia persona. Los cambios separados por menos de 0,3 segundos cuentan como un solo paso.
editor.history.transaction(fn)convierte todo lo que cambiafnen un solo paso, aunquefnsea asíncrona.editor.history.skip(fn)deja los cambios fuera del historial. Úsalo para los datos que tu plugin calcula a partir de la escena. Si no, al deshacer se borran esos datos, tu plugin los vuelve a escribir y la persona nunca llega a recuperar su propia edición.
Elementos de menú y atajos
actions añade elementos al menú de clic derecho de las filas de Layers y de la lista de
recursos:
"actions": [
{ "id": "check", "title": "Check the scene", "panel": "main",
"targets": ["entity"], "shortcut": "Mod+Alt+KeyD" }
]
editor.actions.on('check', ({ target, ids }) => {
// lo que se pulsó con el botón derecho o lo que está seleccionado
});
targetsesentitypara las filas de capas yresourcepara las filas de recursos. Un plugin puede tener hasta 20 acciones.- Un clic abre tu
panelsi está cerrado y llama allí al manejador. - Un
shortcutllevaMod,Alto los dos, en el ordenMod+Alt+Shift, más el código de la tecla, comoKeyD. Actúa sobre la selección actual. Los atajos del propio editor tienen prioridad, y las teclas que se reserva el navegador, comoMod+KeyW, se rechazan.
Comandos
commands permite que otros plugins y el asistente llamen a tu
plugin:
"commands": [
{ "name": "diagnose", "description": "Lists the problems in the scene", "panel": "main" }
]
editor.commands.handle('diagnose', async (input, { caller }) => {
return { problems: await findProblems() };
});
- El comando se llama
plugin.<plugin id>.<name>. Otros plugins lo encuentran coneditor.commands.list(). Un plugin puede tener hasta 20 comandos, con nombres en camelCase. - Devuelve JSON o lanza un error. Comprueba tú mismo la entrada: nadie lo hace por ti.
- Si el panel está cerrado, primero se abre, pero solo en un plugin que la persona ya ha permitido.
permissionsindica lo que tiene que tener el plugin que llama. Si no lo indicas, quien llama necesita todos los permisos de tu plugin.- Con
destructive: true, la persona confirma antes cada llamada, la haga quien la haga.
Publica
El formulario de publicación te pide:
| Campo | Límite |
|---|---|
| Icon | se crea a partir de la imagen que elijas, recortada a un cuadrado de 256×256 |
| Name | — |
| Tags | hasta 12 |
| Description | hasta 500 caracteres |
| Screenshots | hasta 4 |
| Access | Free, Premium o Subscribers only: consulta Valoraciones y plugins de pago |
Si el plugin tiene archivos de idioma, el nombre y la descripción salen de ellos, en cada idioma.
Otros límites: hasta 64 archivos por plugin, 512 KB por archivo, 50 plugins por cuenta. Las imágenes y los demás archivos binarios tienen que pesar menos de unos 384 KB cada uno.
Si vuelves a publicar, se actualiza el mismo plugin. Para editar un plugin publicado, descárgalo: Studio escribe sus archivos en la carpeta que elijas.
Los archivos de un plugin publicado se sirven de forma normal, así que una ruta como
./icon.png funciona. Una carpeta local solo carga los archivos .ts, .tsx, .js, .jsx y
.json de su nivel superior, además de los archivos .html que tu manifiesto indica como
entradas. Las imágenes, los estilos y los archivos de subcarpetas solo aparecen después de
publicar.
Permisos
Indica en el manifiesto lo que necesita tu plugin: scene:read, scene:write,
resources:write, spaces:write, collaboration:read, collaboration:write, editor:panels y
otros. El servidor rechaza los permisos que no conoce.
Studio no restringe la mayoría de estos permisos: todos los plugins los reciben. Hay dos
excepciones. Un panel solo recibe world si el manifiesto incluye scene:read. Los datos del
proyecto (editor.data) solo responden a un plugin cuyo manifiesto incluya data:read o
data:write.
Los revisores leen tus permisos, y la gente juzga tu plugin por ellos. Pide el conjunto más pequeño que te sirva.
Cuando instales el plugin de otra persona, da por hecho que puede acceder a todo el proyecto. Instala solo lo que tengas motivos para considerar fiable. Consulta Confianza y verificación.
Siguiente: Componentes personalizados: añade tu propio componente a los objetos.