Wonderland Engine 1.0.0 JavaScript Migration

Wonderland Engineは、JavaScriptコードの処理、インタラクション、配布方法に大幅な改善を行っています。

このブログ記事では、それらの変更について説明します。また、Migrationsセクションでは、バージョン1.0.0以前のプロジェクトに対する各新機能の移行手順を詳細に説明します。

モチベーション 

これまで、Wonderland Engineのデフォルトバンドラはローカルスクリプトの連結に依存していました。ユーザーはスクリプトを作成し、エディタがそれを取得して最終的なアプリケーションを作成するためにバンドルします。 外部ライブラリを予めバンドルし、プロジェクトフォルダ構造に配置する必要がありました。

また、NPMプロジェクトをセットアップすることも可能でしたが、セットアップは手動であり、チームのアーティストはNodeJSをセットアップし、依存関係をインストールする手順を踏む必要がありました。

新しいシステムの主な利点:

  • デフォルトでのNPMパッケージ依存
  • IDEからの非常に優れた補完提案
  • TypeScript などの高度なツールとの統合が非常に簡単
  • 他のWebAssemblyライブラリとの互換性
  • ページごとに複数のWonderland Engineインスタンス
  • 非開発チームメンバーのためのNPMプロジェクトの自動管理

お気に入りのツールをシームレスに使用できるJavaScriptエコシステムを構築中です。

エディタコンポーネント 

以前NPMを使用していた場合、以下のようなことがあったかもしれません:

Wonderland Engine 1.0.0 JavaScript Migration

バージョン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": "My Wonderland project",
5  "dependencies": {
6    "@wonderlandengine/components": "^1.0.0-rc.5"
7  }
8}

エディタは、package.json を読み取ることでコンポーネントを見つけ、開発時間を短縮し、共有性を向上させることができます。 詳細については、Writing JavaScript Libraries チュートリアルをご覧ください。

バンドリング 

新しい設定により、バンドリングプロセスを変更できます:

Views > Project Settings > JavaScript > bundlingType

Wonderland Engine 1.0.0 JavaScript Migration

それぞれのオプションを見てみましょう:

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

Wonderland Engine 1.0.0 JavaScript Migration

このスクリプトは、最終アプリケーションバンドルを生成する限り、任意のコマンドを実行できます。

お気に入りのバンドラ、WebpackRollup を使用できます。 しかし、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 という単一のコンポーネントを持つプロジェクトのために自動生成されます。 重要なのは、エディタはシーンで使用されている、つまりオブジェクトにアタッチされたコンポーネントのみをインポートし、登録することです。

もしあなたのアプリケーションがランタイムでのみ使用するコンポーネントを持つ場合、次のことをする必要があります:

  • それらを依存関係としてマークする。Component Dependencies セクションで詳細を参照
  • 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 クラスが付属しています:

1engine.onXRSessionStart.add((session, mode) => {
2    console.log(`Start session '${mode}'!`);
3})

リスナーを識別子を使用して管理することができます:

1engine.onXRSessionStart.add((session, mode) => {
2    console.log(`Start session '${mode}'!`);
3}, {id: 'my-listener'});
4
5// 終了するときは、`id`を使用して単に削除できます。
6engine.onXRSessionStart.remove('my-listener');

詳細については、Emitter API ドキュメントを参照してください。

オブジェクト 

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        comonentAGlobal.init = true;
6    },
7});

component-b.js

1WL.registerComponent('component-b', {}, {
2    init: function() {
3        if(componentAGlobal.init) {
4              console.log('Component A has been initializaed before B!');
5        }
6    },
7});

上記のコードは、componentAGlobal 変数に関していくつかの仮定をしています。それは component-a が最初に登録され、バンドルにプレフィックスが追加されることを期待しています。

これはWonderland Engine内部バンドラがアイソレーションを行わなかったため、以前は機能していました。

1.0.0では、esbuildまたはnpmを使用しても、これでは機能しません。バンドラは component-a で使用される componentAGlobalcomponent-b で使用されるもののリンクを作ることができません。

慣例として: 各ファイルを分離されたものとして考えてください。

移行 

プロジェクトが以前npmを使用していたかどうかに応じて、いくつかの手動の移行手順が必要です。

各セクションでは、以前のセットアップに基づいて必要な適切な手順を説明します。

エディタコンポーネント (#migration-editor-components) 

内部バンドラ 

以前内部バンドラを使用していたユーザー、つまり useInternalBundler チェックボックスが有効になっている:

Views > Project Settings > JavaScript > useInternalBundler

さらなる手順は必要ありません**。

Npm 

npmユーザーに対しては、スクリプトが sourcePaths 設定にリストされていることを保証する必要があります。

ライブラリを使用している場合、それが Writing JavaScript Libraries チュートリアルに従ってWonderland Engine 1.0.0 に移行されていることを確認してください。

依存関係の1つが最新でない場合、node_modules フォルダへのローカルパスを sourcePaths 設定に追加することができます。例:

Wonderland Engine 1.0.0 JavaScript Migration

生成されたバンドルが 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}

これらの例は等価であり、同じ結果につながります。

5行目の違いに注目してください:

1WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));

1this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));

標準バンドリングと npm依存関係 に移行したため、グローバル WL 変数は不要になりました。

グローバルにエンジンを露出させることには次の2つの制限がありました:

  • コンポーネントの共有が難しくなる
  • 複数のエンジンインスタンスを実行できない

2番目のポイントは一般的な使用ケースではありませんが、どのユーザーにもスケーラビリティの制限をかけたくありません。

オブジェクト変換 

新しいAPIは、Wonderland Engine全体で使用される通常のパターンに基づいています:

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

翻訳回転スケーリングのコンポーネントも現在このパターンに従っています:

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

詳細については、Object3D API ドキュメントを参照してください。(TODO:リンクを置き換えます)

JavaScriptアイソレーション 

このセクションはすべてのユーザーに共通で、useInternalBundler が有効かどうかに関わらず同じです。

コンポーネント間でデータを共有する方法は多数あり、アプリケーションの開発者が最も適切な方法を選択します。

ここでは、グローバル変数に依存しないデータ共有方法のいくつかの例を示します。

コンポーネント内の状態 

コンポーネントは、他のコンポーネントによってアクセスされるデータの袋です。

したがって、アプリケーションの状態を保持するためのコンポーネントを作成できます。たとえば、3つの状態があるゲームを作成している場合:

  • 実行中
  • 勝利
  • 敗北

次の形のシングルトンコンポーネントを作成できます:

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 アプリケーションでグローバルを置き換える方法の1つを示しています。

エクスポート 

インポートおよびエクスポートを使用して変数を共有することも可能ですが、オブジェクトはバンドル全体で同一であることに注意してください。

上記の例をエクスポートで再訪してみましょう:

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 に依存します
  • ライブラリ Agamestate バージョン 2.0.0 を依存します

あなたのアプリケーションは2つの gamestate ライブラリのコピーを持つことになります、なぜなら両方がバージョンにおいて互換性がないからです。

ライブラリ AGameState オブジェクトを更新すると、それは実際にはこのエクスポートの自身のインスタンスを変更しています。このことは、両方のバージョンが互換性がないために、アプリケーションが2つの異なるインスタンスのライブラリをバンドルしているので起こります。

終わりの言葉 

このガイドを使って、あなたはWonderland Engine 1.0.0 へのプロジェクト移行を行う準備が整いました!

もし問題が発生した場合は、Discordサーバー でコミュニティに連絡してください。

Last Update: April 14, 2023

最新情報をお届けします。