Управляйте своими настройками cookies. Вы можете включать или отключать различные виды cookies ниже. Для получения более подробной информации см. нашу Политику конфиденциальности.

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, возможно, сталкивались с этой проблемой:

Wonderland Engine 1.0.0 Миграция JavaScript

Начиная с версии 1.0.0, редактор больше не выбирает типы компонентов из пакета приложения. Помимо исправления вышеприведенной ошибки, редактор перечисляет больше компонентов, чем может быть зарегистрировано в финальном приложении. Это позволит опытным пользователям настроить сложные проекты с потоковыми компонентами в будущем.

С этим изменением редактор теперь потребуется:

  • Указать компоненты или папки в Views > Project Settings > JavaScript > sourcePaths
  • Добавить зависимости в корневой файл package.json

Пример package.json с библиотекой, предоставляющей компоненты:

1{
2  "name": "my-wonderful-project",
3  "version": "1.0.0",
4  "description": "Мой проект Wonderland",
5  "dependencies": {
6    "@wonderlandengine/components": "^1.0.0-rc.5"
7  }
8}

Редактор теперь может находить компоненты, читая ваш package.json, чтобы ускорить время разработки и улучшить совместимость. Для получения дополнительной информации обратитесь к руководству по Созданию JavaScript-библиотек.

Бандлинг 

Новое настройка позволяет изменять процесс бандлинга:

Views > Project Settings > JavaScript > bundlingType

Wonderland Engine 1.0.0 Миграция JavaScript

Рассмотрим каждый из вариантов:

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

Wonderland Engine 1.0.0 Миграция JavaScript

Этот скрипт может выполнять любую команду, если она генерирует ваш окончательный пакет приложения.

Вы можете использовать ваш любимый бандлер, такой как 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-файлом вручную позволяет вам создавать приложения с несколькими точками входа, а также непосредственно регистрировать компоненты, о которых редактор не знает.

Классы 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 для облегчения взаимодействия с событиями:

1engine.onXRSessionStart.add((session, mode) => {
2    console.log(`Начало сессии '${mode}'!`);
3})

Вы можете управлять своими слушателями, используя идентификатор:

1engine.onXRSessionStart.add((session, mode) => {
2    console.log(`Начало сессии '${mode}'!`);
3}, {id: 'my-listener'});
4
5// Когда вы закончите, вы можете просто удалить его по `id`.
6engine.onXRSessionStart.remove('my-listener');

Для получения дополнительной информации, пожалуйста, ознакомьтесь с документацией API Emitter.

Object 

Класс Object также подвергся изменениям в рамках всей переработки. API были изменены, чтобы стать более последовательными и безопасными.

Имя экспортируемого объекта 

Класс Object теперь экспортируется как Object3D. Это изменение сделано, чтобы избежать затенения конструктора JavaScript Object.

Чтобы облегчить миграцию, Object будет по-прежнему экспортироваться, но удостоверьтесь, что вы теперь используете

1import {Object3D} from '@wonderlandengine/api';

для облегчения будущих миграций.

Трансформации 

API трансформаций также подверглись изменениям. Движок теперь отказывается от использования геттеров и сеттеров (аксессоров) для трансформации:

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

Эти геттеры / сеттеры имели несколько недостатков:

  • Последовательность: Не соответствуют другим API трансформаций
  • Производительность: Выделение Float32Array при каждом вызове
  • Безопасность: Представления памяти могли изменяться другими компонентами
    • Возможные ошибки при хранении ссылки на Float32Array для последующего чтения

Если вас интересует новый API, обратитесь к разделу Object Transform.

Изоляция JavaScript 

Для пользователей, которые использовали внутренний бандлер, возможно, вы видели код следующего типа:

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('Component A был инициализирован перед B!');
5        }
6    },
7});

Вышеприведенный код делает предположения о переменной 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. Пример:

Wonderland Engine 1.0.0 Миграция JavaScript

Всегда помните, что сгенерированный пакет через 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 основан на общеупотребительной схеме:

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

Компоненты translation, rotation и scaling теперь следуют этой схеме:

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

Как и в остальной части API, использование пустого out параметра для геттеров приведет к созданию выходного массива. Обратите внимание, что всегда лучше переиспользовать массивы, если это возможно (по причинам производительности).

Также вы можете непосредственно работать с преобразованиями пространства в масштабе мира:

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

Для получения дополнительной информации, пожалуйста, ознакомьтесь с документацией API Object3D.

Изоляция JavaScript 

Этот раздел одинаков для всех пользователей, независимо от того, имели ли вы включён useInternalBundler или нет.

Существует множество способов обмена данными между компонентами, и выбор наиболее подходящего остаётся за разработчиком приложения.

Мы дадим несколько примеров обмена данными, которые не зависят от глобальных переменных.

Состояние в компонентах 

Компоненты представляют собой контейнеры данных, которые могут быть доступны другими компонентами.

Можно создавать компоненты для хранения состояния вашего приложения. Например, если вы создаете игру с тремя состояниями:

  • Запущено
  • Победа
  • Поражение

Вы можете создать синглтон-компонент, который будет выглядеть следующим образом:

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}

Компонент 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

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}

Это решение работает, но не безусловно.

Позвольте привести пример, когда это не сработает. Представим, что этот код находится в библиотеке под названием gamestate.

  • Ваше приложение зависит от gamestate версии 1.0.0
  • Ваше приложение зависит от библиотеки A
  • Библиотека A зависит от gamestate версии 2.0.0

Ваше приложение в конечном итоге будет иметь две копии библиотеки gamestate, поскольку версии не совместимы.

Когда библиотека A обновляет объект GameState, она фактически изменяет собственный экземпляр этого экспорта. Это происходит, потому что обе версии не совместимы, и ваше приложение связывает два разных экземпляра библиотеки.

Заключительное слово 

С помощью этого руководства вы теперь готовы мигрировать свои проекты на Wonderland Engine 1.0.0!

Если вы столкнётесь с какой-либо проблемой, пожалуйста, свяжитесь с сообществом на Discord сервере.

Last Update: March 28, 2025

Будьте в курсе.