Wonderland Engine は、JavaScriptコードの処理方法、やり取り方法、および配布方法に関して大幅な改善を遂げています。

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

動機 

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

代わりに、NPMプロジェクトを設定することも可能でしたが、そのセットアップは手動で行われ、チームのアーティストはNodeJSを設定し、依存関係のインストールなどのステップに従う必要がありました。

新しいシステムにはいくつかの利点があります:

  • デフォルトでNPMパッケージ依存関係のサポート
  • IDEからのより優れた補完提案
  • 高度なツール、例えばTypeScriptとの統合が容易に
  • 他のWebAssemblyライブラリとの互換性
  • ページごとの複数のWonderland Engineインスタンス
  • 開発チーム以外のメンバーのためのNPMプロジェクトの自動管理

私たちは、お気に入りのツールをシームレスに使用できるように、新しいJavaScriptエコシステムの開発に取り組んでいます。

エディタコンポーネント 

以前にNPMを使用していた場合、次のような問題に遭遇したかもしれません:

Wonderland Engineでのコンポーネント警告の例

バージョン1.0.0から、エディタはアプリケーションバンドルからコンポーネントタイプを取得しなくなりました。この変更により、エラーが修正されただけでなく、登録するコンポーネントが最終アプリケーションに含まれているかどうかに関わらず、複数のコンポーネントをエディタリスト化することができます。将来的には、ストリーム可能なコンポーネントを用いた複雑なプロジェクトを設定できるようになります。

この変更により、エディタは以下を必要とするようになります:

  • Views > Project Settings > JavaScript > sourcePathsの中にコンポーネントまたはフォルダをリストします。
  • ルートpackage.jsonファイルに依存関係を追加します

ライブラリがコンポーネントを公開しているpackage.jsonの例:

{
  "name": "my-wonderful-project",
  "version": "1.0.0",
  "description": "My Wonderland project",
  "dependencies": {
    "@wonderlandengine/components": "^1.0.0-rc.5"
  }
}

エディタは、package.jsonを読み込むことにより、コンポーネントを見つけることができるようになり、開発時間の短縮共有性の向上を実現します。詳細については、JavaScriptライブラリの作成チュートリアルをご覧ください。

バンドル 

新しい設定により、バンドルプロセスを変更することができます:

Views > Project Settings > JavaScript > bundlingType

Wonderland Engineでのバンドリングタイプの設定

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

esbuild 

あなたのスクリプトはesbuildバンドラーを使ってバンドルされます。

これはデフォルトの選択肢です。パフォーマンスの理由から、可能な限りこの設定を維持することをお勧めします。

npm 

あなたのスクリプトは独自のnpmスクリプトを使ってバンドルされます。

カスタムbuildスクリプトを含むpackage.jsonの例:

{
  "name": "MyWonderfulProject",
  "version": "1.0.0",
  "description": "My Wonderland project",
  "type": "module",
  "module": "js/index.js",
  "scripts": {
    "build": "esbuild ./js/index.js --bundle --format=esm --outfile=\"deploy/MyWonderfulProject-bundle.js\""
  },
  "devDependencies": {
    "esbuild": "^0.15.18"
  }
}

npmスクリプト名はエディターの設定で設定できます:

Views > Project Settings > JavaScript > npmScript

npmスクリプトの選択方法

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

お好みのバンドラーを使うことができます。 例えば、WebpackRollupがそうです。しかし、ユーザーには、イテレーション時間を短縮するためにesbuild のようなツールを使用することをお勧めします。

アプリケーションのエントリーポイント 

コンポーネントの登録はブラウザでの実行時に異なり、実行時に自動的に行われます。

実行時には、エディタがアプリケーションのエントリポイントであるindex.jsファイルの管理を自動的に行います。

エディタは次のようなテンプレートを使用します:

/* wle:auto-imports:start */
/* wle:auto-imports:end */

import {loadRuntime} from '@wonderlandengine/api';

/* wle:auto-constants:start */
/* wle:auto-constants:end */

const engine = await loadRuntime(RuntimeBaseName, {
    physx: WithPhysX,
    loader: WithLoader,
});

// ...

/* wle:auto-register:start */
/* wle:auto-register:end */

engine.scene.load(`${ProjectName}.bin`);

/* wle:auto-benchmark:start */
/* wle:auto-benchmark:end */

このテンプレートは、新しく作成されたプロジェクトと古い1.0.0以前のプロジェクトに自動的にコピーされます。

テンプレートには以下のタグがあります:

  • wle:auto-imports: インポート文が書かれる箇所を示します
  • wle:auto-register: 登録文が書かれる箇所を示します
  • wle:auto-constants: エディタが定数を書き込む箇所、例えば、
    • ProjectName: プロジェクトの.wlpファイルに記載されている名前
    • WithPhysX: 物理エンジンが有効な場合はtrue
    • WithLoader: 実行時にglTFの読み込みをサポートする必要がある場合はtrue

以下は例です:

/* wle:auto-imports:start */
import {Forward} from './forward.js';
/* wle:auto-imports:end */

import {loadRuntime} from '@wonderlandengine/api';

/* wle:auto-constants:start */
const ProjectName = 'MyWonderland';
const RuntimeBaseName = 'WonderlandRuntime';
const WithPhysX = false;
const WithLoader = false;
/* wle:auto-constants:end */

const engine = await loadRuntime(RuntimeBaseName, {
  physx: WithPhysX,
  loader: WithLoader
});

// ...

/* wle:auto-register:start */
engine.registerComponent(Forward);
/* wle:auto-register:end */

engine.scene.load(`${ProjectName}.bin`);

// ...

このインデックスファイルは、js/forward.jsに定義された単一のコンポーネントForwardを持つプロジェクト用に自動生成されています。エディタはシーンで使用されている、すなわちオブジェクトにアタッチされたコンポーネントのみをインポートおよび登録します。

アプリケーションが実行時にのみコンポーネントを使用する場合は、以下のいずれかが必要です:

  • 依存関係としてマークします。詳細はコンポーネント依存関係セクションを参照してください。
  • index.jsファイルで手動でインポートします

単純なアプリケーションでは、このテンプレートファイルで十分で、変更は必要ありません。より複雑な使用例では、タグコメントを削除して独自のindex.jsファイルを作成および管理することができます。

インデックスファイルを手動で管理することで、複数のエントリーポイントを持つアプリケーションを作成したり、エディタが把握していないコンポーネントを登録することが可能になります。

JavaScriptクラス 

Wonderland Engine 1.0.0では、コンポーネントを宣言する新しい方法であるES6 クラスが導入されました。

import {Component, Property} from '@wonderlandengine/api';

class Forward extends Component {
    /* コンポーネントの登録名。 */
    static TypeName = 'forward';
    /* エディタに公開されるプロパティ。 */
    static Properties = {
        speed: Property.float(1.5)
    };

    _forward = new Float32Array(3);

    update(dt) {
        this.object.getForward(this._forward);
        this._forward[0] *= this.speed;
        this._forward[1] *= this.speed;
        this._forward[2] *= this.speed;
        this.object.translate(this._forward);
    }
}

注意点:

  • グローバルなWLシンボルを使用せず、@wonderlandengine/apiからのAPIを使用します。
  • APIのComponentクラスを継承するクラスを作成します。
  • コンポーネントの登録名は静的プロパティです。
  • プロパティはクラス上に設定されます。

JavaScriptプロパティ 

オブジェクトリテラルのプロパティはファンクターに置き換えられました:

import {Component, Property} from '@wonderlandengine/api';

class MyComponent extends MyComponent {
    /* コンポーネントの登録名。 */
    static TypeName = 'forward';
    /* エディタに公開されるプロパティ。 */
    static Properties = {
        myFloat: Property.float(1.0),
        myBool: Property.bool(true),
        myEnum: Property.enum(['first', 'second'], 'second'),
        myMesh: Property.mesh()
    };
}

コンポーネントの依存関係 

依存関係は、あなたのコンポーネントが登録されたときに自動的に登録されるコンポーネントです。

Forward例にSpeedコンポーネントを追加しましょう:

import {Component, Type} from '@wonderlandengine/api';

class Speed extends Component {
    static TypeName = 'speed';
    static Properties = {
        value: Property.float(1.5)
    };
}

class Forward extends Component {
    static TypeName = 'forward';
    static Dependencies = [Speed];

    _forward = new Float32Array(3);

    start() {
      this._speed = this.object.addComponent(Speed);
    }

    update(dt) {
        this.object.getForward(this._forward);
        this._forward[0] *= this._speed.value;
        this._forward[1] *= this._speed.value;
        this._forward[2] *= this._speed.value;
        this.object.translate(this._forward);
    }
}

Forwardの登録時に、依存関係としてリストされているためSpeedが自動的に登録されます。

この動作は、index.jsで作成されたWonderlandEngineオブジェクトのautoRegisterDependenciesブール値によって管理されます。

イベント 

Wonderland Engineは以前、配列のリスナーを使用してイベントを処理していました。例:

  • WL.onSceneLoaded
  • WL.onXRSessionStart
  • WL.scene.onPreRender
  • WL.scene.onPreRender

エンジンには、イベントとのやり取りを簡素化するためにEmitterクラスが追加されました:

engine.onXRSessionStart.add((session, mode) => {
    console.log(`セッション '${mode}' 開始!`);
})

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

engine.onXRSessionStart.add((session, mode) => {
    console.log(`セッション '${mode}' 開始!`);
}, {id: 'my-listener'});

// 終わったら、`id`を使って削除できます。
engine.onXRSessionStart.remove('my-listener');

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

オブジェクト 

この改良全体に伴いObjectも変更を受けました。APIがより一貫性を持ち、安全性が向上しました。

エクスポート名 

Objectクラスは、JavaScriptのObjectコンストラクタを上書きしないように、Object3D としてエクスポートされます。

移行を円滑にするために、Objectも引き続きエクスポートされますが、今後のマイグレーションを容易にするために、次のようにしてください:

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

トランスフォーム 

トランスフォーメーションAPIも変更されました。エンジンは、トランスフォーメーションのためのゲッターとセッター(アクセサー)の使用を非推奨としています:

this.object.translationLocal;
this.object.translationWorld;
this.object.rotationLocal;
this.object.rotationWorld;
this.object.scalingLocal;
this.object.scalingWorld;
this.object.transformLocal;
this.object.transformWorld;

これらのゲッター/セッターにはいくつかの欠点があります:

  • 一貫性: 他のトランスフォーメーションAPIに従っていない
  • パフォーマンス: 各呼び出しごとにFloat32Arrayを割り当て
  • 安全性: メモリビューが他のコンポーネントによって変更される可能性あり
    • 後に読み取るためのFloat32Array参照を格納する際の潜在的なバグ

新しいAPIが気になる方は、オブジェクトのトランスフォーメーションセクションをご覧ください。

JavaScriptの分離 

内部バンドラーを使用していたユーザーは、次のようなコードを見かけたことがあるかもしれません:

component-a.js

var componentAGlobal = {};

WL.registerComponent('component-a', {}, {
    init: function() {
        componentAGlobal.init = true;
    },
});

component-b.js

WL.registerComponent('component-b', {}, {
    init: function() {
        if(componentAGlobal.init) {
            console.log('Component AがBの前に初期化されました!');
        }
    },
});

上記のコードはcomponentAGlobal変数に関する仮定を行っています。それはcomponent-aが最初に登録され、バンドルで先頭にあることを期待しています。

これは、Wonderland Engineの内部バンドラーが分離を行っていなかったために機能しました。

しかし、1.0.0では、esbuildまたはnpmを使用している場合、これも機能しなくなります。バンドラーはcomponent-aで使用されているcomponentAGlobal と、component-bで使用されているcomponentAGlobal の間のリンクを確立することができません。

バンドラーを使用する場合は、各ファイルを分離されたものとして考えることをお勧めします。

マイグレーション 

手動でのマイグレーション手順が必要で、その手順はプロジェクトが以前npmを使用していたかどうかによります。

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

エディタコンポーネント 

内部バンドラー 

内部バンドラーを使用していた、つまりuseInternalBundlerチェックボックスが 有効化されている場合:

Views > Project Settings > JavaScript > useInternalBundler

追加ステップはありません

Npm 

npmユーザーは、自分のスクリプトがsourcePaths設定にリストされていることを確認する必要があります。

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

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

Wonderland Engineでのコンポーネント警告の例

生成されたバンドルがnpmesbuildを経由して使われなくなることを常に心に留めておいてください。それは単にアプリケーションが実行される際に使用されるのです。

バンドリング 

追加ステップはありません。プロジェクトは自動的に移行されるべきです。

JavaScriptクラス、プロパティ、イベント 

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

旧方式と新方式を比較するコードを見てみましょう:

1.0.0 前

WL.registerComponent('player-height', {
    height: {type: WL.Type.Float, default: 1.75}
}, {
    init: function() {
        WL.onXRSessionStart.push(this.onXRSessionStart.bind(this));
        WL.onXRSessionEnd.push(this.onXRSessionEnd.bind(this));
    },
    start: function() {
        this.object.resetTranslationRotation();
        this.object.translate([0.0, this.height, 0.0]);
    },
    onXRSessionStart: function() {
        if(!['local', 'viewer'].includes(WebXR.refSpace)) {
            this.object.resetTranslationRotation();
        }
    },
    onXRSessionEnd: function() {
        if(!['local', 'viewer'].includes(WebXR.refSpace)) {
            this.object.resetTranslationRotation();
            this.object.translate([0.0, this.height, 0.0]);
        }
    }
});

1.0.0 後

/* npm 依存関係を使用することを忘れないでください */
import {Component, Property} from '@wonderlandengine/api';

export class PlayerHeight extends Component {
    static TypeName = 'player-height';
    static Properties = {
        height: Property.float(1.75)
    };

    init() {
        /* Wonderland Engine 1.0.0では、グローバルインスタンスの使用をやめました。
         * 現在のエンジンインスタンスには`this.engine`を介してアクセスできます。 */
        this.engine.onXRSessionStart.add(this.onXRSessionStart.bind(this));
        this.engine.onXRSessionEnd.add(this.onXRSessionEnd.bind(this));
    }
    start() {
        this.object.resetTranslationRotation();
        this.object.translate([0.0, this.height, 0.0]);
    }
    onXRSessionStart() {
        if(!['local', 'viewer'].includes(WebXR.refSpace)) {
            this.object.resetTranslationRotation();
        }
    }
    onXRSessionEnd() {
        if(!['local', 'viewer'].includes(WebXR.refSpace)) {
            this.object.resetTranslationRotation();
            this.object.translate([0.0, this.height, 0.0]);
        }
    }
}

これらの例は同等で、同じ結果を生み出します。

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

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

それに対して

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

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

グローバルに公開されたエンジンは以下の2つの制限をもたらしていました:

  • コンポーネントの共有が困難
  • 複数のエンジンインスタンスを実行することが不可能

2番目の箇条書きは一般的なユースケースではありませんが、スケーラビリティの観点からユーザーを制限したくありません。

オブジェクトのトランスフォーム 

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

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

トランスレーションローテーションスケーリングのコンポーネントもこのパターンに従います:

const translation = this.object.getTranslationLocal();
this.object.setTranslationLocal([1, 2, 3]);

const rot = this.object.getRotationLocal();
this.object.setRotationLocal([1, 0, 0, 1]);

const scaling = this.object.getScalingLocal();
this.object.setScalingLocal([2, 2, 2]);

他のAPIと同様、ゲッター用の空のoutパラメータを使用すると、出力配列が作成されます。可能な限り配列を再利用することが(パフォーマンスの理由から)常に推奨されます。

オブジェクトのローカル空間の変換を読み書きできるだけでなく、ワールド空間で直接操作することもできます:

const translation = this.object.getTranslationWorld();
this.object.setTranslationWorld([1, 2, 3]);

const rot = this.object.getRotationWorld();
this.object.setRotationWorld([1, 0, 0, 1]);

const scaling = this.object.getScalingWorld();
this.object.setScalingWorld([2, 2, 2]);

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

JavaScriptの分離 

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

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

以下にグローバル変数に依存しないデータ共有の例をいくつか示します。

コンポーネント内のステート 

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

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

  • 実行中
  • 勝利
  • 敗北

以下の形でシングルトンコンポーネントを作成することができます:

game-state.js

import {Component, Type} from '@wonderlandengine/api';

export class GameState extends Component {
  static TypeName = 'game-state';
  static Properties = {
    state: {
      type: Type.Enum,
      values: ['running', 'won', 'lost'],
      default: 0
    }
  };
}

GameStateコンポーネントは、次に状態を変更するためのマネージャーオブジェクトに追加されます。このオブジェクトは、ゲームの状態を変更するコンポーネントによって参照されなければなりません。

プレイヤーの死亡時にゲームの状態を変更するコンポーネントを作成しましょう:

player-health.js

import {Component, Type}from '@wonderlandengine/api';

import {GameState} from './game-state.js';

export class PlayerHealth extends Component {
  static TypeName = 'player-health';
  static Properties = {
    manager: {type: Type.Object},
    health: {type: Type.Float, default: 100.0}
  };
  update(dt) {
    /* プレイヤーが死亡した場合、状態を変更します。 */
    if(this.health <= 0.0) {
      const gameState = this.manager.getComponent(GameState);
      gameState.state = 2; // 状態を`lost`に設定します。
    }
  }
}

これは、プレ1.0.0アプリケーションのグローバルを置き換える方法の1つを示しています。

エクスポート 

importexportを介して変数を共有することも可能です。しかし、オブジェクトがバンドル全体で同一になることを忘れないでください。

上記の例をエクスポートを用いて再考します:

game-state.js

export const GameState = {
  state: 'running'
};

player-health.js

import {Component, Type} from '@wonderlandengine/api';

import {GameState} from './game-state.js';

export class PlayerHealth extends Component {
  static TypeName = 'player-health';
  static Properties = {
    manager: {type: Type.Object},
    health: {type: Type.Float, default: 100.0}
  };
  update(dt) {
    if(this.health <= 0.0) {
      GameState.state = 'lost';
    }
  }
}

この解決策は機能しますが、万全ではありません。

これがうまく機能しない例を見てみましょう。このコードがgamestateというライブラリにあると仮定します。

  • あなたのアプリケーションはgamestateバージョン1.0.0に依存しています
  • あなたのアプリケーションはライブラリAに依存しています
  • ライブラリAgamestateバージョン2.0.0に依存しています

あなたのアプリケーションはgamestateライブラリを2つのコピーで終わることになります。これらのバージョンが互換性がないためです。

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

最終的なまとめ 

このガイドで、Wonderland Engine 1.0.0に向けてプロジェクトをマイグレーションする準備が整いました!

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

Last Update: March 27, 2025

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