Ein Plugin schreiben
Ein Plugin ist ein Ordner auf Ihrer Platte. Sie bearbeiten ihn in Ihrer eigenen IDE, er läuft im Editor ohne Build-Schritt, und Sie veröffentlichen ihn, wenn er fertig ist.
Anfangen
Drücken Sie im Panel Plugins auf Create. Studio fragt nach einem Ordner und schreibt eine Vorlage hinein:
my-plugin/
meta.json das Manifest
main.html das Markup Ihres Panels
main.js Ihr Code — .ts, .tsx und .jsx gehen auch
plugin.d.ts generierte Typen für die ganze API
tsconfig.json damit Ihre IDE diese Typen findet
.wasignore was beim Veröffentlichen nicht hochgeladen wird
Dateien bearbeiten, Reload from disk drücken — Ihre Änderungen sind live. Kein Bundler, kein Installationsschritt.
.ts, .tsx und .jsx werden beim Lesen kompiliertSie können direkt TypeScript und JSX schreiben. Die Typen stammen aus dem generierten
plugin.d.ts, Ihr Editor vervollständigt also die gesamte API.
Das Manifest
{
"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,…"
}
Das Symbol ist eine Data-URL statt eines Dateipfads, damit ein lokaler Ordner und ein veröffentlichtes Plugin überall gleich aussehen, wo das Plugin auftaucht — Werkzeugleiste, Panel-Kopfzeile, Bibliothekskarte. Das Veröffentlichungsformular erzeugt es aus jedem Bild für Sie.
Ihr Panel
Das einfachste Panel ist eine HTML-Datei und ein Skript:
<div id="root">Loading…</div>
<script type="module">
import { signal } from '@was/signals';
const clicks = signal(0);
init(async () => {
// editor, world und meta sind hier bereit
document.getElementById('root').textContent = `${meta.name} — ${world.getEntities().length} entities`;
});
</script>
Oder lassen Sie HTML ganz weg und zeigen Sie mit entry auf eine .tsx-Datei — die Runtime hängt
deren Default-Export ein, und Sie bauen das Panel aus der Komponentenbibliothek des Editors:
// 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() wartet, bis die Welt wirklich da istEs läuft, sobald die Verbindung zum Editor offen und die synchronisierte Kopie der Szene bereit ist — Sie müssen nie auf eines von beiden pollen.
Vier Globals stehen immer bereit: editor (die Editor-API), world (eine lebende,
synchronisierte Kopie der Szene als Entities und Komponenten), meta (Ihr Manifest) und
init.
Importieren dürfen Sie @was/ecs, @was/signals, @was/engine, @was/editor-api, @was/ui,
@was/icons, react und react-dom/client. Alles andere scheitert laut, statt still ins Leere zu
laufen.
Panels aus @was/ui nutzen ein vorgebautes Stylesheet, es existieren also nur die Klassen, die es
mitbringt. Beliebige Werte wie text-[13px] gehören nicht dazu — nehmen Sie ein Inline-style für
Größen außerhalb der Skala.
Mit der Szene arbeiten
Sie könnten Entities von Hand über world zusammensetzen, aber für Szenen und Spaces gibt es einen
besseren Weg: Fragen Sie den Editor, und Sie bekommen seine eigenen Voreinstellungen, Nummerierung
und Sicherungen:
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' });
Der trigger sind nur die Daten eines Ankers, geprüft vom selben Schema, das der Editor benutzt —
neue Auslösertypen funktionieren also, ohne dass sich das Plugin-Protokoll ändert. Ein
Bild-Auslöser errechnet seine physische Größe aus dem Bild selbst, wenn Sie keine angeben.
Die eigene Arbeit wiederfinden
Ein Plugin, das Szenen erzeugt, muss sie später wiederfinden. Dafür sind tags da — sie liegen
automatisch im Namensraum Ihres Plugins, sodass zwei Plugins einander nie überschreiben, und der
Editor zeigt sie weder an noch fasst er sie an:
tags.set(entity, { kind: 'scene', card: '2' });
tags.find({ kind: 'scene' });
tags.remove(entity, 'card');
Gemeinsamer Sitzungszustand
Mancher Zustand gehört zur Besprechung, nicht zum Dokument: ein laufender Timer, eine offene Abstimmung, eine erhobene Hand. Ihn rückgängig zu machen, zu veröffentlichen oder im Projekt zu speichern wäre jeweils falsch.
Der Projektraum ist ein kleiner gemeinsamer Schlüssel-Wert-Speicher, den alle sehen, die gerade im Projekt sind:
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');
Er überlebt ein Neuladen der Seite, läuft nach zwölf Stunden ab und fasst bis zu 64 Schlüssel zu je 8 KB.
now ist die Uhr des Servers, und genau darum geht esDie Rechner zweier Leute können um Minuten auseinanderliegen. Speichern Sie absolute Endzeiten von der Serveruhr und lassen Sie jeden Client selbst herunterzählen — speichern Sie nie „verbleibende Sekunden", sonst hieße ein laufender Timer, jede Sekunde für alle im Projekt in den Raum zu schreiben.
Der Server merkt sich auch, wer welchen Schlüssel geschrieben hat — das macht ehrliches
Abstimmen möglich: Eine Stimme zählt nur, wenn der Schlüssel vote/<userId> tatsächlich von diesem
Nutzer geschrieben wurde.
Das Viewport-Overlay
Ein Panel ist privat und lässt sich schließen — untauglich für etwas, das alle sehen sollen. Ein
Panel, das mit "surface": "hud" deklariert ist, wird stattdessen als kleines Overlay über der
Szene gezeichnet:
editor.hud.set({ visible: true, width: 240, height: 96 });
editor.panels.open('main'); // ein Overlay kann sein eigenes Panel herbeirufen
Es startet verborgen und zeigt sich, wenn es etwas zu zeigen hat — ein unsichtbarer transparenter Rahmen über der Szene würde Klicks verschlucken. Die Größe ist auf 640×400 begrenzt.
Es ist für alle und die ganze Zeit offen, deshalb ist es bewusst billig: Was es anzeigen muss, legt das Panel in den Raum.
Veröffentlichen
Das Veröffentlichungsformular nimmt ein Symbol, einen Namen, bis zu 12 Tags, eine Beschreibung von höchstens 500 Zeichen, bis zu 4 Screenshots und ein Kennzeichen „nur mit Abo".
Grenzen: 512 KB pro Datei, 64 Dateien, 50 Plugins pro Konto.
Erneutes Veröffentlichen aktualisiert denselben Eintrag. Sie können ein veröffentlichtes Plugin auch zum Bearbeiten herunterladen, was seine Dateien zurück in einen Ordner Ihrer Wahl schreibt.
Die Dateien eines veröffentlichten Plugins werden ordentlich ausgeliefert, also löst sich
./icon.png auf. Ein lokaler Entwicklungsordner inlined nur seine .ts, .js und .json —
Bilder, auf die per Pfad verwiesen wird, erscheinen erst nach dem Veröffentlichen.
Berechtigungen
Das Manifest listet, was Ihr Plugin braucht — scene:read, scene:write, resources:write,
spaces:write, collaboration:read, collaboration:write, editor:panels und weitere — und der
Server weist unbekannte zurück.
Berechtigungen sind das, was Prüfer lesen und woran Leute Ihr Plugin messen. Verlangen Sie den engsten Satz, der die Arbeit erledigt.
Und wenn Sie das Plugin einer anderen Person installieren, gehen Sie davon aus, dass es das ganze Projekt erreichen kann — installieren Sie nur, wofür Sie einen Grund zum Vertrauen haben. → Vertrauen und Prüfung
Weiter: Komponenten-Erweiterungen