Plugin schreiben
Bauen Sie Ihr eigenes Plugin für Studio: Ordner anlegen, ein Panel schreiben, mit der Szene arbeiten und das Plugin veröffentlichen.
Ein Plugin ist ein Ordner auf Ihrem Computer. Sie bearbeiten ihn in Ihrem eigenen Code-Editor, Studio führt ihn ohne Build-Schritt aus, und Sie veröffentlichen ihn, wenn er fertig ist.
Für Code, der im veröffentlichten Projekt läuft, siehe Laufzeitcode.
Plugin anlegen
- Öffnen Sie das Panel Plugins.
- Drücken Sie Plugin erstellen.
- Wählen Sie einen Ordner. Studio legt darin eine Startvorlage an:
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
Um eine Änderung zu sehen, speichern Sie Ihre Dateien und drücken Neu laden. Es gibt nichts zu bündeln oder zu installieren.
Sie können .ts-, .tsx- und .jsx-Dateien direkt schreiben. Die generierte plugin.d.ts gibt
Ihrem Code-Editor Autovervollständigung für die ganze API.
Das Manifest
meta.json beschreibt Ihr 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,…"
}
icon ist eine Data-URL, kein Dateipfad. So zeigen ein lokaler Ordner und ein veröffentlichtes
Plugin überall dasselbe Symbol: in der Werkzeugleiste, im Kopf des Panels und auf der Karte in der
Bibliothek. Das Veröffentlichungsformular erzeugt es für Sie aus einem beliebigen Bild.
Das Manifest kann noch mehr hinzufügen:
| Feld | Was es hinzufügt |
|---|---|
components | eine Komponente für Objekte → Eigene Komponenten |
runtime | Code für das veröffentlichte Projekt → Laufzeitcode |
triggers, steps | Einträge im Panel Ereignisse → Trigger und Schritte |
settings | einen Bereich in den Projekteinstellungen → Projekteinstellungen |
locales | Übersetzungen → Plugin übersetzen |
actions | Einträge im Rechtsklickmenü von Ebenen und Assets → Menüeinträge und Tastenkürzel |
commands | Befehle, die andere Plugins und der Assistent aufrufen können → Befehle |
skills | Anleitungen, die der Assistent in Projekten mit dem Plugin liest, jede als .md-Datei im Ordner |
Ihr Panel
Das einfachste Panel ist eine HTML-Datei mit einem Skript:
<div id="root">Loading…</div>
<script type="module">
init(async () => {
// editor, world und meta sind hier bereit
document.getElementById('root').textContent = `${meta.name} — ${world.getEntities().length} entities`;
});
</script>
Sie können das HTML auch weglassen und entry auf eine .tsx-Datei zeigen lassen. Studio bindet
deren Default-Export ein, und Sie können das Panel aus den Komponenten des Editors selbst bauen:
// 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 alles bereit istinit() läuft, sobald der Editor und die Szene bereit sind. Beides müssen Sie nicht selbst
prüfen.
Was Ihr Code nutzen kann
Diese globalen Variablen sind verfügbar:
| Global | Was es ist |
|---|---|
editor | die Editor-API |
world | die Szene in Echtzeit, als Entitäten und Komponenten; null, wenn Ihr Manifest scene:read nicht aufführt, und in einem Overlay immer null |
tags | die eigenen Markierungen Ihres Plugins an Objekten (siehe unten); null, wenn world es ist |
meta | Ihr Manifest |
init | führt Ihren Code aus, sobald Editor und Szene bereit sind |
Sie können @was/ecs, @was/signals, @was/engine, @was/editor-api, @was/ui, @was/icons,
react und react-dom/client importieren. Jeder andere Import schlägt mit einem Fehler fehl, sodass
Sie es sofort merken.
Panels, die mit @was/ui gebaut sind, nutzen ein fertiges Stylesheet, daher funktionieren nur
dessen Klassen. Eigene Werte wie text-[13px] funktionieren nicht. Für eine Größe außerhalb der
Skala verwenden Sie ein Inline-style.
Mit der Szene arbeiten
Entitäten können Sie über world von Hand bauen. Für Szenen und Räume fragen Sie besser den
Editor: So bekommen Sie dieselben Standardwerte, dieselbe Nummerierung und dieselben
Sicherheitsprüfungen wie bei der Arbeit in 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' });
Der trigger sind die Daten eines Ankers. Sie werden gegen dasselbe Schema geprüft, das der Editor
verwendet. Neue Trigger-Typen funktionieren daher in Ihrem Plugin, ohne dass sich die Plugin-API
ändern muss. Geben Sie bei einem Bild-Trigger keine physische Größe an, wird sie aus dem Bild
berechnet.
Eigene Objekte später wiederfinden
Ein Plugin, das Szenen erzeugt, muss sie wiederfinden können. Nutzen Sie dafür tags:
tags.set(entity, { kind: 'scene', card: '2' });
tags.find({ kind: 'scene' });
tags.remove(entity, 'card');
Tags werden für jedes Plugin getrennt gespeichert, zwei Plugins überschreiben sich also nie gegenseitig die Tags. Der Editor zeigt sie nicht an und ändert sie nicht.
Der Projekt-Room
Mancher Zustand gehört zur aktuellen Sitzung und nicht zum Projekt: ein laufender Timer, eine offene Abstimmung, eine gehobene Hand. Er soll weder rückgängig gemacht noch veröffentlicht oder mit dem Projekt gespeichert werden.
Der Projekt-Room ist ein kleiner gemeinsamer Speicher für Schlüssel und Werte. Alle, die gerade im Projekt sind, sehen ihn.
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 übersteht ein Neuladen der Seite.
- Er läuft 12 Stunden nach dem letzten Schreibvorgang ab.
- Er fasst bis zu 64 Schlüssel. Ein Schlüsselname hat bis zu 128 Zeichen, ein Wert als JSON bis zu 8 KB. Ein Schreibvorgang über einer Grenze wird ignoriert.
- Alle im Projekt können ihn lesen. Schreiben können nur Personen, die das Projekt bearbeiten dürfen.
Die Uhren zweier Computer können um Minuten abweichen, deshalb liefert now die Serverzeit.
Speichern Sie die Endzeit auf Basis von now, und lassen Sie den Editor jeder Person selbst
herunterzählen. Speichern Sie keine „verbleibenden Sekunden“: Dann würde der Timer jede Sekunde in
den Room schreiben, für alle.
Der Server merkt sich, wer welchen Schlüssel geschrieben hat, und ein Plugin kann das nicht
fälschen. Das macht ehrliche Abstimmungen möglich, aber Ihr Plugin muss es selbst prüfen: Der
Server lässt jeden, der bearbeiten darf, jeden beliebigen Schlüssel schreiben. Das Beispiel
Session voting zählt den Schlüssel vote/<userId> nur, wenn genau diese Person ihn geschrieben
hat.
Das Overlay im Viewport
Ein Panel sehen nur Sie, und Sie können es schließen. Für etwas, das alle sehen sollen, deklarieren
Sie ein Panel mit "surface": "hud". Es wird 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 öffnen
Das Overlay ist zu Beginn ausgeblendet. Zeigen Sie es nur, wenn es etwas anzuzeigen gibt: Auch ein leeres Overlay fängt Klicks ab, die für die Szene gedacht sind. Die maximale Größe ist 640×400.
In einem Overlay ist world gleich null. Legen Sie alles, was es anzeigen soll, von Ihrem Panel
aus in den Projekt-Room.
Plugin übersetzen
Legen Sie für jede Sprache ein Wörterbuch in einem gemeinsamen Ordner ab, und nennen Sie den Ordner im Manifest:
"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,descriptionund die Titel von Panels und Komponenten können${…}-Vorlagen sein, die aus dem Wörterbuch gelesen werden.- In einem Panel bekommen Sie mit
useEditorLocale()aus@was/editor-apioder miteditor.i18n()die Funktiont(key, vars)in der Sprache des Editors. Wechselt die Person die Sprache, aktualisiert sie sich. - Fehlt eine Sprache, wird Englisch verwendet und danach irgendeine Sprache, die das Plugin hat.
- Wenn Sie veröffentlichen, zeigt die Bibliothek Name und Beschreibung in jeder Sprache.
Einstellungen merken
Ein Panel hat kein localStorage. Um sich etwas für die Person bis zum nächsten Besuch zu merken,
nutzen Sie editor.storage:
await editor.storage.set('lastShape', 'sphere');
const shape = await editor.storage.get('lastShape');
editor.storage.subscribe((values) => render(values));
Außerdem gibt es getAll(), delete(key) und clear(). Die Werte werden als JSON gespeichert,
nur für diese Person und dieses Plugin: bis zu 256 KB pro Plugin und 1 MB für alle Plugins einer
Person zusammen. Für Werte, die alle im Projekt sehen sollen, nutzen Sie den
Projekt-Room.
Benachrichtigungen und Rückfragen
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 });
- Eine Benachrichtigung kann außerdem ein
detail, einekind(success,warningodererror) und einedurationhaben. confirmantwortet mittrueoderfalse. Ein Klick daneben zählt als Nein.- Beide zeigen den Namen Ihres Plugins. Bis zu drei Benachrichtigungen bleiben auf dem Bildschirm, und nur eine Frage kann gleichzeitig auf eine Antwort warten.
- Nutzen Sie sie statt
alert()undconfirm(): Diese kann ein Panel nicht anzeigen.
Die Kamera
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]);
setgleitet zu einer nahen Ansicht und springt zu einer weit entfernten. Mitanimate: falsespringt die Kamera immer.focusrückt ein oder mehrere Objekte ins Bild. Gibt es nichts zu zeigen, antwortet es mitfalse.- In 2D verschiebt und zoomt
setnur. - Dafür braucht das Plugin die Berechtigung
editor:viewport.
Rückgängig machen
Die Änderungen, die Ihr Plugin an der Szene macht, lassen sich rückgängig machen, genau wie die eigenen Änderungen der Person. Änderungen, die weniger als 0,3 Sekunden auseinanderliegen, werden zu einem Schritt zusammengefasst.
editor.history.transaction(fn)macht alles, wasfnändert, zu einem einzigen Schritt, auch wennfnasynchron ist.editor.history.skip(fn)hält Änderungen aus dem Verlauf heraus. Nutzen Sie das für Daten, die Ihr Plugin aus der Szene berechnet. Sonst entfernt ein Rückgängig diese Daten, Ihr Plugin schreibt sie erneut, und die Person kommt nie zu ihrer eigenen Änderung zurück.
Menüeinträge und Tastenkürzel
actions fügen dem Rechtsklickmenü der Zeilen in Ebenen und in der Asset-Liste Einträge hinzu:
"actions": [
{ "id": "check", "title": "Check the scene", "panel": "main",
"targets": ["entity"], "shortcut": "Mod+Alt+KeyD" }
]
editor.actions.on('check', ({ target, ids }) => {
// was per Rechtsklick angeklickt oder ausgewählt wurde
});
targetsistentityfür Zeilen in den Ebenen undresourcefür Zeilen in den Assets. Ein Plugin kann bis zu 20 Aktionen haben.- Ein Klick öffnet Ihr
panel, falls es geschlossen ist, und ruft dort den Handler auf. - Ein
shortcutenthältMod,Altoder beides, in der ReihenfolgeMod+Alt+Shift, dazu den Code der Taste, etwaKeyD. Er wirkt auf die aktuelle Auswahl. Die eigenen Tastenkürzel des Editors haben Vorrang, und Tasten, die der Browser für sich behält, etwaMod+KeyW, werden abgelehnt.
Befehle
Mit commands können andere Plugins und der Assistent Ihr Plugin
aufrufen:
"commands": [
{ "name": "diagnose", "description": "Lists the problems in the scene", "panel": "main" }
]
editor.commands.handle('diagnose', async (input, { caller }) => {
return { problems: await findProblems() };
});
- Der Befehl heißt
plugin.<plugin id>.<name>. Andere Plugins finden ihn miteditor.commands.list(). Ein Plugin kann bis zu 20 Befehle haben, benannt in camelCase. - Geben Sie JSON zurück oder werfen Sie einen Fehler. Prüfen Sie die Eingabe selbst: Das macht niemand für Sie.
- Ist das Panel geschlossen, öffnet es sich zuerst, aber nur bei einem Plugin, das die Person schon erlaubt hat.
permissionsführt auf, was ein aufrufendes Plugin haben muss. Fehlt die Angabe, braucht der Aufrufer alle Berechtigungen Ihres Plugins.- Mit
destructive: truebestätigt die Person jeden Aufruf vorher, egal wer ihn auslöst.
Veröffentlichen
Das Veröffentlichungsformular fragt nach:
| Feld | Grenze |
|---|---|
| Symbol | aus einem beliebigen Bild erzeugt, auf ein Quadrat von 256×256 zugeschnitten |
| Name | — |
| Tags | bis zu 12 |
| Beschreibung | bis zu 500 Zeichen |
| Screenshots | bis zu 4 |
| Zugriff | Kostenlos, Premium oder Nur für Abonnenten – siehe Bewertungen und kostenpflichtige Plugins |
Hat das Plugin Sprachdateien, kommen Name und Beschreibung aus ihnen, in jeder Sprache.
Weitere Grenzen: bis zu 64 Dateien pro Plugin, 512 KB pro Datei, 50 Plugins pro Konto. Bilder und andere Binärdateien müssen jeweils unter etwa 384 KB bleiben.
Erneutes Veröffentlichen aktualisiert dasselbe Plugin. Um ein veröffentlichtes Plugin zu bearbeiten, laden Sie es herunter: Studio schreibt seine Dateien in einen Ordner Ihrer Wahl.
Die Dateien eines veröffentlichten Plugins werden normal ausgeliefert, daher funktioniert ein Pfad
wie ./icon.png. Ein lokaler Ordner lädt nur die .ts-, .tsx-, .js-, .jsx- und
.json-Dateien auf seiner obersten Ebene, dazu die .html-Dateien, die Ihr Manifest als
Einstiegspunkte nennt. Bilder, Styles und Dateien in Unterordnern erscheinen erst nach dem
Veröffentlichen.
Berechtigungen
Tragen Sie im Manifest ein, was Ihr Plugin braucht: scene:read, scene:write, resources:write,
spaces:write, collaboration:read, collaboration:write, editor:panels und weitere. Der Server
lehnt Berechtigungen ab, die er nicht kennt.
Studio beschränkt ein Plugin bei den meisten dieser Berechtigungen nicht: Jedes Plugin bekommt sie.
Es gibt zwei Ausnahmen. Ein Panel bekommt world nur, wenn das Manifest scene:read aufführt.
Projektdaten (editor.data) antworten nur einem Plugin, dessen Manifest data:read oder
data:write aufführt.
Prüfer lesen Ihre Berechtigungen, und Nutzer beurteilen Ihr Plugin danach. Fordern Sie die kleinste Auswahl an, mit der die Aufgabe erledigt ist.
Wenn Sie das Plugin eines anderen installieren, gehen Sie davon aus, dass es auf das ganze Projekt zugreifen kann. Installieren Sie nur, wem Sie aus gutem Grund vertrauen. Siehe Vertrauen und Prüfung.
Weiter: Eigene Komponenten – fügen Sie Objekten Ihre eigene Komponente hinzu.