【Meta Horizon】Asset(アセット)の基本と動的スポーンの実装
この記事では、Meta Horizon Worlds Desktop Editorを用いた開発におけるAsset(アセット)の仕組みと、スクリプトによる動的な生成方法について紹介します。
Assetとは
Meta HorizonにおけるAssetは、UnityにおけるPrefabのような機能です。Entity(オブジェクト)とスクリプトをひとまとめにして保存し、再利用することができます。
ワールド内にAssetを配置することで、保存された構成のまま新しいEntityとしてインスタンス化(実体化)できます。
Asset運用の注意点と更新方法
⚠️ スクリプト重複のトラブルについて
スクリプトがアタッチされているAssetを扱う際、以下の条件で問題が発生します。
- 条件: 既に同名のスクリプトが存在し、かつ更新日時が異なる場合
-
現象:
Script2のような名前で全く同じ内容の複製スクリプトが自動生成され、新しく設置したEntityにアタッチされてしまう
これにより想定外の挙動(参照切れなど)を引き起こす可能性があります。
対処法:
もしこの状態になってしまった場合は、以下の手順で解消します。
- 複製された既存のスクリプトを削除する
- 新たに作られたEntityを削除する
- 再度、Assetを設置し直す
Assetの更新(上書き保存)
設置済みのAsset(インスタンス)に対して変更を加えた場合、元のAssetに上書き保存が可能です。ただし、更新とみなされる操作にはルールがあります。
- 更新とみなされる操作: 子Entityに対する操作、アタッチされているスクリプトの更新
- 更新に含まれない操作: Assetの親(ルート)自体の位置、回転、スケールの変更
構造を大きく変えたい場合
設置済みのAssetに対して子Entityを追加するなど、構造を大きく変更したい場合は2つのアプローチがあります。
-
Asset化の解除(Unlink):
既存のAssetとのリンクを切り、独立したEntityとして扱います。再度Asset化すると、それは別の新しいAssetとなります

-
Assetそのものを編集:
Assetの編集モードに入り、Asset自体を更新します。この場合、ワールド内に配置済みのすべてのAssetに対しても変更が適用されます

スクリプトによる動的生成(spawnAsset)
spawnAsset メソッドを使用することで、作成済みのAssetをスクリプトから動的にワールドへ追加できます。
パフォーマンスについての注意:
スポーン処理は負荷が高いため、頻繁に呼び出すことは推奨されません。
弾薬や敵キャラなど、生成と削除を繰り返すものは「オブジェクトプーリング(予め生成しておき、表示・非表示で使い回す手法)」の利用を推奨します。
実装例:プレイヤー入室時にAssetを生成し、所有権を渡す
以下は、プレイヤーが入室した際にAssetをランダムな位置に生成し、そのプレイヤーに所有権を渡す(ローカルスクリプトを動作させるための準備)サーバーサイドスクリプトの例です。
static propsDefinition = {
localAsset: { type: hz.PropTypes.Asset }, // スポーンさせたいAssetを指定
};
// プレイヤーID (number) と、そのプレイヤー用に生成したルートエンティティを紐付けるMap
private playerEntities = new Map<number, hz.Entity>();
preStart() {
// プレイヤーが入室したイベントを検知(サーバーで実行)
this.connectCodeBlockEvent(
this.entity,
hz.CodeBlockEvents.OnPlayerEnterWorld,
(player) => this.onPlayerJoin(player)
);
// プレイヤーが退出したイベントを検知(お掃除用)
this.connectCodeBlockEvent(
this.entity,
hz.CodeBlockEvents.OnPlayerExitWorld,
(player) => this.onPlayerExit(player)
);
}
async onPlayerJoin(player: hz.Player) {
if (!this.props.localAsset) return;
// --- ランダムなスポーン位置の計算 ---
const randomX = (Math.random() * 20) - 10; // X: -10 から 10
const randomZ = (Math.random() * 3) - 2; // Z: -2 から 1
const spawnPos = new hz.Vec3(randomX, 1, randomZ);
// 1. Assetを指定したランダム位置にスポーンする(まだ所有者はServer)
const spawnedEntities = await this.world.spawnAsset(
this.props.localAsset,
spawnPos
);
// 非同期処理中にプレイヤーがいなくなった場合のガード処理
if (!player.isValidReference.get()) {
// 生成したものを即座に削除して終了
this.world.deleteAsset(spawnedEntities[0], true);
return;
}
// 2. スポーンされたルートエンティティを取得
const rootEntity = spawnedEntities[0];
// --- Mapで管理 ---
this.playerEntities.set(player.id, rootEntity);
// 3. 【重要】再帰的に所有権をそのプレイヤーに移譲する
// これを行わないと、Asset内のローカルスクリプトがプレイヤー端末で動作しません
this.setOwnershipRecursive(rootEntity, player);
}
// 再帰的に所有権を変更するヘルパー関数
setOwnershipRecursive(targetEntity: hz.Entity, newOwner: hz.Player) {
targetEntity.owner.set(newOwner);
const children = targetEntity.children.get();
children.forEach(child => {
this.setOwnershipRecursive(child, newOwner);
});
}
onPlayerExit(player: hz.Player) {
// MapからそのプレイヤーIDに紐付いたエンティティを取得
const entity = this.playerEntities.get(player.id);
if (entity) {
// fullDelete: true を指定して、アセットに含まれる子エンティティも全て削除します
this.world.deleteAsset(entity, true);
// Mapからエントリーを削除してメモリを解放
this.playerEntities.delete(player.id);
}
}
実装例:スポーンされるAsset側のスクリプト
スポーンされ、所有権を受け取った側のAssetにアタッチしておくローカルスクリプトの例です。 自身の所有者(Owner)の名前を表示するUIを生成します。
private messageBinding = new Binding("LocalScript New UI Panel");
initializeUI(): UINode {
return View({
children: [
Text({
text: this.messageBinding,
style: {
fontSize: 48,
textAlign: 'center',
textAlignVertical: 'center',
height: this.panelHeight,
width: this.panelWidth,
}
})
],
style: {
backgroundColor: 'black',
height: this.panelHeight,
width: this.panelWidth,
}
});
}
start() {
// 所有権が正しく移譲されていれば、このスクリプトは各プレイヤー端末で実行され、
// ownerはそのプレイヤー自身になっています。
const owner: Player = this.entity.owner.get();
const ownerName = owner.name.get();
this.messageBinding.set(`LocalScript New UI Panel\nOwner: ${ownerName}`);
}
Asset元は非表示化してあります
カスタムUIが表示されている方は自分が所有しているLocalScriptエンティティとなっています。
また、spawnAssetしたエンティティの名前は変更することができないため、
管理を行う場合は別途Mapを使うなどして管理を行う必要があります。
💡 Tips:Asset ID指定による確実な更新と時短
通常は static propsDefinition で PropTypes.Asset を定義し、エディタのプロパティ画面でAssetをドラッグ&ドロップして指定しますが、開発中にAssetの中身を更新しても、スクリプト側が古いキャッシュ(古いバージョンのAsset)を参照し続けてしまうことがあります。
これを回避し、常に最新のAsset内容を確実にスポーンさせる方法として、Asset IDを直接指定するテクニックがあります。
この方法を使うと、Assetを更新するたびにプロパティ欄で「×ボタンで削除 → 再度ドラッグ&ドロップ」という再アタッチの手間を省くことができ、スムーズな開発が可能です。
実装コード例
AssetのID(BigInt)を直接コンストラクタに渡してインスタンス化します。
// IDから直接Assetオブジェクトを生成してスポーン
// ※IDはAssetライブラリで対象Assetの「...」メニュー等からコピーできます
const assetId = BigInt("1234567890123456");
const newAsset = new hz.Asset(assetId);
const spawnedEntities = await this.world.spawnAsset(
newAsset,
spawnPos
);
メリット:
- Asset更新時の再アタッチ作業が不要になる。
- エディタの同期ズレによる「更新したはずなのに反映されない」トラブルを防げる。
注意点:
- Asset自体を作り直して(削除して新規作成して)IDが変わった場合は、コード内のIDを書き換える必要があります
- ただし、プロパティ指定の場合でも再アタッチ作業は発生するため、手間の総量はあまり変わりません



