Valorizziamo la tua privacy. Usiamo i cookie per migliorare la tua esperienza sul nostro sito. Utilizzando questo sito accetti la nostra Informativa sulla privacy.

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:

Migrazione a JavaScript di Wonderland Engine 1.0.0

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:

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

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

Migrazione a JavaScript di Wonderland Engine 1.0.0

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

Migrazione a JavaScript di Wonderland Engine 1.0.0

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 importazione
  • wle:auto-register: Delimita dove dovrebbero essere scritte le dichiarazioni di registrazione
  • wle:auto-constants: Delimita dove l’editor scriverà le costanti, ad esempio:
    • ProjectName: Nome elencato nel file .wlp del progetto
    • WithPhysX: Un booleano abilitato se il motore fisico è abilitato
    • WithLoader: 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:

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

Puoi gestire i tuoi listener utilizzando un identificatore:

1engine.onXRSessionStart.add((session, mode) => {
2    console.log(`Inizia sessione '${mode}'!`);
3}, {id: 'my-listener'});
4
5// Quando finito, puoi semplicemente rimuoverlo con l'id.
6engine.onXRSessionStart.remove('my-listener');

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:

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

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

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

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

component-b.js

1WL.registerComponent('component-b', {}, {
2    init: function() {
3        if(componentAGlobal.init) {
4              console.log('Il Componente A è stato inizializzato prima di B!');
5        }
6    },
7});

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:

Migrazione a JavaScript di Wonderland Engine 1.0.0

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:

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

Le componenti translation, rotation, e scaling seguono ora questo pattern:

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]);

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:

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]);

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

 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}

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

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}

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

Last Update: June 13, 2025

Resta aggiornato.