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": "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}
Имя скрипта 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
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 основан на обычной схеме, используемой в Wonderland Engine:
Компоненты translation, rotation и scaling теперь следуют этой схеме:
Как и в остальной части api, использование пустого параметра
out
для геттеров приведет к созданию выходного массива. Обратите внимание, что всегда лучше переиспользовать массивы, когда это возможно (из-за причин производительности).
Кроме возможности читать и записывать локальные пространственные преобразования объекта, можно также напрямую работать в мировом пространстве:
Для получения дополнительной информации, пожалуйста, ознакомьтесь с документацией API Object3D. (TODO: Замените ссылку)
Изоляция JavaScript
Этот раздел одинаков для всех пользователей, независимо от того, имели ли вы включенный useInternalBundler
или нет.
Существует множество способов обмена данными между компонентами, и выбор наиболее подходящего зависит от разработчика приложения.
Мы приведем несколько примеров обмена данными, которые не зависят от глобальных переменных.
Состояние в компонентах
Компоненты представляют собой контейнеры данных, которые доступны через другие компоненты.
Вы можете создавать компоненты для хранения состояния вашего приложения. Например, если вы создаете игру с тремя состояниями:
- Running
- Won
- Lost
Вы можете создать одиночный компонент с следующимни структурами:
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.
Экспорты
Также можно делиться переменными через импорт и экспорт. Однако помните, что объект будет идентичным во всем пакете.
Мы можем пересмотреть приведенный выше пример с экспортами:
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 сервере.