5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MetaHorizonのWorldsDesktopEditorを用いた、Assetについて

Last updated at Posted at 2025-12-17

【Meta Horizon】Asset(アセット)の基本と動的スポーンの実装

この記事では、Meta Horizon Worlds Desktop Editorを用いた開発におけるAsset(アセット)の仕組みと、スクリプトによる動的な生成方法について紹介します。

Assetとは

Meta HorizonにおけるAssetは、UnityにおけるPrefabのような機能です。Entity(オブジェクト)とスクリプトをひとまとめにして保存し、再利用することができます。

スクリーンショット 2025-12-04 165914.png

ワールド内にAssetを配置することで、保存された構成のまま新しいEntityとしてインスタンス化(実体化)できます。

Asset運用の注意点と更新方法

⚠️ スクリプト重複のトラブルについて

スクリプトがアタッチされているAssetを扱う際、以下の条件で問題が発生します。

  • 条件: 既に同名のスクリプトが存在し、かつ更新日時が異なる場合
  • 現象: Script2 のような名前で全く同じ内容の複製スクリプトが自動生成され、新しく設置したEntityにアタッチされてしまう

これにより想定外の挙動(参照切れなど)を引き起こす可能性があります。

対処法:
もしこの状態になってしまった場合は、以下の手順で解消します。

  1. 複製された既存のスクリプトを削除する
  2. 新たに作られたEntityを削除する
  3. 再度、Assetを設置し直す

Assetの更新(上書き保存)

設置済みのAsset(インスタンス)に対して変更を加えた場合、元のAssetに上書き保存が可能です。ただし、更新とみなされる操作にはルールがあります。

  • 更新とみなされる操作: 子Entityに対する操作、アタッチされているスクリプトの更新
  • 更新に含まれない操作: Assetの親(ルート)自体の位置、回転、スケールの変更

スクリーンショット 2025-12-04 171335.png

スクリーンショット 2025-12-04 171347.png

構造を大きく変えたい場合

設置済みのAssetに対して子Entityを追加するなど、構造を大きく変更したい場合は2つのアプローチがあります。

  1. Asset化の解除(Unlink):
    既存のAssetとのリンクを切り、独立したEntityとして扱います。再度Asset化すると、それは別の新しいAssetとなります
    スクリーンショット 2025-12-04 171555.png

  2. Assetそのものを編集:
    Assetの編集モードに入り、Asset自体を更新します。この場合、ワールド内に配置済みのすべてのAssetに対しても変更が適用されます
    スクリーンショット 2025-12-04 171617.png

スクリプトによる動的生成(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を使うなどして管理を行う必要があります。

スクリーンショット 2025-12-04 191351.png

💡 Tips:Asset ID指定による確実な更新と時短

通常は static propsDefinitionPropTypes.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を書き換える必要があります
    • ただし、プロパティ指定の場合でも再アタッチ作業は発生するため、手間の総量はあまり変わりません
5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?