Wonderland Engine 1.0.0 JavaScript Migration
Wonderland Engine ha experimentado importantes mejoras en cómo maneja, interactúa y distribuye el código JavaScript.
Esta publicación del blog te guiará a través de esos cambios. Además, la sección de Migraciones abordará cada nueva característica y detallará los pasos de migración para tu proyecto pre-1.0.0.
Motivación
Hasta ahora, el empaquetador predeterminado de Wonderland Engine se basaba en concatenar scripts locales. Los usuarios creaban scripts y el editor los recogía y empaquetaba para crear la aplicación final. Era necesario pre-empaquetar cualquier librería de terceros y colocarlas en la estructura de carpetas del proyecto.
Alternativamente, era posible configurar proyectos NPM, pero el proceso era manual y los artistas del equipo debían configurar NodeJS y seguir pasos para instalar dependencias, entre otras cosas.
El nuevo sistema tiene varias ventajas:
- Soporte para dependencias de paquetes NPM por defecto
- Mejoras significativas en las sugerencias de autocompletado del IDE
- Integración mucho más sencilla con herramientas avanzadas, como TypeScript
- Compatibilidad con otras librerías WebAssembly
- Múltiples instancias de Wonderland Engine por página
- Gestión automática del proyecto NPM para miembros del equipo no desarrolladores.
Hemos estado trabajando en un nuevo ecosistema de JavaScript para ayudarte a trabajar sin problemas con tus herramientas favoritas.
Componentes del Editor
Si anteriormente eras un usuario de NPM, probablemente te hayas encontrado con esto:

A partir de la versión 1.0.0, el editor ya no extrae tipos de componentes del paquete de aplicaciones. Además de corregir el error mencionado anteriormente, el editor lista más componentes de los que pueden estar registrados en la aplicación final. Esto permitirá a los usuarios avanzados configurar proyectos complejos con componentes transmisibles en el futuro.
Con este cambio, el editor ahora requerirá:
- Listar los componentes o carpetas en
Views > Project Settings > JavaScript > sourcePaths
- Agregar dependencias en el archivo raíz
package.json
Ejemplo de package.json
con una librería que expone componentes:
El editor ahora es capaz de encontrar componentes leyendo tu package.json
para acelerar el tiempo de desarrollo y mejorar la compartibilidad. Para más información, por favor consulta el tutorial Escribiendo Librerías JavaScript.
Agrupamiento
Una nueva configuración permite modificar el proceso de agrupamiento:
Views > Project Settings > JavaScript > bundlingType

Veamos cada una de estas opciones:
esbuild
Tus scripts se agruparán usando el empaquetador esbuild.
Esta es la opción predeterminada. Se recomienda mantener esta configuración siempre que sea posible por razones de rendimiento.
npm
Tus scripts se agruparán usando tu propio script npm.
Ejemplo de package.json
con un script build
personalizado:
1{
2 "name": "MyWonderfulProject",
3 "version": "1.0.0",
4 "description": "My Wonderland project",
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}
El nombre del script npm puede configurarse en la configuración del editor:
Views > Project Settings > JavaScript > npmScript

Este script puede ejecutar cualquier comando, siempre que genere tu paquete de aplicación final.
Puedes usar tu empaquetador favorito, como Webpack o Rollup. Sin embargo, recomendamos a los usuarios utilizar herramientas como esbuild para reducir el tiempo de iteración.
Punto de Entrada de la Aplicación
Los componentes se registran de manera diferente en tiempo de ejecución, es decir, cuando se ejecutan en el navegador.
Para el tiempo de ejecución, el editor puede gestionar automáticamente el punto de entrada de tu aplicación, es decir, un archivo index.js
.
El editor utiliza una plantilla que se ve aproximadamente así:
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 */
Esta plantilla se copia automáticamente en proyectos nuevos y antiguos pre-1.0.0.
La plantilla viene con las siguientes etiquetas:
wle:auto-imports
: Delimita dónde deben escribirse las declaraciones de importaciónwle:auto-register
: Delimita dónde deben escribirse las declaraciones de registrowle:auto-constants
: Delimita dónde el editor escribirá constantes, por ejemplo,ProjectName
: Nombre listado en el archivo.wlp
del proyectoWithPhysX
: Un booleano habilitado si el motor de física está habilitadoWithLoader
: Un booleano habilitado si la carga de glTF en tiempo de ejecución debe ser compatible
Como ejemplo:
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// ...
Este archivo de índice se genera automáticamente para un proyecto con un único componente llamado Forward
, definido en js/forward.js
.
Es importante notar que el editor solo importará y registrará componentes que estén usados en la escena, es decir, adjuntos a un objeto.
Si tu aplicación utiliza un componente solo en tiempo de ejecución, tienes dos opciones:
- Marcarlos como dependencias. Más información en la sección Dependencias del Componente
- Importarlos manualmente en tu archivo
index.js
Para aplicaciones simples, este archivo de plantilla será suficiente y no se requerirá ninguna modificación. Para casos de uso más complejos, puedes crear y gestionar tu propio archivo index.js
eliminando los comentarios de las etiquetas.
Gestionar el archivo de índice manualmente te permite crear aplicaciones con múltiples puntos de entrada y registrar componentes que el editor no conoce.
Clases JavaScript
Wonderland Engine 1.0.0 viene con una nueva forma de declarar componentes: Clases ES6.
1import {Component, Property} from '@wonderlandengine/api';
2
3class Forward extends Component {
4 /* Nombre de registro del componente. */
5 static TypeName = 'forward';
6 /* Propiedades expuestas en el 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}
Hay un par de cosas a tener en cuenta:
- Ya no usamos el símbolo global
WL
, sino que usamos la API de@wonderlandengine/api
en su lugar - Creamos una clase que hereda de la clase API
Component
- El nombre de registro del componente ahora es una propiedad estática
- Las propiedades se establecen en la clase
Propiedades JavaScript
Las propiedades de literales de objeto han sido reemplazadas por funtores:
1import {Component, Property}from '@wonderlandengine/api';
2
3class MyComponent extends MyComponent {
4 /* Nombre de registro del componente. */
5 static TypeName = 'forward';
6 /* Propiedades expuestas en el 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}
Dependencias del Componente
Las dependencias son componentes que se registran automáticamente cuando se registra tu componente.
Agreguemos un componente Speed
al ejemplo 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}
Al registrarse Forward
, Speed
se registrará automáticamente porque está listado como una dependencia.
Este comportamiento es gestionado por el booleano autoRegisterDependencies
en el objeto WonderlandEngine
creado en index.js
.
Eventos
Wonderland Engine solía manejar eventos usando arrays de oyentes, por ejemplo:
WL.onSceneLoaded
WL.onXRSessionStart
WL.scene.onPreRender
WL.scene.onPreRender
El motor ahora viene con una clase Emitter
para facilitar las interacciones en eventos:
Puedes gestionar tus oyentes usando un identificador:
Para más información, por favor consulta la documentación de la API Emitter.
Objeto
El Objeto
no ha quedado fuera de toda esta reestructuración. Se ha sometido a cambios para hacer que la API sea más consistente y segura.
Nombre de Exportación
La clase Object
ahora se exporta como Object3D
. Este cambio se realizó para evitar el sombreado del constructor Object de JavaScript.
Para suavizar la migración, Object
seguirá exportándose, pero asegúrate de usar
1import {Object3D}from '@wonderlandengine/api';
para facilitar futuras migraciones.
Transformaciones
La API de transformación tampoco se ha salvado. El motor ahora está descontinuando el uso de getters y setters (accesores) para transformación:
Estos getters / setters tienen algunos inconvenientes:
- Consistencia: No seguir otras API de transformación
- Rendimiento: Asignar
Float32Array
en cada llamada - Seguridad: Las vistas de memoria podrían ser alteradas por otro componente
- Posible error al almacenar luego la referencia de
Float32Array
para lectura
- Posible error al almacenar luego la referencia de
Si estás ansioso por ver la nueva API, por favor lee la sección Transformación de Objeto.
Aislamiento de JavaScript
Para los usuarios que usan el empaquetador interno, es posible que hayas visto código como:
component-a.js
component-b.js
El código anterior asume ciertas cosas sobre la variable componentAGlobal
. Espera que component-a
se registre primero y se anteponga en el paquete.
Esto solía funcionar porque el empaquetador interno de Wonderland Engine no realizaba aislamiento.
Con 1.0.0, ya sea que uses esbuild o npm, esto no funcionará más. Los empaquetadores no podrán hacer el enlace entre componentAGlobal
utilizado en component-a
y el utilizado en component-b
;
Como regla general: Piensa en cada archivo como aislado cuando uses un empaquetador.
Migraciones
Se requieren algunos pasos de migración manuales según si tu proyecto usaba npm o no.
Cada sección describirá los pasos apropiados requeridos según tu configuración anterior.
Componentes del Editor (#migration-editor-components)
Empaquetador Interno
Para los usuarios que anteriormente usaban el empaquetador interno, es decir, con la casilla useInternalBundler
activada:
Views > Project Settings > JavaScript > useInternalBundler
No se requieren más pasos.
npm
Para los usuarios de npm, necesitarás asegurarte de que tus propios scripts están listados en la configuración sourcePaths
.
Si estás utilizando una librería, asegúrate de que ha sido migrada a Wonderland Engine 1.0.0 siguiendo el tutorial Escribiendo Librerías JavaScript.
En caso de que una de tus dependencias no esté actualizada, puedes añadir la ruta local a la carpeta node_modules
en la configuración sourcePaths
. Ejemplo:

Ten en cuenta que el paquete generado mediante npm o esbuild ya no se usará para encontrar los componentes en el editor. Solo se usará al ejecutar tu aplicación.
Agrupamiento
No se requieren más pasos. El proyecto debería migrarse automáticamente.
Clases, Propiedades y Eventos JavaScript
Esta sección es la misma para todos los usuarios, ya sea que tuvieras useInternalBundler
habilitado o no.
Veamos un poco de código comparando el modo antiguo con el nuevo:
Antes de 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});
Después de 1.0.0
1/* No olvides que ahora usamos dependencias 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 se está alejando de una instancia global.
12 * Ahora puedes acceder a la instancia actual del motor a través de `this.engine`. */
13 this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
14 this.engine.onXRSessionEnd.add(this.onXRSessionEnd.bind(this));
15 }
16 start() {
17 this.object.resetTranslationRotation();
18 this.object.translate([0.0, this.height, 0.0]);
19 }
20 onXRSessionStart() {
21 if(!['local', 'viewer'].includes(WebXR.refSpace)) {
22 this.object.resetTranslationRotation();
23 }
24 }
25 onXRSessionEnd() {
26 if(!['local', 'viewer'].includes(WebXR.refSpace)) {
27 this.object.resetTranslationRotation();
28 this.object.translate([0.0, this.height, 0.0]);
29 }
30 }
31}
Estos dos ejemplos son equivalentes y llevan al mismo resultado.
Notarás una diferencia en la línea 5:
1WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));
vs
1this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
Dado que ahora hemos migrado a dependencias npm y empaquetamiento estándar, no hay necesidad de una variable global WL
.
Tener un motor expuesto globalmente tenía dos limitaciones:
- Más difícil compartir componentes
- Imposible tener múltiples instancias del motor en ejecución
Aunque el segundo punto no es un caso de uso común, no queremos limitar a ningún usuario en términos de escalabilidad.
Transformación de Objeto
La nueva API se basa en el patrón usual utilizado en todo Wonderland Engine:
Los componentes de traducción, rotación y escalado ahora siguen este patrón también:
Al igual que con el resto de la API, usar un parámetro
out
vacío para los getters llevará a la creación del arreglo de salida. Nota que siempre es mejor reutilizar arreglos cuando sea posible (por razones de rendimiento).
Además de poder leer y escribir las transformaciones del espacio local del objeto, también puedes operar directamente en el espacio mundial:
Para más información, por favor consulta la documentación de la API Object3D.
Aislamiento de JavaScript
Esta sección es la misma para todos los usuarios, ya sea que tenías useInternalBundler
habilitado o no.
Existen múltiples formas de compartir datos entre componentes, y depende del desarrollador de la aplicación elegir la más adecuada.
Proporcionaremos algunos ejemplos sobre cómo compartir datos sin depender de variables globales.
Estado en Componentes
Los componentes son contenedores de datos que son accedidos a través de otros componentes.
Por lo tanto, puedes crear componentes para mantener el estado de tu aplicación. Por ejemplo, si estás creando un juego con tres estados:
- Running
- Ganado
- Perdido
Puedes crear un componente singleton con la siguiente forma:
game-state.js
El componente GameState
puede ser añadido a un objeto gestor. Este objeto debe ser referenciado por componentes que alterarán el estado del juego.
Creemos un componente para cambiar el estado del juego cuando un jugador muere:
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 /* El jugador murió, cambia el estado. */
13 if(this.health <= 0.0) {
14 const gameState = this.manager.getComponent(GameState);
15 gameState.state = 2; // Establecemos el estado en `lost`.
16 }
17 }
18}
Esta es una forma de demostrar cómo reemplazar variables globales en tu aplicación pre-1.0.0.
Exportaciones
También es posible compartir variables mediante import y export. Sin embargo, ten en cuenta que el objeto será idéntico en el paquete completo.
Podemos revisar el ejemplo anterior con exportaciones:
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}
Esta solución funciona, pero no es infalible.
Veamos un ejemplo donde esto no funcionará. Supongamos que este código está en una librería llamada gamestate
.
- Tu aplicación depende de la versión 1.0.0 de
gamestate
- Tu aplicación depende de la librería
A
- La librería
A
depende de la versión 2.0.0 degamestate
Tu aplicación terminará con dos copias de la librería gamestate
, porque ambas no son compatibles en términos de versión.
Cuando la librería A
actualiza el objeto GameState
, en realidad está cambiando sus propias instancias de esta exportación. Esto sucede porque ambas versiones no son compatibles, lo que hace que tu aplicación empaquete dos instancias distintas de la librería.
Palabra Final
¡Con esta guía, ahora estás listo para migrar tus proyectos a Wonderland Engine 1.0.0!
Si encuentras algún problema, por favor contacta a la comunidad en el servidor de Discord.