Wonderland Engine 1.0.0 Миграция JavaScript
Wonderland Engine претерпел значительные улучшения в том, как он обрабатывает, взаимодействует с и распределяет JavaScript-код.
Этот пост в блоге поможет вам разобраться в этих изменениях. Кроме того, в разделе Миграции будет рассмотрена каждая новая функция, чтобы подробно описать шаги миграции для вашего проекта до версии 1.0.0.
Мотивация
До настоящего времени стандартный бандлер Wonderland Engine основывался на объединении локальных скриптов. Пользователи могли создавать скрипты, а редактор собирал их и создавал финальное приложение. Требовалось предварительно собирать любые сторонние библиотеки и размещать их в структуре папок проекта.
Альтернативно, можно было настроить проекты NPM, но установка была ручной и требовала от участников команды установки NodeJS и выполнения шагов для установки зависимостей и других действий.
Новая система имеет несколько преимуществ:
- Поддержка зависимостей пакета NPM по умолчанию
- Гораздо более качественные предложения по автозавершению из вашего IDE
- Намного более простая интеграция с усовершенствованными инструментами, такими как TypeScript
- Совместимость с другими библиотеками WebAssembly
- Несколько экземпляров Wonderland Engine на странице
- Автоматическое управление вашим проектом NPM для членов команды без навыков разработки.
Мы создали новую экосистему JavaScript, которая позволит вам работать беспроблемно с вашими любимыми инструментами.
Компоненты редактора
Если вы ранее работали с NPM, возможно, сталкивались с этой проблемой:

Начиная с версии 1.0.0, редактор больше не выбирает типы компонентов из пакета приложения. Помимо исправления вышеприведенной ошибки, редактор перечисляет больше компонентов, чем может быть зарегистрировано в финальном приложении. Это позволит опытным пользователям настроить сложные проекты с потоковыми компонентами в будущем.
С этим изменением редактор теперь потребуется:
- Указать компоненты или папки в
Views > Project Settings > JavaScript > sourcePaths
- Добавить зависимости в корневой файл
package.json
Пример package.json
с библиотекой, предоставляющей компоненты:
Редактор теперь может находить компоненты, читая ваш package.json
, чтобы ускорить время разработки и улучшить совместимость.
Для получения дополнительной информации обратитесь к руководству по Созданию JavaScript-библиотек.
Бандлинг
Новое настройка позволяет изменять процесс бандлинга:
Views > Project Settings > JavaScript > bundlingType

Рассмотрим каждый из вариантов:
esbuild
Ваши скрипты будут собраны с помощью бандлера esbuild.
Это выбор по умолчанию. Вам рекомендуется придерживаться этой настройки по возможности в целях повышения производительности.
npm
Ваши скрипты будут собраны с использованием собственного скрипта npm.
Пример package.json
с пользовательским скриптом build
:
1{
2 "name": "MyWonderfulProject",
3 "version": "1.0.0",
4 "description": "Мой проект 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}
Имя скрипта npm можно указать в настройках редактора:
Views > Project Settings > JavaScript > npmScript

Этот скрипт может выполнять любую команду, если она генерирует ваш окончательный пакет приложения.
Вы можете использовать ваш любимый бандлер, такой как Webpack или Rollup. Тем не менее, мы советуем использовать такие инструменты, как esbuild, чтобы сократить время итерации.
Точка входа приложения
Компоненты регистрируются по-другому в момент выполнения, т.е. при запуске в браузере.
Редактор может автоматически управлять точкой входа вашего приложения, т.е. файлом index.js
.
Редактор использует шаблон, который выглядит примерно так:
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 */
Этот шаблон автоматически копируется в новосозданные и старые проекты до версии 1.0.0.
Шаблон включает в себя следующие теги:
wle:auto-imports
: Ограничивает, где должны быть записаны директивы импортаwle:auto-register
: Ограничивает, где должны быть записаны директивы регистрацииwle:auto-constants
: Ограничивает, где редактор будет записывать константы, например,ProjectName
: Имя в файле.wlp
проектаWithPhysX
: Логическое значение, если физический движок включенWithLoader
: Логическое значение, если нужна поддержка загрузки glTF во время выполнения
Пример использования:
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// ...
Этот index-файл автоматически сгенерирован для проекта с единственным компонентом Forward
, определённым в js/forward.js
.
Важно отметить, что редактор будет импортировать и регистрировать только те компоненты, которые используются в пределах сцены, т.е. прикреплены к объекту.
Если ваше приложение использует компонент только в момент выполнения, вам нужно либо:
- Отметить их как зависимости. Подробнее в разделе Зависимости компонентов
- Импортировать их вручную в ваш
index.js
файл
Для простых приложений этот шаблонный файл будет достаточно, и не потребуется никаких изменений. Для более сложных случаев использования вы можете свободно создавать и управлять своим собственным index.js
файлом, удаляя комментарии с тегами.
Управление index-файлом вручную позволяет вам создавать приложения с несколькими точками входа, а также непосредственно регистрировать компоненты, о которых редактор не знает.
Классы JavaScript
Wonderland Engine 1.0.0 предлагает новый способ объявления компонентов: Классы ES6.
1import {Component, Property} from '@wonderlandengine/api';
2
3class Forward extends Component {
4 /* Регистрационное имя компонента */
5 static TypeName = 'forward';
6 /* Свойства, отображаемые в редакторе */
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}
Есть несколько важных моментов:
- Мы больше не используем глобальный символ
WL
, вместо этого используем API из@wonderlandengine/api
- Мы создаем класс, который наследуется от класса
Component
API - Регистрационное имя компонента теперь статическое свойство
- Свойства задаются непосредственно в классе
Свойства JavaScript
Свойства объектных литералов заменены функторами:
1import {Component, Property} from '@wonderlandengine/api';
2
3class MyComponent extends MyComponent {
4 /* Регистрационное имя компонента */
5 static TypeName = 'forward';
6 /* Свойства, отображаемые в редакторе */
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}
Зависимости компонентов
Зависимости — это компоненты, которые автоматически регистрируются при регистрации вашего компонента.
Давайте добавим Speed
компонент в пример с 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}
При регистрации Forward
, Speed
будет автоматически зарегистрирован, так как он указан как зависимость.
Это поведение управляется логическим значением autoRegisterDependencies
на объекте WonderlandEngine
, созданном в index.js
.
События
Ранее Wonderland Engine обрабатывал события с помощью массивов слушателей, например:
WL.onSceneLoaded
WL.onXRSessionStart
WL.scene.onPreRender
Теперь в движке используется класс Emitter
для облегчения взаимодействия с событиями:
Вы можете управлять своими слушателями, используя идентификатор:
Для получения дополнительной информации, пожалуйста, ознакомьтесь с документацией API Emitter.
Object
Класс Object
также подвергся изменениям в рамках всей переработки. API были изменены, чтобы стать более последовательными и безопасными.
Имя экспортируемого объекта
Класс Object
теперь экспортируется как Object3D
. Это изменение сделано, чтобы избежать затенения конструктора JavaScript Object.
Чтобы облегчить миграцию, Object
будет по-прежнему экспортироваться, но удостоверьтесь, что вы теперь используете
1import {Object3D} from '@wonderlandengine/api';
для облегчения будущих миграций.
Трансформации
API трансформаций также подверглись изменениям. Движок теперь отказывается от использования геттеров и сеттеров (аксессоров) для трансформации:
Эти геттеры / сеттеры имели несколько недостатков:
- Последовательность: Не соответствуют другим API трансформаций
- Производительность: Выделение
Float32Array
при каждом вызове - Безопасность: Представления памяти могли изменяться другими компонентами
- Возможные ошибки при хранении ссылки на
Float32Array
для последующего чтения
- Возможные ошибки при хранении ссылки на
Если вас интересует новый API, обратитесь к разделу Object Transform.
Изоляция JavaScript
Для пользователей, которые использовали внутренний бандлер, возможно, вы видели код следующего типа:
component-a.js
component-b.js
Вышеприведенный код делает предположения о переменной componentAGlobal
. Он ожидает, что component-a
будет зарегистрирован первым и добавлен в начало пакета.
Это работало, потому что внутренний бандлер Wonderland Engine не обеспечивал изоляцию.
С версией 1.0.0, будь то использование esbuild или npm, это больше не будет работать. Бандлеры не смогут связать componentAGlobal
, использованный в component-a
, с тем, который используется в component-b
;
Как правило: Считайте каждый файл изолированным при использовании бандлера.
Миграции
Некоторые шаги ручной миграции потребуются в зависимости от того, использовали ли вы npm раньше.
Каждый раздел опишет соответствующие шаги, необходимые в зависимости от вашей предыдущей настройки.
Компоненты редактора (#migration-editor-components)
Внутренний бандлер
Для пользователей, ранее использовавших внутренний бандлер, т.е. с активированным флажком useInternalBundler
:
Views > Project Settings > JavaScript > useInternalBundler
Дальнейшие шаги не требуются.
Npm
https://www.npmjs.com/package/wle-js-upgrade
Для пользователей npm вам необходимо будет убедиться, что ваши собственные скрипты перечислены в настройке sourcePaths
.
Если вы используете библиотеку, убедитесь, что она была мигрирована на Wonderland Engine 1.0.0 в соответствии с инструкциями в Создание JavaScript-библиотек.
Если какая-либо из ваших зависимостей не была обновлена, вы можете добавить локальный путь в папку node_modules
в настройках sourcePaths
. Пример:

Всегда помните, что сгенерированный пакет через npm или esbuild больше не будет использоваться для поиска компонентов в редакторе. Пакет будет использоваться только при запуске вашего приложения.
Бандлинг
Дальнейшие шаги не требуются. Проект должен автоматически быть мигрирован.
Классы, свойства и события JavaScript
Этот раздел одинаков для всех пользователей, независимо от того, был ли у вас включён useInternalBundler
или нет.
Давайте взглянем на код, сравнивающий старый и новый подход:
До 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});
После 1.0.0
1/* Не забывайте, что мы теперь используем 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 уходит от глобального
12 * экземпляра. Теперь вы можете получить доступ к текущему экземпляру движка
13 * через `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}
Эти два примера эквивалентны и будут иметь одинаковый результат.
Обратите внимание на различие в строке 5:
1WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));
против
1this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
Поскольку теперь мы перешли на npm зависимости и стандартный бандлинг, необходимость в глобальной переменной WL
отпала.
Наличие глобально доступного движка ограничивало в двух отношениях:
- Делиться компонентами было сложнее
- Невозможно было запускать несколько экземпляров движка
Хотя второй случай использования не распространен, мы не хотим ограничивать пользователей в плане масштабируемости.
Трансформация объекта
Новый API основан на общеупотребительной схеме:
Компоненты translation, rotation и scaling теперь следуют этой схеме:
Как и в остальной части API, использование пустого
out
параметра для геттеров приведет к созданию выходного массива. Обратите внимание, что всегда лучше переиспользовать массивы, если это возможно (по причинам производительности).
Также вы можете непосредственно работать с преобразованиями пространства в масштабе мира:
Для получения дополнительной информации, пожалуйста, ознакомьтесь с документацией API Object3D.
Изоляция JavaScript
Этот раздел одинаков для всех пользователей, независимо от того, имели ли вы включён useInternalBundler
или нет.
Существует множество способов обмена данными между компонентами, и выбор наиболее подходящего остаётся за разработчиком приложения.
Мы дадим несколько примеров обмена данными, которые не зависят от глобальных переменных.
Состояние в компонентах
Компоненты представляют собой контейнеры данных, которые могут быть доступны другими компонентами.
Можно создавать компоненты для хранения состояния вашего приложения. Например, если вы создаете игру с тремя состояниями:
- Запущено
- Победа
- Поражение
Вы можете создать синглтон-компонент, который будет выглядеть следующим образом:
game-state.js
Компонент GameState
можно добавить на объект менеджера. Этот объект должен быть предоставлен в качестве ссылки для компонентов, которые будут изменять состояние игры.
Давайте создадим компонент, который изменит состояние игры, если игрок потерпел поражение:
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 /* Если игрок потерпел поражение, изменяем состояние. */
13 if(this.health <= 0.0) {
14 const gameState = this.manager.getComponent(GameState);
15 gameState.state = 2; // Устанавливаем состояние на `lost`.
16 }
17 }
18}
Это один из примеров демонстрации того, как заменить глобальные переменные в вашем приложении до версии 1.0.0.
Экспорты
Также можно обмениваться переменными через import и export. Однако помните, что объект будет идентичным во всём пакете.
Мы можем пересмотреть вышеупомянутый пример с использованием экспортов:
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}
Это решение работает, но не безусловно.
Позвольте привести пример, когда это не сработает. Представим, что этот код находится в библиотеке под названием gamestate
.
- Ваше приложение зависит от
gamestate
версии 1.0.0 - Ваше приложение зависит от библиотеки
A
- Библиотека
A
зависит отgamestate
версии 2.0.0
Ваше приложение в конечном итоге будет иметь две копии библиотеки gamestate
, поскольку версии не совместимы.
Когда библиотека A
обновляет объект GameState
, она фактически изменяет собственный экземпляр этого экспорта. Это происходит, потому что обе версии не совместимы, и ваше приложение связывает два разных экземпляра библиотеки.
Заключительное слово
С помощью этого руководства вы теперь готовы мигрировать свои проекты на Wonderland Engine 1.0.0!
Если вы столкнётесь с какой-либо проблемой, пожалуйста, свяжитесь с сообществом на Discord сервере.