Die gemeinsamen Pakete
Editor, Runtime und Ihr eigener Code sind aus denselben Paketen gebaut, und mehrere davon stehen Ihnen offen. Welche, hängt davon ab, wo Ihr Code läuft.
| Paket | Gibt Ihnen | Im Skript | Im Plugin | In der Komponenten-Erweiterung |
|---|---|---|---|---|
@was/ecs | Entities, Komponenten, die Welt | ✓ | ✓ | ✓ |
@was/engine | die Komponentenklassen | ✓ | ✓ | ✓ |
@was/signals | das Reaktivitätssystem | — | ✓ | ✓ |
@was/svdt | Schemata und Validierung | ✓ | ✓ | — |
@was/utils | kleine Helfer | ✓ | ✓ | — |
@was/ui | die Komponentenbibliothek des Editors | — | ✓ | ✓ |
@was/icons | den Symbolsatz des Editors | — | ✓ | ✓ |
react | React 19 | — | ✓ | ✓ |
ctx.effectEin Skript kann das Signals-Paket nicht direkt importieren — ctx.effect ist derselbe Mechanismus,
mit der Lebensdauer für Sie verwaltet: Ein Effekt stirbt mit seinem Objekt, statt zu lecken.
@was/signals — das Reaktivitätssystem
Darauf ist die ganze Plattform gebaut: Werte, die wissen, wer sie liest, sodass eine Änderung genau das aktualisiert, was von ihr abhing, und sonst nichts.
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); // läuft nur erneut, wenn score sich ändert
});
batch(() => {
score.value += 1;
score.value += 1; // Effekte laufen einmal, nicht zweimal
});
stop();
| Funktion | Tut |
|---|---|
signal(v) | ein beobachtbarer Wert; .value wird gelesen und geschrieben |
computed(fn) | ein abgeleiteter Wert, nur neu berechnet, wenn sich Gelesenes ändert |
effect(fn) | läuft jetzt und wieder, wann immer sich Gelesenes ändert; gibt einen Stopper zurück |
batch(fn) | Schreibvorgänge gruppieren, damit Beobachter am Ende einmal laufen |
untracked(fn) | lesen, ohne zur Abhängigkeit zu werden |
ref(obj) | ein ganzes Objekt reaktiv machen, bis in seine Blätter |
snapshot(obj) | eine schlichte, nicht reaktive Kopie |
raw(obj) | das zugrunde liegende Objekt, ohne Verfolgung |
readonly(obj) | eine Sicht, in die nicht geschrieben werden kann |
Für React-Panels gibt es einen Begleiter mit useSignal, useComputed, useSignalEffect und
useLiveSignal, sodass eine Komponente ganz ohne Verdrahtung aus einem Signal neu rendert.
Das ist es, was Editor und Runtime so verhalten lässt, wie sie es tun: Nichts pollt, nichts rendert vorsorglich neu, und ein Panel aktualisiert sich, weil sich die Daten geändert haben, nicht weil jemand es ihm gesagt hat. → Warum diese Engine
@was/ui — die Komponenten des Editors
Plugin-Panels und Komponenten-Erweiterungen lassen sich aus derselben Bibliothek bauen, die der Editor benutzt, sie wirken also nativ statt wie eine eingebettete Webseite. Gut dreißig Komponenten:
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
Dazu @was/icons für den Symbolsatz.
Panels benutzen einen festen Satz Utility-Klassen, der mit der Bibliothek ausgeliefert wird.
Beliebige Werte wie text-[13px] sind nicht dabei — nehmen Sie ein Inline-style für Größen
außerhalb der Skala.
@was/ecs — das Weltmodell
Entity, Component, Space. Ein Plugin liest und ändert eine Szene mit genau derselben API, die
die Engine intern benutzt; es gibt keine getrennte, schwächere „Plugin-API".
Space — die Welt
| Aufruf | Gibt zurück |
|---|---|
getEntity(id) | eine Entity, oder nichts |
hasEntity(id) | ob sie existiert |
getEntities() | alle |
queryEntities(A, B, …) | jede Entity, die all diese Komponenten trägt |
makeQueryEntities(A, B, …) | dieselbe Abfrage, vorgebaut, zur wiederholten Nutzung |
createEntity(id?, components?) | eine neue Entity, der Welt hinzugefügt |
addEntity(…e) · removeEntity(…e) | eine hineinlegen oder herausnehmen |
getSystem(S) · hasSystem(S) | an ein System herankommen |
execute() | ein Bild ausführen |
queryEntities ist das, wonach Sie greifen solltenDahinter steckt ein Index, „alles mit einem Licht und einem Transform" zu erfragen ist also billig.
getEntities() ist nicht dasselbe — es reicht Ihnen die ganze Welt und lässt Sie filtern.
Entity — ein Ding in der Welt
| Aufruf | Tut |
|---|---|
getComponent(Type) | eine Komponente, oder null |
getComponents(A, B) | mehrere auf einmal, in dieser Reihenfolge |
hasComponent(A, B) | ob sie alle davon trägt |
addComponent(…c) | hinzufügen; einen bereits vorhandenen Typ zu ergänzen wird ignoriert |
removeComponent(…c) | entfernen, per Klasse oder Instanz |
component(fn) | fn für jede Komponente ausführen, jetzt und künftig |
clone() · clean() | kopieren oder bis aufs Gerippe abräumen |
.id · .components | ihre Id und ihre Komponenten nach Typ |
Component — die Daten
| Aufruf | Tut |
|---|---|
.x oder get('x') | ein Feld lesen; in einem Effekt abonniert das zugleich |
$data | das Ganze als schlichtes Objekt |
$rawData | das gespeicherte Objekt, ohne Abo — schreibgeschützt |
update({ … }) | der einzige Weg zu schreiben |
updateAt(path, value) | tief in eine große Komponente schreiben, ohne alles neu zu validieren |
version(field?) | ein Zähler, der bei Änderungen tickt, damit Sie von einem Feld abhängen können, ohne es zu lesen |
reset(data?) | zurück auf die Voreinstellungen |
clone() | eine Kopie |
version() ist der Trick hinter der Leistung bei großen ListenEinen großen Wert zu lesen abonniert Sie auf jedes Blatt darin. Stattdessen von seinem Versionszähler abzuhängen heißt, Sie erfahren, dass er sich geändert hat, ohne alles zu beobachten — so übersteht ein Panel eine Liste mit zwanzigtausend Einträgen.
@was/engine — die Komponentenklassen
Jeder Komponententyp als Klasse: TransformComponent, MaterialComponent, RigidBodyComponent
und die übrigen. Sie importieren die Klasse und reichen sie an getComponent, hasComponent oder
query weiter.
Die vollständige Liste mit jedem Feld: Komponenten-Referenz.
@was/svdt — Schemata
Die Validierungsschicht, mit der Komponenten beschrieben werden. Kompiliert statt interpretiert — deshalb kostet das Parsen auf dem Animationspfad nichts.
import { s, compile } from '@was/svdt';
const schema = s.object({ speed: s.f64(1), name: s.string('') });
const codec = compile(schema); // einmal bei der Deklaration kompilieren — nie pro Aufruf
const value = codec.parse(input);
Eine Form beschreiben
Zahlen: s.f32 s.f64 s.i8 s.u8 s.i16 s.u16 s.i32 s.u32, jede mit einer Voreinstellung.
Skalare: s.bool, s.string, s.color, s.literal, s.enum, s.unknown, s.ref.
Vektoren: s.vec2, s.vec3, s.mat4.
Zusammengesetzt: s.object, s.variant, s.array, s.record, s.union, s.preprocess,
s.lazy.
An jedes davon anhängbar: .default(v), .optional(), .nullable(), .min(n), .max(n),
.int().
Was ein kompilierter Codec liefert
| Aufruf | Tut |
|---|---|
parse(input) | validieren und normalisieren, bei schlechter Eingabe werfen |
safeParse(input) | dasselbe, gibt Erfolg oder Fehler zurück, statt zu werfen |
parseAt(path, v) | ein Feld validieren, ohne den Rest anzufassen |
equals(a, b) | Tiefenvergleich, für diese Form erzeugt |
diff(a, b) | was sich geändert hat |
apply(target, p) | ein Diff anwenden |
invert(p) | eines umkehren — die Grundlage des Rückgängigmachens |
pack · unpack | in eine kompakte Binärform und zurück |
Und um ein Schema statt Daten zu untersuchen: introspect, keys, requiredKeys.
@was/utils
Kleinigkeiten, die überall gebraucht werden: pick, omit, assign, keys, values,
entries, capitalize, basename, extname, dispose (Aufräumfunktionen zusammenkleben) und
die MIME-Helfer hinter der Upload-Prüfung.
Weiter: Glossar