Wonderland Engine 1.0.0 JavaScript Migration
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
を読み込むことで、コンポーネントを見つけて開発時間を短縮し、共有性を向上させるための改善を行います。
詳しい情報については、Writing JavaScript Libraries チュートリアルをぜひご覧ください。
バンドリング
新しい設定により、バンドリングプロセスを変更できます:
Views > Project Settings > JavaScript > bundlingType

各オプションを見ていきましょう:
esbuild
あなたのスクリプトは、esbuild バンドラを使用してバンドルされます。
これはデフォルトの選択で、パフォーマンス上の理由からこの設定を使用することをお勧めします。
npm
あなたのスクリプトは、独自の npm スクリプトを用いてバンドルされます。
カスタム build
スクリプトを持つ package.json
の例:
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// ...
このインデックスファイルは、js/forward.js
に定義された単一のコンポーネント Forward
を持つプロジェクト用に自動生成されます。
重要なのは、エディタはシーンで使用されている、つまりオブジェクトにアタッチされているコンポーネントのみをインポートし、登録することです。
アプリケーションがランタイムでのみ使用するコンポーネントを持つ場合、次のいずれかを行う必要があります:
- それを依存関係としてマークする。コンポーネントの依存関係 セクションで詳細を確認
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 Component {
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
クラスもこの再設計から除外されていません。APIをより一貫性があり、安全にするための変更が加えられました。
エクスポート名
Object
クラスは現在、Object3D
としてエクスポートされています。この変更は、JavaScriptのObjectコンストラクターを覆い隠すことを防ぐために行われました。
移行を円滑にするために、Object
は引き続きエクスポートされますが、将来の移行を円滑にするために、今後は
1import {Object3D} from '@wonderlandengine/api';
を使用してください。
変換
変換APIも変更されています。エンジンは現在、ゲッターおよびセッター(アクセサー)の使用を変換において非推奨としています:
これらのゲッター / セッターの欠点は以下の通りです:
- 一貫性: 他の変換APIと不一致
- パフォーマンス: 各呼び出しで
Float32Array
が割り当てられる - 安全性: メモリビューが他のコンポーネントによって変更される可能性
- 後で読み出しのために
Float32Array
参照を保存したときにバグが発生する可能性
- 後で読み出しのために
新しいAPIについては、オブジェクト変換セクションを参照してください。
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
npmユーザーの場合、スクリプトが sourcePaths
設定にリストされていることを確認する必要があります。
ライブラリを使用している場合、そのライブラリがWriting JavaScript Librariesチュートリアルに従って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 * 現在のエンジンインスタンスにアクセスできます。 */
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}
これらの2つの例は等価であり、同じ結果をもたらします。
5行目の違いに注目してください:
1WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));
対
1this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
npm依存関係と標準バンドリングに移行したため、もはやグローバル WL
変数は必要ありません。
グローバルエンジンを公開することには次の2つの制約がありました:
- コンポーネントの共有が難しい
- 複数のエンジンインスタンスを実行できない
2番目のポイントは一般的な使用例ではありませんが、ユーザーがスケーラビリティについて制限されることを望んでいません。
オブジェクト変換
新しいAPIは、Wonderland Engine全体での使用を意図して通常のパターンに基づいています:
翻訳、回転、およびスケールのコンポーネントも、このパターンに従います:
他のAPIと同様に、ゲッターの
out
パラメータを空で使用すると、出力配列が作成されます。再利用可能な配列を使用することが常に望ましいです(パフォーマンスの理由で)。
ローカルスペースでのオブジェクト変換を読み書きできるだけでなく、ワールドスペースで直接操作することも可能です:
詳しくは、Object3D API ドキュメントをご覧ください。
JavaScriptのアイソレーション
このセクションは、全てのユーザーに共通で、以前に useInternalBundler
が有効だったかどうかに関係なく同じです。
コンポーネント間でデータを共有する方法は多数あり、アプリケーションの開発者が最も適した方法を選択します。
ここでは、グローバル変数に依存しないデータ共有方法のいくつかの例を紹介します。
コンポーネント内の状態
コンポーネントは、他のコンポーネントによってアクセスされるデータの集合体です。
したがって、アプリケーションの状態を保持するためのコンポーネントを作成できます。たとえば、3つの状態があるゲームを作成している場合:
- 実行中
- 勝利
- 敗北
次の形のシングルトンコンポーネントを作成できます:
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}
この方法は機能しますが、完全ではありません。
以下は機能しない例の1つです。このコードが gamestate
というライブラリにあると想像してください。
- あなたのアプリケーションは
gamestate
バージョン 1.0.0 に依存しています - あなたのアプリケーションはライブラリ
A
に依存しています - ライブラリ
A
はgamestate
バージョン 2.0.0 に依存しています
アプリケーションは2つの gamestate
ライブラリのコピーを持つことになります。なぜなら、両バージョンは互換性がないからです。
ライブラリ A
が GameState
オブジェクトを更新すると、それは自身のこのエクスポートのインスタンスを変更しています。これは、両方のバージョンが互換性がないため、アプリケーションが2つの異なるインスタンスのライブラリをバンドルしているので起こります。
最後に
このガイドにより、あなたはWonderland Engine 1.0.0へのプロジェクトの移行を行うための準備ができました!
何か問題が発生した場合は、Discordサーバーでコミュニティに連絡してください。