Zum Hauptinhalt springen

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​

  1. Öffnen Sie das Panel Plugins.
  2. Drücken Sie Plugin erstellen.
  3. 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.

TypeScript und JSX funktionieren direkt

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:

FeldWas es hinzufügt
componentseine Komponente für Objekte → Eigene Komponenten
runtimeCode für das veröffentlichte Projekt → Laufzeitcode
triggers, stepsEinträge im Panel Ereignisse → Trigger und Schritte
settingseinen Bereich in den Projekteinstellungen → Projekteinstellungen
localesÜbersetzungen → Plugin übersetzen
actionsEinträge im Rechtsklickmenü von Ebenen und Assets → Menüeinträge und Tastenkürzel
commandsBefehle, die andere Plugins und der Assistent aufrufen können → Befehle
skillsAnleitungen, 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 ist

init() 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:

GlobalWas es ist
editordie Editor-API
worlddie Szene in Echtzeit, als Entitäten und Komponenten; null, wenn Ihr Manifest scene:read nicht aufführt, und in einem Overlay immer null
tagsdie eigenen Markierungen Ihres Plugins an Objekten (siehe unten); null, wenn world es ist
metaIhr Manifest
initfü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.

Es gibt nur die eingebauten Styles

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.
Nutzen Sie für Timer die Serveruhr

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.

Das Overlay kann die Szene nicht lesen

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, description und 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-api oder mit editor.i18n() die Funktion t(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, eine kind (success, warning oder error) und eine duration haben.
  • confirm antwortet mit true oder false. 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() und confirm(): 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]);
  • set gleitet zu einer nahen Ansicht und springt zu einer weit entfernten. Mit animate: false springt die Kamera immer.
  • focus rückt ein oder mehrere Objekte ins Bild. Gibt es nichts zu zeigen, antwortet es mit false.
  • In 2D verschiebt und zoomt set nur.
  • 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, was fn ändert, zu einem einzigen Schritt, auch wenn fn asynchron 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.

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
});
  • targets ist entity für Zeilen in den Ebenen und resource fü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 shortcut enthält Mod, Alt oder beides, in der Reihenfolge Mod+Alt+Shift, dazu den Code der Taste, etwa KeyD. Er wirkt auf die aktuelle Auswahl. Die eigenen Tastenkürzel des Editors haben Vorrang, und Tasten, die der Browser für sich behält, etwa Mod+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 mit editor.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.
  • permissions führt auf, was ein aufrufendes Plugin haben muss. Fehlt die Angabe, braucht der Aufrufer alle Berechtigungen Ihres Plugins.
  • Mit destructive: true bestätigt die Person jeden Aufruf vorher, egal wer ihn auslöst.

Veröffentlichen​

Das Veröffentlichungsformular fragt nach:

FeldGrenze
Symbolaus einem beliebigen Bild erzeugt, auf ein Quadrat von 256×256 zugeschnitten
Name—
Tagsbis zu 12
Beschreibungbis zu 500 Zeichen
Screenshotsbis zu 4
ZugriffKostenlos, 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.

Bilder funktionieren erst nach dem Veröffentlichen

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.

Fordern Sie nur an, was Sie brauchen

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.