Wonderland Engine 1.0.0 JavaScript Migration
Wonderland Engine ha estado sometido a mejoras significativas en cómo maneja, interactúa y distribuye código JavaScript.
Esta publicación de blog te guiará a través de esos cambios. Además, la sección Migraciones revisará cada nueva característica para detallar los pasos de migración para su proyecto pre-1.0.0.
Motivación
Hasta ahora, el empaquetador por defecto de Wonderland Engine dependía de concatenar scripts locales. Los usuarios crearían scripts, y el editor los recogería y empaquetaría 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 la configuración era manual y los artistas del equipo necesitarían configurar NodeJS y seguir pasos para instalar dependencias y más.
El nuevo sistema tiene varias ventajas:
- Soporte para dependencias de paquetes NPM por defecto
- Mejoras significativas en las sugerencias de autocompletado de tu IDE
- Integración mucho más fácil con herramientas avanzadas, como TypeScript
- Compatibilidad con otras librerías WebAssembly
- Múltiples instancias de Wonderland Engine por página
- Gestión automática de tu proyecto NPM para miembros del equipo que no son 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, es probable que te hayas encontrado con esto:

A partir de la versión 1.0.0, el editor ya no toma tipos de componentes del paquete de la aplicación. Además de corregir el error anterior, 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 transmistibles 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 echa un vistazo al 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 apegarse a 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 establecerse 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, aconsejamos 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, al ejecutarse en el navegador.
Para el tiempo de ejecución, el editor puede manejar automáticamente el punto de entrada de tu aplicación, es decir, un archivo index.js
.
El editor usa 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 recién creados y antiguos pre-1.0.0.
La plantilla viene con las siguientes etiquetas:
wle:auto-imports
: Delimita dónde deberían escribirse las declaraciones de importaciónwle:auto-register
: Delimita dónde deberían 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 solo 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 usa un componente solo en tiempo de ejecución, necesitas:
- 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 será necesaria ninguna modificación. Para casos de uso más complejos, eres libre de 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 echa un vistazo a la documentación de la API Emitter.
Objeto
El Objeto
no ha quedado fuera de toda esta reestructuración. Se sometió 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 sombrear al constructor Object de JavaScript.
Para suavizar la migración, Object
todavía se exportará, pero asegúrate de que ahora estás usando
1import {Object3D} from '@wonderlandengine/api';
para facilitar futuras migraciones.
Transformaciones
La API de transformación tampoco ha sido perdonada. El motor ahora está descontinuando el uso de getters & setters (accesores) para la transformación:
Estos getters / setters tienen algunos inconvenientes:
- Consistencia: No sigue otras API de transformación
- Rendimiento: Asigna
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 está haciendo algunas suposiciones 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 requiere 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:

Siempre ten en cuenta que el paquete generado mediante npm o esbuild ya no se utilizará para encontrar los componentes en el editor. Solo se usará al ejecutar tu aplicación.
Agrupamiento
No se requiere 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 la forma antigua frente a la nueva:
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
12 * instancia global. Ahora puedes acceder a la instancia
13 * actual del motor a través de `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}
Esos dos ejemplos son equivalentes y conducirán 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));
Debido a 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 echa un vistazo a la documentación de la API Object3D.
Aislamiento de JavaScript
Esta sección es la misma para todos los usuarios, ya sea que tuvieras 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 apropiada.
Daremos algunos ejemplos sobre cómo compartir datos sin depender de variables globales.
Estado en Componentes
Los componentes son bolsas de datos que son accedidas 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 entonces ser añadido a un objeto gestor. Este objeto debería luego 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 manera de demostrar cómo reemplazar globales en tu aplicación pre-1.0.0.
Exportaciones
También es posible compartir variables mediante import y export. Sin embargo, recuerda 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 funciona. Imaginemos 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, haciendo 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.