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:

Wonderland Engine 1.0.0 JavaScript Migration

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:

1{
2  "name": "my-wonderful-project",
3  "version": "1.0.0",
4  "description": "Mein Wonderland-Projekt",
5  "dependencies": {
6    "@wonderlandengine/components": "^1.0.0-rc.5"
7  }
8}

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

Wonderland Engine 1.0.0 JavaScript Migration

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

Wonderland Engine 1.0.0 JavaScript Migration

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 sollen
  • wle:auto-register: Begrenzt den Bereich, in dem Registrierungsanweisungen geschrieben werden sollen
  • wle: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 ist
    • WithPhysX: Ein boolescher Wert, der aktiviert wird, wenn die Physik-Engine aktiviert ist
    • WithLoader: 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:

1engine.onXRSessionStart.add((session, mode) => {
2    console.log(`Start session '${mode}'!`);
3})

Du kannst Deine Listener mit einem Identifikator verwalten:

1engine.onXRSessionStart.add((session, mode) => {
2    console.log(`Start session '${mode}'!`);
3}, {id: 'my-listener'});
4
5// Wenn Du fertig bist, kannst Du ihn einfach mit der `id` entfernen.
6engine.onXRSessionStart.remove('my-listener');

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:

1this.object.translationLocal;
2this.object.translationWorld;
3this.object.rotationLocal;
4this.object.rotationWorld;
5this.object.scalingLocal;
6this.object.scalingWorld;
7this.object.transformLocal;
8this.object.transformWorld;

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

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

1var componentAGlobal = {};
2
3WL.registerComponent('component-a', {}, {
4    init: function() {
5        comonentAGlobal.init = true;
6    },
7});

component-b.js

1WL.registerComponent('component-b', {}, {
2    init: function() {
3        if(componentAGlobal.init) {
4              console.log('Component A wurde vor B initialisiert!');
5        }
6    },
7});

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:

Wonderland Engine 1.0.0 JavaScript Migration

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:

1getValue(out) { ... }
2setValue(v) { ... }

Die translation, rotation, und scaling Komponenten folgen jetzt diesem Muster:

1const translation = this.object.getTranslationLocal();
2this.object.setTranslationLocal([1, 2, 3]);
3
4const rot = this.object.getRotationLocal();
5this.object.setRotationLocal([1, 0, 0, 1]);
6
7const scaling = this.object.getScalingLocal();
8this.object.setScalingLocal([2, 2, 2]);

Ä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:

1const translation = this.object.getTranslationWorld();
2this.object.setTranslationWorld([1, 2, 3]);
3
4const rot = this.object.getRotationWorld();
5this.object.setRotationWorld([1, 0, 0, 1]);
6
7const scaling = this.object.getScalingWorld();
8this.object.setScalingWorld([2, 2, 2]);

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

 1import {Component, Type} from '@wonderlandengine/api';
 2
 3export class GameState extends Component {
 4  static TypeName = 'game-state';
 5  static Properties = {
 6    state: {
 7      type: Type.Enum,
 8      values: ['running', 'won', 'lost'],
 9      default: 0
10    }
11  };
12}

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

1export const GameState = {
2  state: 'running'
3};

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 von gamestate 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.

Last Update: April 14, 2023

Bleiben Sie auf dem Laufenden.