Wonderland Engine 1.0.0 JavaScript Migration
Wonderland Engine hat massive Verbesserungen erfahren, wie JavaScript-Code behandelt, interagiert und verteilt wird.
Dieser Blogbeitrag wird Dich durch diese Änderungen führen. Zusätzlich wird der Abschnitt Migrationen jede neue Funktion durchgehen, um Migrationsschritte für Dein Projekt vor Version 1.0.0 detailliert darzustellen.
Motivation
Bis jetzt hat sich der Standard-Bundler von Wonderland Engine auf das Zusammenfügen lokaler Skripte verlassen. Benutzer würden Skripte erstellen, und der Editor würde sie einsammeln und zu einer finalen Anwendung bündeln. Es war erforderlich, alle Drittanbieter-Bibliotheken vorab zu bündeln und in der Projektordnerstruktur zu platzieren.
Alternativ war es möglich, NPM-Projekte einzurichten, aber die Einrichtung war manuell, und Künstler im Team mussten NodeJS einrichten und Schritte zum Installieren von Abhängigkeiten und mehr befolgen.
Das neue System hat mehrere Vorteile:
- Unterstützung für NPM-Paketabhängigkeiten standardmäßig
- Weit bessere Vervollständigungsvorschläge von Deinem IDE
- Viel einfachere Integration mit fortgeschrittenen Tools wie TypeScript
- Kompatibilität mit anderen WebAssembly-Bibliotheken
- Mehrere Wonderland Engine-Instanzen pro Seite
- Automatisches Management Deines NPM-Projekts für Nicht-Entwicklungsteammitglieder.
Wir haben an einem neuen JavaScript-Ökosystem gearbeitet, um Dir zu helfen, nahtlos mit Deinen bevorzugten Tools zu arbeiten.
Editor-Komponenten
Wenn Du zuvor ein NPM-Nutzer warst, besteht die Möglichkeit, dass Du auf Folgendes gestoßen bist:

Ab Version 1.0.0 wählt der Editor keine Komponententypen mehr aus dem Anwendungsbundle aus. Neben der Behebung des obigen Fehlers listet der Editor mehr Komponenten auf als möglicherweise in der finalen Anwendung registriert sind. Dies wird es fortgeschrittenen Benutzern ermöglichen, in Zukunft komplexe Projekte mit streambaren Komponenten einzurichten.
Mit dieser Änderung erfordert der Editor nun:
- Eine Auflistung der Komponenten oder Ordner in
Views > Project Settings > JavaScript > sourcePaths
- Das Hinzufügen von dependencies in der root
package.json
Datei
Beispiel für eine package.json
mit einer Bibliothek, die Komponenten bereitstellt:
Der Editor ist nun in der Lage, Komponenten zu finden, indem er Deine package.json
liest, um Entwicklungszeit zu verkürzen und Teilbarkeit zu verbessern. Für weitere Informationen sieh Dir bitte das Writing JavaScript Libraries Tutorial an.
Bundling
Eine neue Einstellung erlaubt es, den Bundling-Prozess zu modifizieren:
Views > Project Settings > JavaScript > bundlingType

Lass uns einen Blick auf jede dieser Optionen werfen:
esbuild
Deine Skripte werden mit dem esbuild Bundler gebündelt.
Dies ist die Standard-Wahl. Es wird empfohlen, diese Einstellung beizubehalten, wann immer Du kannst, aus Leistungsgründen.
npm
Deine Skripte werden mit Deinem eigenen npm Skript gebündelt.
Beispiel für eine package.json
mit einem benutzerdefinierten build
Skript:
1{
2 "name": "MyWonderfulProject",
3 "version": "1.0.0",
4 "description": "Mein Wonderland-Projekt",
5 "type": "module",
6 "module": "js/index.js",
7 "scripts": {
8 "build": "esbuild ./js/index.js --bundle --format=esm --outfile=\"deploy/MyWonderfulProject-bundle.js\""
9 },
10 "devDependencies": {
11 "esbuild": "^0.15.18"
12 }
13}
Der npm Skriptname kann in den Editor-Einstellungen festgelegt werden:
Views > Project Settings > JavaScript > npmScript

Dieses Skript kann jeden Befehl ausführen, solange es Dein finales Anwendungsbundle generiert.
Du kannst Deinen bevorzugten Bundler verwenden, wie Webpack oder Rollup. Allerdings raten wir Benutzern, Tools wie esbuild zu verwenden, um die Iterationszeit zu verkürzen.
Eingangspunkt der Anwendung
Komponenten werden im Laufzeitbetrieb, d.h., beim Ausführen im Browser, anders registriert.
Für die Laufzeit kann der Editor den Einstiegspunkt Deiner Anwendung, d.h., eine index.js
Datei, automatisch verwalten.
Der Editor verwendet eine Vorlage, die ungefähr so aussieht:
1/* wle:auto-imports:start */
2/* wle:auto-imports:end */
3
4import {loadRuntime} from '@wonderlandengine/api';
5
6/* wle:auto-constants:start */
7/* wle:auto-constants:end */
8
9const engine = await loadRuntime(RuntimeBaseName, {
10 physx: WithPhysX,
11 loader: WithLoader,
12});
13
14// ...
15
16/* wle:auto-register:start */
17/* wle:auto-register:end */
18
19engine.scene.load(`${ProjectName}.bin`);
20
21/* wle:auto-benchmark:start */
22/* wle:auto-benchmark:end */
Diese Vorlage wird automatisch in neu erstellte und alte Projekte vor Version 1.0.0 kopiert.
Die Vorlage kommt mit den folgenden Tags:
wle:auto-imports
: Begrenzt den Bereich, in dem Importanweisungen geschrieben werden sollenwle:auto-register
: Begrenzt den Bereich, in dem Registrierungsanweisungen geschrieben werden sollenwle:auto-constants
: Begrenzt den Bereich, in dem der Editor Konstanten schreiben wird, z.B.ProjectName
: Name, der in der.wlp
-Datei des Projekts aufgeführt istWithPhysX
: Ein boolescher Wert, der aktiviert wird, wenn die Physik-Engine aktiviert istWithLoader
: Ein boolescher Wert, der aktiviert wird, wenn das Laufzeitladen von glTF unterstützt werden soll
Als Beispiel:
1/* wle:auto-imports:start */
2import {Forward} from './forward.js';
3/* wle:auto-imports:end */
4
5import {loadRuntime} from '@wonderlandengine/api';
6
7/* wle:auto-constants:start */
8const ProjectName = 'MyWonderland';
9const RuntimeBaseName = 'WonderlandRuntime';
10const WithPhysX = false;
11const WithLoader = false;
12/* wle:auto-constants:end */
13
14const engine = await loadRuntime(RuntimeBaseName, {
15 physx: WithPhysX,
16 loader: WithLoader
17});
18
19// ...
20
21/* wle:auto-register:start */
22engine.registerComponent(Forward);
23/* wle:auto-register:end */
24
25engine.scene.load(`${ProjectName}.bin`);
26
27// ...
Diese Indexdatei wird automatisch für ein Projekt mit einer einzigen Komponente namens Forward
generiert, die in js/forward.js
definiert ist. Es ist wichtig zu beachten, dass der Editor nur Komponenten importiert und registriert, die im Szene benutzt werden, d.h., an ein Objekt angehängt sind.
Wenn Deine Anwendung eine Komponente nur zur Laufzeit verwendet, musst Du entweder:
- Sie als Abhängigkeiten kennzeichnen. Weitere Informationen findest Du im Abschnitt Component Dependencies
- Sie manuell in Deiner
index.js
Datei importieren
Für einfache Anwendungen wird diese Vorlagendatei ausreichen und keine Änderungen notwendig sein. Für komplexere Anwendungsfälle kannst Du Deine eigene index.js
Datei erstellen und verwalten, indem Du die Tag-Kommentare entfernst.
Das manuelle Verwalten der Indexdatei ermöglicht es Dir, Anwendungen mit mehreren Einstiegspunkten zu erstellen und Komponenten zu registrieren, die der Editor nicht kennt.
JavaScript-Klassen
Wonderland Engine 1.0.0 kommt mit einer neuen Möglichkeit, Komponenten zu deklarieren: ES6 Klassen.
1import {Component, Property} from '@wonderlandengine/api';
2
3class Forward extends Component {
4 /* Registrierungsname der Komponente. */
5 static TypeName = 'forward';
6 /* Eigenschaften, die im Editor angezeigt werden. */
7 static Properties = {
8 speed: Property.float(1.5)
9 };
10
11 _forward = new Float32Array(3);
12
13 update(dt) {
14 this.object.getForward(this._forward);
15 this._forward[0] *= this.speed;
16 this._forward[1] *= this.speed;
17 this._forward[2] *= this.speed;
18 this.object.translate(this._forward);
19 }
20}
Es gibt ein paar Dinge zu beachten:
- Wir verwenden nicht mehr ein globales
WL
-Symbol, sondern nutzen die API von@wonderlandengine/api
- Wir erstellen eine Klasse, die von der API
Component
-Klasse erbt - Der Registrierungsname der Komponente ist jetzt eine statische Eigenschaft
- Die Eigenschaften werden in der Klasse festgelegt
JavaScript-Eigenschaften
Objektliteraleigenschaften wurden durch Funktoren ersetzt:
1import {Component, Property} from '@wonderlandengine/api';
2
3class MyComponent extends MyComponent {
4 /* Registrierungsname der Komponente. */
5 static TypeName = 'forward';
6 /* Eigenschaften, die im Editor angezeigt werden. */
7 static Properties = {
8 myFloat: Property.float(1.0),
9 myBool: Property.bool(true),
10 myEnum: Property.enum(['first', 'second'], 'second'),
11 myMesh: Property.mesh()
12 };
13}
Komponentenabhängigkeiten
Abhängigkeiten sind Komponenten, die automatisch registriert werden, wenn Deine Komponente registriert wird.
Lass uns eine Speed
-Komponente zum Forward
-Beispiel hinzufügen:
1import {Component, Type} from '@wonderlandengine/api';
2
3class Speed extends Component {
4 static TypeName = 'speed';
5 static Properties = {
6 value: Property.float(1.5)
7 };
8}
9
10class Forward extends Component {
11 static TypeName = 'forward';
12 static Dependencies = [Speed];
13
14 _forward = new Float32Array(3);
15
16 start() {
17 this._speed = this.object.addComponent(Speed);
18 }
19
20 update(dt) {
21 this.object.getForward(this._forward);
22 this._forward[0] *= this._speed.value;
23 this._forward[1] *= this._speed.value;
24 this._forward[2] *= this._speed.value;
25 this.object.translate(this._forward);
26 }
27}
Bei der Registrierung von Forward
wird Speed
automatisch registriert, da es als Abhängigkeit aufgelistet ist.
Dieses Verhalten wird durch den booleschen autoRegisterDependencies
auf dem WonderlandEngine
-Objekt verwaltet, das in index.js
erstellt wird.
Ereignisse
Die Wonderland Engine hat früher Ereignisse mittels Listener-Arrays behandelt, z.B.:
WL.onSceneLoaded
WL.onXRSessionStart
WL.scene.onPreRender
WL.scene.onPreRender
Die Engine kommt jetzt mit einer Emitter
-Klasse, um Interaktionen mit Ereignissen zu erleichtern:
Du kannst Deine Listener mit einem Identifikator verwalten:
Für mehr Informationen sieh Dir bitte die Emitter API-Dokumentation an.
Object
Das Object
wurde in dieser gesamten Überarbeitung nicht ausgelassen. Es hat Änderungen erfahren, die die API konsistenter und sicherer machen.
Exportname
Die Object
-Klasse wird nun als Object3D
exportiert. Diese Änderung wurde durchgeführt, um das Überschatten des JavaScript Object-Konstruktors zu verhindern.
Um die Migration zu erleichtern, wird Object
weiterhin exportiert, aber stelle sicher, dass Du jetzt
1import {Object3D} from '@wonderlandengine/api';
verwendest, um zukünftige Migrationen zu erleichtern.
Transformationen
Auch die Transformations-API wurde nicht verschont. Die Engine zieht nun die Verwendung von Gettern & Settern (Accessoren) für Transformationen zurück:
Diese Getter/Setter hatten einige Nachteile:
- Konsistenz: Entspricht nicht anderen Transformations-APIs
- Leistung: Bei jedem Aufruf wird
Float32Array
zugewiesen - Sicherheit: Speicheransichten könnten von anderen Komponenten verändert werden
- Potenzieller Fehler beim Speichern der
Float32Array
-Referenz für späteres Lesen
- Potenzieller Fehler beim Speichern der
Wenn Du auf die neue API gespannt bist, lies bitte den Abschnitt Object Transform.
JavaScript-Isolierung
Für Benutzer, die den internen Bundler verwenden, könnte es Code wie diesen gegeben haben:
component-a.js
component-b.js
Der obige Code wird Annahmen über die componentAGlobal
-Variable treffen. Er erwartet, dass component-a
zuerst registriert wird und im Bundle vorangestellt wird.
Das funktionierte, weil der interne Bundler von Wonderland Engine keine Isolierung durchführte.
Mit 1.0.0, egal ob Du esbuild oder npm verwendest, wird das nicht mehr funktionieren. Bundler werden nicht in der Lage sein, den Link zwischen componentAGlobal
in component-a
und dem in component-b
herzustellen.
Als Faustregel gilt: Betrachte jede Datei als isoliert, wenn Du einen Bundler verwendest.
Migrationen
Einige manuelle Migrationsschritte sind erforderlich, abhängig davon, ob Dein Projekt npm verwendete oder nicht.
Jede Sektion beschreibt die geeigneten Schritte basierend auf Deiner vorherigen Einrichtung.
Editor-Komponenten (#migration-editor-components)
Interner Bundler
Für Benutzer, die zuvor den internen Bundler nutzten, d.h., mit dem aktivierten useInternalBundler
Kontrollkästchen:
Views > Project Settings > JavaScript > useInternalBundler
Keine weiteren Schritte sind erforderlich.
Npm
https://www.npmjs.com/package/wle-js-upgrade
Für npm-Nutzer, Du musst sicherstellen, dass Deine eigenen Skripte in der sourcePaths
-Einstellung aufgelistet sind.
Wenn Du eine Bibliothek verwendest, stelle sicher, dass sie auf Wonderland Engine 1.0.0 migriert wurde, indem Du dem Writing JavaScript Libraries Tutorial folgst.
Falls eine Deiner Abhängigkeiten nicht auf dem neuesten Stand ist, kannst Du den lokalen Pfad zum node_modules
Ordner in den sourcePaths
Einstellungen hinzufügen. Beispiel:

Denke immer daran, dass das generierte Bundle über npm oder esbuild im Editor nicht mehr verwendet wird, um die Komponenten zu finden. Es wird nur verwendet, wenn Deine Anwendung läuft.
Bundling
Keine weiteren Schritte sind erforderlich. Das Projekt sollte automatisch migriert werden.
JavaScript-Klassen, Eigenschaften & Ereignisse
Dieser Abschnitt ist für alle Benutzer gleich, egal ob Du useInternalBundler
aktiviert hattest oder nicht.
Lass uns einen Blick auf ein wenig Code werfen, der den alten Weg gegenüber dem neuen Weg vergleicht:
Vor 1.0.0
1WL.registerComponent('player-height', {
2 height: {type: WL.Type.Float, default: 1.75}
3}, {
4 init: function() {
5 WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));
6 WL.onXRSessionEnd.push(this.onXRSessionEnd.bind(this));
7 },
8 start: function() {
9 this.object.resetTranslationRotation();
10 this.object.translate([0.0, this.height, 0.0]);
11 },
12 onXRSessionStart: function() {
13 if(!['local', 'viewer'].includes(WebXR.refSpace)) {
14 this.object.resetTranslationRotation();
15 }
16 },
17 onXRSessionEnd: function() {
18 if(!['local', 'viewer'].includes(WebXR.refSpace)) {
19 this.object.resetTranslationRotation();
20 this.object.translate([0.0, this.height, 0.0]);
21 }
22 }
23});
Nach 1.0.0
1/* Vergiss nicht, dass wir jetzt npm-Abhängigkeiten verwenden */
2import {Component, Property} from '@wonderlandengine/api';
3
4export class PlayerHeight extends Component {
5 static TypeName = 'player-height';
6 static Properties = {
7 height: Property.float(1.75)
8 };
9
10 init() {
11 /* Wonderland Engine 1.0.0 bewegt sich weg von einer globalen
12 * Instanz. Du kannst jetzt auf die aktuelle Engine-Instanz
13 * über `this.engine` zugreifen. */
14 this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
15 this.engine.onXRSessionEnd.add(this.onXRSessionEnd.bind(this));
16 }
17 start() {
18 this.object.resetTranslationRotation();
19 this.object.translate([0.0, this.height, 0.0]);
20 }
21 onXRSessionStart() {
22 if(!['local', 'viewer'].includes(WebXR.refSpace)) {
23 this.object.resetTranslationRotation();
24 }
25 }
26 onXRSessionEnd() {
27 if(!['local', 'viewer'].includes(WebXR.refSpace)) {
28 this.object.resetTranslationRotation();
29 this.object.translate([0.0, this.height, 0.0]);
30 }
31 }
32}
Diese beiden Beispiele sind äquivalent und führen zum gleichen Ergebnis.
Du wirst einen Unterschied auf Zeile 5 bemerken:
1WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));
gegenüber
1this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
Da wir nun auf npm-Abhängigkeiten und Standard-Bundling migriert sind, gibt es keinen Bedarf mehr für eine globale WL
-Variable.
Eine global exponierte Engine zu haben, kam mit zwei Einschränkungen:
- Schwieriger, Komponenten zu teilen
- Unmöglich, mehrere Engine-Instanzen auszuführen
Während der zweite Punkt kein häufiger Anwendungsfall ist, möchten wir keine Nutzer in Bezug auf Skalierbarkeit einschränken.
Object Transform
Die neue API basiert auf dem üblichen Muster, das in der gesamten Wonderland Engine verwendet wird:
Die translation, rotation, und scaling Komponenten folgen jetzt diesem Muster:
Ähnlich wie beim Rest der API führt die Verwendung eines leeren
out
-Parameters für Getters zur Erstellung des Ausgabe-Arrays. Beachte, dass es immer besser ist, Arrays wann immer möglich wiederzuverwenden (aus Leistungsgründen).
Neben der Möglichkeit, die Transformationen des Objekts im lokalen Raum zu lesen und zu schreiben, kannst Du auch direkt im Welt-Raum arbeiten:
Für mehr Informationen, sieh Dir bitte die Object3D API-Dokumentation an. (TODO: Link ersetzen)
JavaScript-Isolierung
Dieser Abschnitt ist für alle Benutzer gleich, egal ob Du useInternalBundler
aktiviert hattest oder nicht.
Es gibt mehrere Möglichkeiten, Daten zwischen Komponenten auszutauschen, und es liegt am Entwickler der Anwendung, die geeignetste auszuwählen.
Wir werden einige Beispiele geben, wie man Daten austauscht, die nicht auf globale Variablen angewiesen sind.
Zustand in Komponenten
Komponenten sind Datentüten, die über andere Komponenten zugänglich sind.
Du kannst also Komponenten erstellen, die den Zustand Deiner Anwendung halten. Ein Beispiel wäre, wenn Du ein Spiel mit drei Zuständen erstellst:
- Laufen
- Gewonnen
- Verloren
Du kannst eine Singleton-Komponente mit der folgenden Form erstellen:
game-state.js
Die GameState
-Komponente kann dann zu einem Manager-Objekt hinzugefügt werden. Dieses Objekt sollte dann von Komponenten referenziert werden, die den Zustand des Spiels ändern werden.
Lass uns eine Komponente erstellen, um den Zustand des Spiels zu ändern, wenn ein Spieler stirbt:
player-health.js
1import {Component, Type} from '@wonderlandengine/api';
2
3import {GameState} from './game-state.js';
4
5export class PlayerHealth extends Component {
6 static TypeName = 'player-health';
7 static Properties = {
8 manager: {type: Type.Object},
9 health: {type: Type.Float, default: 100.0}
10 };
11 update(dt) {
12 /* Der Spieler ist gestorben, den Zustand ändern. */
13 if(this.health <= 0.0) {
14 const gameState = this.manager.getComponent(GameState);
15 gameState.state = 2; // Zustand auf `lost` setzen.
16 }
17 }
18}
Dies ist eine Möglichkeit, um zu demonstrieren, wie man Globals in Deiner Anwendung vor-Version 1.0.0 ersetzt.
Exports
Es ist auch möglich, Variablen über import und export zu teilen. Denke jedoch daran, dass das Objekt im gesamten Bundle identisch sein wird.
Wir können das obige Beispiel mit Exports neu betrachten:
game-state.js
player-health.js
1import {Component, Type} from '@wonderlandengine/api';
2
3import {GameState} from './game-state.js';
4
5export class PlayerHealth extends Component {
6 static TypeName = 'player-health';
7 static Properties = {
8 manager: {type: Type.Object},
9 health: {type: Type.Float, default: 100.0}
10 };
11 update(dt) {
12 if(this.health <= 0.0) {
13 GameState.state = 'lost';
14 }
15 }
16}
Diese Lösung funktioniert, ist allerdings nicht narrensicher.
Lass uns einen Blick auf ein Beispiel werfen, wo dies nicht funktioniert. Lass uns annehmen, dass dieser Code in einer Bibliothek namens gamestate
ist.
- Deine Anwendung hängt von
gamestate
Version 1.0.0 ab - Deine Anwendung hängt von der Bibliothek
A
- Bibliothek
A
hängt vongamestate
Version 2.0.0 ab
Deine Anwendung wird mit zwei Kopien der gamestate
-Bibliothek enden, da beide in Bezug auf die Version nicht kompatibel sind.
Wenn Bibliothek A
das GameState
-Objekt aktualisiert, ändert sie tatsächlich ihre eigene Instanzen von diesem Export. Das passiert, weil beide Versionen nicht kompatibel sind, was Deinen Anwendungskontext dazu führt, zwei verschiedene Instanzen der Bibliothek zu bündeln.
Abschließend
Mit diesem Leitfaden bist Du jetzt bereit, Deine Projekte auf Wonderland Engine 1.0.0 zu migrieren!
Wenn Du auf Probleme stößt, wende Dich bitte an die Community auf dem Discord-Server.