Migrazione a JavaScript di Wonderland Engine 1.0.0
Wonderland Engine ha subito notevoli miglioramenti nel modo in cui gestisce, interagisce e distribuisce il codice JavaScript.
Questo post sul blog ti guiderà attraverso questi cambiamenti. Inoltre, la sezione Migrazioni esaminerà ciascuna nuova caratteristica per dettagliare i passaggi di migrazione per il tuo progetto pre-1.0.0.
Motivazione
Fino ad ora, il bundler predefinito di Wonderland Engine si basava sulla concatenazione di script locali. Gli utenti creavano script e l’editor li rilevava e li raggruppava per creare l’applicazione finale. Era necessario pre-raggruppare le librerie di terze parti e posizionarle nella struttura di cartelle del progetto.
In alternativa, era possibile impostare progetti NPM, ma l’impostazione era manuale e gli artisti del team dovevano configurare NodeJS e seguire i passaggi per installare le dipendenze e altro.
Il nuovo sistema presenta numerosi vantaggi:
- Supporto per le dipendenze dei pacchetti NPM di default
- Suggerimenti di completamento molto migliori dal tuo IDE
- Integrazione molto più facile con strumenti avanzati, come TypeScript
- Compatibilità con altre librerie WebAssembly
- Istanze multiple di Wonderland Engine per pagina
- Gestione automatica del tuo progetto NPM per i membri del team non sviluppatori.
Abbiamo lavorato su un nuovo ecosistema JavaScript per aiutarti a lavorare senza problemi con i tuoi strumenti preferiti.
Componenti dell’Editor
Se eri precedentemente un utente NPM, potrebbe esserti capitato di incontrare questo:

A partire dalla versione 1.0.0, l’editor non rileva più i tipi di componenti dal bundle dell’applicazione. Oltre a correggere l’errore sopra, l’editor elenca più componenti di quelli che possono essere registrati nell’applicazione finale. Questo permetterà agli utenti avanzati di impostare progetti complessi con componenti streaming in futuro.
Con questo cambiamento, l’editor richiederà ora:
- Di elencare i componenti o le cartelle in
Views > Project Settings > JavaScript > sourcePaths
- Di aggiungere dipendenze nel file
package.json
principale
Esempio di package.json
con una libreria che espone componenti:
L’editor è ora in grado di trovare i componenti leggendo il tuo package.json
per velocizzare il tempo di sviluppo e migliorare la condivisibilità.
Per ulteriori informazioni, dai un’occhiata al tutorial Scrivere Librerie JavaScript.
Bundling
Una nuova impostazione permette di modificare il processo di bundling:
Views > Project Settings > JavaScript > bundlingType

Esaminiamo ciascuna di queste opzioni:
esbuild
I tuoi script saranno raggruppati usando il bundler esbuild.
Questa è la scelta predefinita. Ti consigliamo di mantenere questa impostazione ogni volta che puoi per motivi di prestazioni.
npm
I tuoi script saranno raggruppati usando il tuo script npm.
Esempio di package.json
con uno script build
personalizzato:
1{
2 "name": "MyWonderfulProject",
3 "version": "1.0.0",
4 "description": "Il mio progetto Wonderland",
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}
Il nome dello script npm può essere impostato nelle impostazioni dell’editor:
Views > Project Settings > JavaScript > npmScript

Questo script può eseguire qualsiasi comando, purché generi il bundle finale della tua applicazione.
Puoi usare il tuo bundler preferito, come Webpack o Rollup. Tuttavia, consigliamo agli utenti di utilizzare strumenti come esbuild per ridurre il tempo di iterazione.
Punto di Ingresso dell’Applicazione
I componenti sono registrati diversamente nel runtime, cioè quando si esegue nel browser.
Per il runtime, l’editor può gestire automaticamente il punto di ingresso della tua applicazione, cioè un file index.js
.
L’editor utilizza un template che approssimativamente assomiglia a questo:
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 */
Questo template viene copiato automaticamente in progetti nuovi e vecchi pre-1.0.0.
Il template viene fornito con i seguenti tag:
wle:auto-imports
: Delimita dove dovrebbero essere scritte le dichiarazioni di importazionewle:auto-register
: Delimita dove dovrebbero essere scritte le dichiarazioni di registrazionewle:auto-constants
: Delimita dove l’editor scriverà le costanti, ad esempio:ProjectName
: Nome elencato nel file.wlp
del progettoWithPhysX
: Un booleano abilitato se il motore fisico è abilitatoWithLoader
: Un booleano abilitato se il caricamento del runtime di glTF deve essere supportato
Come esempio:
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// ...
Questo file di indice viene generato automaticamente per un progetto con un singolo componente chiamato Forward
, definito in js/forward.js
.
È importante notare che l’editor importerà e registrerà solo i componenti che sono utilizzati nella scena, cioè collegati a un oggetto.
Se la tua applicazione utilizza un componente solo a runtime, avrai bisogno di:
- Segnarli come dipendenze. Maggiori informazioni nella sezione Dipendenze Componente
- Importarli manualmente nel tuo file
index.js
Per applicazioni semplici, questo file template sarà sufficiente e non sarà necessaria alcuna modifica. Per casi d’uso più complessi,
sei libero di creare e gestire il tuo file index.js
rimuovendo i commenti dei tag.
Gestire manualmente il file di indice ti permette di creare applicazioni con molteplici punti di ingresso e di registrare componenti che l’editor non conosce.
Classi JavaScript
Wonderland Engine 1.0.0 introduce un nuovo modo di dichiarare componenti: Classi ES6.
1import {Component, Property} from '@wonderlandengine/api';
2
3class Forward extends Component {
4 /* Nome di registrazione del componente. */
5 static TypeName = 'forward';
6 /* Proprietà esposte nell'editor. */
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}
Ci sono alcune cose da notare:
- Non usiamo più un simbolo globale
WL
, ma utilizziamo l’API da@wonderlandengine/api
- Creiamo una classe che eredita dalla classe
Component
dell’API - Il nome di registrazione del componente è ora una proprietà statica
- Le proprietà sono impostate sulla classe
Proprietà JavaScript
Le proprietà letterali dell’oggetto sono state sostituite da funzioni:
1import {Component, Property}from '@wonderlandengine/api';
2
3class MyComponent extends MyComponent {
4 /* Nome di registrazione del componente. */
5 static TypeName = 'forward';
6 /* Proprietà esposte nell'editor. */
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}
Dipendenze del Componente
Le dipendenze sono componenti che vengono automaticamente registrati quando il tuo componente è registrato.
Aggiungiamo un componente Speed
all’esempio Forward
:
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}
Alla registrazione di Forward
, Speed
verrà registrato automaticamente perché è elencato come dipendenza.
Questo comportamento è gestito dal booleano autoRegisterDependencies
sull’oggetto WonderlandEngine
creato in index.js
.
Eventi
Wonderland Engine gestiva gli eventi utilizzando array di listener, ad esempio:
WL.onSceneLoaded
WL.onXRSessionStart
WL.scene.onPreRender
WL.scene.onPreRender
Il motore ora include una classe Emitter
per facilitare le interazioni sugli eventi:
Puoi gestire i tuoi listener utilizzando un identificatore:
Per ulteriori informazioni, dai un’occhiata alla documentazione API di Emitter.
Oggetto
L’Oggetto non è stato escluso da questa rivisitazione. Ha subito cambiamenti per rendere l’API più coerente e sicura.
Nome di Esportazione
La classe Object
è ora esportata come Object3D
. Questo cambiamento è stato effettuato per evitare di oscurare il costruttore dell’Oggetto JavaScript Object constructor.
Per facilitare la migrazione, Object
sarà ancora esportato, ma assicurati di utilizzare
1import {Object3D}from '@wonderlandengine/api';
per facilitare le migrazioni future.
Trasformazioni
Anche l’API delle trasformazioni non è stata risparmiata. Il motore sta ora deprecando l’uso di getter e setter (accessori) per la trasformazione:
Questi getter / setter presentavano alcuni svantaggi:
- Coerenza: Non rispetta l’altra API di trasformazione
- Prestazioni: L’allocazione di
Float32Array
ad ogni chiamata - Sicurezza: Le viste di memoria potrebbero essere alterate da altri componenti
- Potenziale bug quando si conserva il riferimento
Float32Array
per letture successive
- Potenziale bug quando si conserva il riferimento
Se sei ansioso di vedere la nuova API, leggi la sezione Trasformazione Oggetto.
Isolamento JavaScript
Per gli utenti che utilizzano il bundler interno, potresti aver visto codice come:
component-a.js
component-b.js
Il codice sopra fa alcune ipotesi sulla variabile componentAGlobal
. Si aspetta che component-a
sia registrato per primo e preceduto nel bundle.
Questo funzionava perché il bundler interno di Wonderland Engine non eseguiva isolamento.
Con 1.0.0, sia che tu usi esbuild o npm, questo non funzionerà più. I bundler non saranno in grado di collegare il componentAGlobal
utilizzato in component-a
e quello utilizzato in component-b
;
Come regola generale: Pensa a ogni file come isolato quando usi un bundler.
Migrazioni
Alcuni passaggi di migrazione manuali sono richiesti a seconda che il tuo progetto utilizzasse npm o meno.
Ogni sezione descriverà i passaggi appropriati richiesti in base alla tua configurazione precedente.
Componenti dell’Editor (#migration-editor-components)
Bundler Interno
Per gli utenti che utilizzavano il bundler interno, cioè con la casella di controllo useInternalBundler
attivata:
Views > Project Settings > JavaScript > useInternalBundler
Nessun ulteriore passaggio è richiesto.
Npm
https://www.npmjs.com/package/wle-js-upgrade
Per gli utenti npm, sarà necessario assicurarsi che i propri script siano elencati nell’impostazione sourcePaths
.
Se stai usando una libreria, assicurati che sia stata migrata a Wonderland Engine 1.0.0 seguendo il tutorial Scrivere Librerie JavaScript.
Nel caso in cui una delle tue dipendenze non sia aggiornata, puoi aggiungere il percorso locale alla cartella node_modules
nelle impostazioni sourcePaths
. Esempio:

Tieni sempre presente che il bundle generato tramite npm o esbuild non sarà più utilizzato per trovare i componenti nell’editor. Sarà usato solo quando si esegue la tua applicazione.
Bundling
Nessun ulteriore passaggio è richiesto. Il progetto dovrebbe essere migrato automaticamente.
Classi, Proprietà, & Eventi JavaScript
Questa sezione è la stessa per tutti gli utenti, che tu avessi useInternalBundler
abilitato o meno.
Diamo un’occhiata a qualche codice confrontando il vecchio modo rispetto al nuovo modo:
Prima di 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});
Dopo 1.0.0
1/* Non dimenticare che ora utilizziamo dipendenze npm */
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 sta abbandonando l'istanza globale.
12 * Puoi ora accedere all'istanza corrente del motore
13 * tramite `this.engine`. */
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}
Questi due esempi sono equivalenti e porteranno allo stesso risultato.
Noterai una differenza nella riga 5:
1WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));
rispetto a
1this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
Poiché ora siamo migrati a dipendenze npm e bundling standard, non c’è più bisogno di una variabile globale WL
.
Avere un motore esposto globalmente comportava due limitazioni:
- Più difficile condividere componenti
- Impossibile avere più istanze di motore in esecuzione
Mentre il secondo punto non è un caso d’uso comune, non vogliamo limitare nessun utente in termini di scalabilità.
Trasformazione Oggetto
La nuova API si basa sul pattern usuale utilizzato in Wonderland Engine:
Le componenti translation, rotation, e scaling seguono ora questo pattern:
Come per il resto dell’API, utilizzare un parametro
out
vuoto per i getter porterà alla creazione dell’array di output. Nota che è sempre meglio riutilizzare gli array quando possibile (per motivi di prestazioni).
In aggiunta alla possibilità di leggere e scrivere le trasformazioni dello spazio locale dell’oggetto, puoi operare direttamente anche nello spazio mondiale:
Per ulteriori informazioni, dai un’occhiata alla documentazione API di Object3D. (TODO: Sostituisci Link)
Isolamento JavaScript
Questa sezione è la stessa per tutti gli utenti, che tu avessi useInternalBundler
abilitato o meno.
Esistono diversi modi per condividere dati tra componenti ed è compito dello sviluppatore dell’applicazione scegliere quello più appropriato.
Forniremo alcuni esempi su come condividere dati senza dipendere da variabili globali.
Stato nei Componenti
I componenti sono contenitori di dati che vengono accessi tramite altri componenti.
Puoi quindi creare componenti per conservare lo stato della tua applicazione. Ad esempio, se stai creando un gioco con tre stati:
- In corso
- Vinto
- Perso
Puoi creare un componente singleton con la seguente forma:
game-state.js
Il componente GameState
può quindi essere aggiunto a un oggetto manager. Questo oggetto dovrebbe quindi essere referenziato da componenti che altereranno lo stato del gioco.
Creiamo un componente per cambiare lo stato del gioco quando un giocatore muore:
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 /* Il giocatore è morto, cambia lo stato. */
13 if(this.health <= 0.0) {
14 const gameState = this.manager.getComponent(GameState);
15 gameState.state = 2; // Impostiamo lo stato a `lost`.
16 }
17 }
18}
Questo è un modo per dimostrare come sostituire le variabili globali nella tua applicazione pre-1.0.0.
Esportazioni
È anche possibile condividere variabili tramite import ed export. Tuttavia, ricorda che l’oggetto sarà identico nell’intero bundle.
Possiamo rivisitare l’esempio sopra con esportazioni:
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}
Questa soluzione funziona, ma non è a prova di errore.
Diamo un’occhiata a un esempio in cui ciò non funziona. Immaginiamo che questo codice sia in una libreria chiamata gamestate
.
- La tua applicazione dipende da
gamestate
versione 1.0.0 - La tua applicazione dipende dalla libreria
A
- La libreria
A
dipende dagamestate
versione 2.0.0
La tua applicazione finirà con due copie della libreria gamestate
, perché entrambe non sono compatibili in termini di versione.
Quando la libreria A
aggiorna l’oggetto GameState
, in realtà sta cambiando le sue proprie istanze di questa esportazione. Ciò accade perché entrambe le versioni non sono compatibili, facendo sì che la tua applicazione includa due istanze diverse della libreria.
Parola Finale
Con questa guida, sei ora pronto a migrare i tuoi progetti a Wonderland Engine 1.0.0!
Se incontri qualche problema, ti invitiamo a contattare la comunità sul server Discord.