概要
銃を連射して大量の弾を発射したり、魔法を使って派手なパーティクルを撒き散らしたりするギミックは、ワールドを楽しくする要素です。しかし、これらのようにオブジェクトを短時間に何度も生成 (Instantiate) し、破棄 (Destroy) する処理は、Unity、特にVRChat環境において深刻なパフォーマンス低下を引き起こす原因となります。
この問題を解決するための古典的かつ非常に効果的な手法がオブジェクトプール (Object Pooling) です。VRChat SDKには、この仕組みを簡単に利用するためのVRCObjectPoolという専用コンポーネントが用意されています。
本記事では、VRCObjectPoolコンポーネントとUdonSharpを連携させ、パフォーマンスに優れたギミックを実装する方法を解説します。
オブジェクトプールの仕組み
オブジェクトプールは、以下のような考え方に基づいています。
- 事前準備 (Pooling): ワールドの開始時に、必要になるであろう数のオブジェクト(例: 弾を100個)をあらかじめ
Instantiateしておき、すべて非表示(SetActive(false))にして「プール(貯蔵庫)」に保管しておく。 - 取り出し (Spawning): オブジェクトが必要になった時(例: 銃を発射した時)、
Destroy&Instantiateするのではなく、プールから待機中のオブジェクトを一つ取り出し、表示(SetActive(true))して使用する。 - 返却 (Returning): オブジェクトが不要になった時(例: 弾が壁に当たった時)、
Destroyするのではなく、再び非表示にしてプールに戻し、次の出番を待たせる。
これにより、高負荷なInstantiateとDestroyの呼び出しをワールド実行中の任意のタイミングで発生させるのを防ぎ、負荷を初期ロード時に集中させることができます。
ステップ1: VRCObjectPoolコンポーネントの設定
- プール管理オブジェクトの作成: シーンに空のGameObjectを作成し、「BulletPool」などと名付けます。
VRCObjectPoolの追加: 「BulletPool」オブジェクトにVRCObjectPoolコンポーネントを追加します。- プール対象オブジェクトの配置:
- プールしたいオブジェクト(例: 弾)を、必要な数だけシーン内に配置します(例: 100個)。
- これらのオブジェクトには、弾としての挙動を制御するUdonSharpスクリプト(後述)、
Rigidbody,Colliderなどをアタッチしておきます。 - すべてのオブジェクトを最初から非アクティブ(
SetActive(false)) にしておきます。
VRCObjectPoolの設定:Pool: シーン内に配置した弾オブジェクトをすべてドラッグ&ドロップして登録します。
重要:
VRCObjectPoolはネットワーク同期されており、TryToSpawn()やReturn()を呼び出せるのはプールオブジェクトの所有者のみです。他のプレイヤーがスポーンを行う場合は、事前にNetworking.SetOwner()で所有権を取得する必要があります。
ステップ2: プールされるオブジェクトのスクリプト
プールから取り出された弾が、どのように振る舞い、どのようにプールへ戻るかを定義するスクリプトです。
スクリプト: PooledBullet.cs
using UdonSharp;
using UnityEngine;
public class PooledBullet : UdonSharpBehaviour
{
[Tooltip("このオブジェクトが所属するVRCObjectPool")]
public VRCObjectPool pool;
public float lifeTime = 5.0f; // 5秒後に自動でプールに戻る
public float speed = 10.0f;
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
}
// オブジェクトがプールから取り出され、有効化された時に呼ばれる
void OnEnable()
{
// ライフタイマーをリセットして、指定時間後にプールに戻る処理を予約
SendCustomEventDelayedSeconds(nameof(ReturnToPool), lifeTime);
// 正面方向に力を加える
if (rb != null)
{
rb.velocity = transform.forward * speed;
}
}
// 壁などに衝突した時に呼ばれる
void OnCollisionEnter(Collision collision)
{
// ここにヒットエフェクトを出す処理などを書く
// プールに戻る
ReturnToPool();
}
// プールにオブジェクトを返すメソッド
public void ReturnToPool()
{
if (pool != null)
{
pool.Return(this.gameObject);
}
else
{
// プールが設定されていない場合は単純に非表示にする
this.gameObject.SetActive(false);
}
}
}
重要なポイント: このスクリプトを、弾のPrefabにアタッチし、Poolフィールドにシーン内の「BulletPool」オブジェクトを割り当てておく必要があります。
ステップ3: オブジェクトを生成する側のスクリプト(銃など)
プレイヤーが操作する銃にアタッチし、プールから弾を取り出して発射する処理を担うスクリプトです。
スクリプト: ObjectPoolGun.cs
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.SDK3.Components;
public class ObjectPoolGun : UdonSharpBehaviour
{
[Tooltip("弾のVRCObjectPool")]
public VRCObjectPool bulletPool;
[Tooltip("弾の発射口となるTransform")]
public Transform muzzle;
// 銃を掴んでいる状態でUseボタンを押した時
public override void OnPickupUseDown()
{
// 自分がこの銃の所有者であることを確認
if (!Networking.IsOwner(this.gameObject)) return;
// プールの所有権を取得(TryToSpawnを呼ぶために必要)
Networking.SetOwner(Networking.LocalPlayer, bulletPool.gameObject);
// プールからオブジェクトを一つ取り出すことを試みる
GameObject bullet = bulletPool.TryToSpawn();
// プールからオブジェクトを取り出せた場合
if (bullet != null)
{
// 弾の位置と向きを発射口に合わせる
bullet.transform.SetPositionAndRotation(muzzle.position, muzzle.rotation);
// これで弾のOnEnableが呼ばれ、弾が飛んでいく
}
else
{
// プールが空だった場合
Debug.LogWarning("弾切れ!プールに利用可能なオブジェクトがありません。");
}
}
}
Unityでの最終設定
- 銃のモデルに
VRCPickupやUdon Behaviourなどを設定します。 ObjectPoolGun.csを銃のUdon Behaviourに割り当てます。Bullet Poolフィールドにシーン内の「BulletPool」オブジェクトを、Muzzleフィールドに銃口となる空のGameObjectを割り当てます。
まとめ
- オブジェクトの頻繁な生成・破棄は高負荷なため、オブジェクトプールを使って再利用するのがパフォーマンス最適化の定石です。
- VRChatでは
VRCObjectPoolコンポーネントを使用します。 - プール管理側 (
VRCObjectPool): シーン内に事前配置したオブジェクトをPool配列に登録します。 - 生成側 (銃など): まず
Networking.SetOwner()でプールの所有権を取得し、pool.TryToSpawn()でオブジェクトを取り出します。戻り値がnullの場合(弾切れ)の考慮が必要です。 - プールされる側 (弾など):
OnEnableで初期化処理(速度設定など)を行い、OnCollisionEnterやタイマーでpool.Return(gameObject)を呼び出して自身をプールに返却します。
オブジェクトプールは、一見すると複雑に見えるかもしれませんが、一度仕組みを理解すれば様々なギミックに応用できる非常に強力なテクニックです。特にパーティクルや射撃系のギミックを実 装する際には、必須の知識と言えるでしょう。