Wonderland Engine 1.0.0 JavaScript 迁移
Wonderland Engine 正在对其处理、交互和分发 JavaScript 代码的方式进行大幅改进。
这篇博文将指导您了解这些变化。此外,迁移部分将详细介绍每个新功能,为您的 pre-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 */
此模板会自动复制到新创建的和旧的 pre-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// ...
此索引文件为具有单个称为 Forward
组件的项目自动生成,定义在 js/forward.js
中。
重要的是要注意,编辑器将仅导入和注册在场景中使用的组件,即附加到对象的组件。
如果您的应用程序仅在运行时使用组件,您将需要:
- 将它们标记为依赖。在组件依赖部分了解更多信息
- 在
index.js
文件中手动导入它们
对于简单的应用程序,这个模板文件就足够了,不需要修改。对于更复杂的使用情况,
您可以通过移除标记注释来自由创建和管理自己的 index.js
文件。
手动管理索引文件可以让您创建具有多个入口点的应用程序,并注册编辑器未知的组件。
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
符号,而是使用@wonderlandengine/api
的 API - 我们创建了一个继承自 API
Component
类的类 - 组件的注册名称现在是一个静态属性
- 属性在类上设置
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}
组件依赖
依赖是您的组件注册时自动注册的组件。
让我们在 Forward
示例中添加一个 Speed
组件:
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
会因被列为依赖而自动注册。
该行为由在 index.js
中创建的 WonderlandEngine
对象上的布尔值 autoRegisterDependencies
管理。
事件
Wonderland Engine 使用监听器数组来处理事件,例如:
WL.onSceneLoaded
WL.onXRSessionStart
WL.scene.onPreRender
WL.scene.onPreRender
引擎现在附带一个 Emitter
类来简化事件交互:
您可以使用标识符管理监听器:
有关更多信息,请查看 Emitter API 文档。
Object
Object
并未在整个重做中被忽视。它经过更改,使 API 更加一致,更加安全。
导出名称
Object
类现在导出为 Object3D
。此更改是为防止对 JavaScript Object 构造函数的遮盖。
为了简化迁移,Object
仍将被导出,但请确保您现在使用
1import {Object3D} from '@wonderlandengine/api';
以便于未来的迁移。
变换
变换 API 也未能幸免。引擎现在不鼓励使用变换的 getter & setter(访问器):
这些 getter / setter 存在一些缺点:
- 一致性:未遵循其它变换 API
- 性能:每次调用时分配
Float32Array
- 安全性:内存视图可能被其他组件更改
- 存储
Float32Array
引用以供后续读取时可能出现潜在错误
- 存储
如果您渴望了解新 API,请阅读 Object Transform 部分。
JavaScript 隔离
对于使用内部打包器的用户,您可能已经看到如以下代码:
component-a.js
component-b.js
上述代码对 componentAGlobal
变量做出了一些假设。它期望 component-a
先行注册并在打包中被预置。
过去,这种方式工作是因为 Wonderland Engine 内部打包器不执行隔离。
在 1.0.0 中,无论您使用 esbuild 还是 npm,这都将不再有效。打包器将无法将 component-a
中使用的 componentAGlobal
与 component-b
中使用的关联起来。
简而言之:在使用打包器时,把每个文件都视为隔离。
迁移
根据您的项目是否使用 npm,可能需要一些手动迁移步骤。
每个部分将描述根据您以前的设置所需的适当步骤。
编辑器组件 (#migration-editor-components)
内部打包器
对于以前使用内部打包器的用户,即 useInternalBundler
复选框处于激活状态:
Views > Project Settings > JavaScript > useInternalBundler
不需要 任何进一步步骤。
Npm
https://www.npmjs.com/package/wle-js-upgrade
对于 npm 用户,您需要确保您的脚本列在 sourcePaths
设置中。
如果您使用的是库,请确保该库已按照 编写 JavaScript 库 教程迁移到 Wonderland Engine 1.0.0。
如果您的依赖项之一没有更新,您可以在 sourcePaths
设置中添加 node_modules
文件夹的本地路径。示例:

请始终记住,通过 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 * 现在可以通过 `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}
这两个示例是等效的,并将导致相同的结果。
您会注意到第 5 行的区别:
1WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));
与之比较
1this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
因为我们现在迁移到了npm 依赖和标准打包,已经不再需要全局 WL
变量。
拥有一个全局暴露的引擎有两个限制:
- 难以共享组件
- 无法运行多个引擎实例
尽管第二个要点并不是一个常见的用例,但我们不想在可扩展性方面限制任何用户。
对象变换
新API基于Wonderland Engine中常用的模式:
变换的平移、旋转、和缩放组件现在也遵循这个模式:
如 API 的其他部分一样,使用空
out
参数的getter会导致输出数组的创建。请注意,尽可能重用数组(出于性能原因)始终是更好的选择。
除了能够读取和写入对象的本地空间变换,您还可以直接操作世界空间:
有关更多信息,请查看 Object3D API 文档。
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}
这是在你的 pre-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 服务器 联系社区。