【VRChat】VRCObjectPool入門:3個までのボールを貸し出して、返してもらう

作成: 2025-12-19最終更新: 2026-09-09

実行中にオブジェクトを増やす代わりに、あらかじめ置いた3個を出し入れします。在庫係への依頼、在庫切れの扱い、返すときに位置を戻す理由を解説します。

来場者にボールを配りたい。押すたびに1個出てきて、遊び終わったら棚に戻る。

Unityの感覚なら Instantiate で作りたくなるところですが、VRChatでは その場で作った物は全員には見えません。 同期して共有したい物は、あらかじめシーンに置いておく必要があります。

この記事では、3個のボールを貸し出して、返してもらう棚 を作ります。

棚から出た1個のボールと、棚に残る2個

この記事でわかること

  • 増やすのではなく、隠してある物を出すという考え方
  • 出し入れできるのは所有者1人だけであること
  • 在庫切れの伝え方
  • 返すときに位置を戻す理由

所有権を理解するNetwork Events を試した状態から始めます。

Sponsored


増やすのではなく、隠してある物を出す

VRCObjectPool は、あらかじめ用意しておいた物を 有効と無効で切り替える 部品です。新しく作っているわけではありません。

3個を用意し、出しては戻して使い回す

倉庫にボールが3個あると考えてください。棚から出す、棚に戻す。4個目は存在しません。

操作は2つだけです。

メソッドすること
TryToSpawn()まだ出ていない物を1つ有効にして返す。空きがなければ null
Return(対象)その物を無効に戻す。次の TryToSpawn() でまた使える

この仕組みには、ありがたい副作用が3つあります。

  • 数が勝手に増えない: 「3個まで」が仕組みとして守られます。来場者が押し続けても、部屋がボールで埋まりません
  • 重くならない: 作っては消すを繰り返すと、そのたびに一瞬引っかかります。使い回せば起きません
  • 途中参加に強い: いま何が出ているかはVRChatが同期してくれるので、あとから入った人にも同じ景色が見えます

SetActive を自分で呼びたくなりますが、それはしません。 自分で切り替えると、プールが把握している状態と食い違って、人によって見え方が変わります。出し入れはプールに任せます。

Sponsored

出し入れするのは、在庫係1人

ここで所有権の話が効いてきます。

TryToSpawn()Return() が実際に効くのは、プールの所有者だけ です。所有者以外が呼ぶと null が返って、何も起きません。エラーも出ません。

押した人が出すのではなく、在庫係へ依頼する

つまり、こういう役割分担になります。

ボタンを押した人が出すのではなく、部屋の在庫係が出す。

来場者は「出してください」と頼むだけです。頼み方は Network Events で使った NetworkEventTarget.Owner そのものです。

// 押した人がやること:所有者に依頼を出す
SendCustomNetworkEvent(NetworkEventTarget.Owner, nameof(LendBall));

なぜ「押した人が所有権を取ってから出す」ではないのか。2人が同時に押したときに、4個目が生まれないため です。窓口が1つなら、依頼は順番に処理されます。

もう1つ注意があります。プールの所有者と、ボールの所有者は別です。 在庫係は棚の担当で、出たボールを持って走っている人はそのボールの担当です。役割が違います。

実践:3個のボール貸出機を作る

押すとボールが1個出て、投げて遊べて、返却ボタンで全部棚に戻る仕掛けを作ります。

1. ボールと棚を置く

床のあるシーンに、次を置きます。

名前作り方と設定
PoolRootCreate Empty。(0, 0, 0)
Ball1 Ball2 Ball3Sphere。PoolRoot の子。(0, 1.2, 2) (0.3, 1.2, 2) (0.6, 1.2, 2)、Scale (0.2, 0.2, 0.2)
LendButtonCube。(-1, 1, 1)、Scale (0.4, 0.4, 0.4)
ReturnButtonCube。(-1, 0.5, 1)、Scale (0.4, 0.4, 0.4)
StockCanvasUI Canvas(World Space)。(0, 1.8, 2)、Scale (0.005, 0.005, 0.005)

StockCanvas の子に TextMeshPro を作り、StockText と名付けます。残りの数をここに出します。

床の上に、棚・ボール3個・ボタン2つ・掲示を配置した図

3つのボールには、それぞれ次を追加します。

  1. VRC Pickup(Rigidbodyも一緒に付きます)
  2. VRC Object Sync

投げて遊べて、位置が共有される状態です。VRC Object Sync と同じ構成です。

3つとも、Inspectorの左上のチェックを外して 無効にしておきます。 出すのはプールの役目です。

PoolRoot に「Add Component」で VRC Object Pool を追加し、Pool配列のSizeを 3 にして、Ball1 から Ball3 をドラッグします。

親子関係と、それぞれに付ける部品

2. コードを書く

Assets/Scripts で「Create → U# Script」を選び、BallDispenser を作ります。

using UdonSharp;
using UnityEngine;
using TMPro;
using VRC.SDK3.Components;
using VRC.SDKBase;
using VRC.Udon.Common.Interfaces;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class BallDispenser : UdonSharpBehaviour
{
    [SerializeField] private VRCObjectPool pool;
    [SerializeField] private TextMeshProUGUI stockText;

    [UdonSynced] private int remaining;

    private void Start()
    {
        if (Networking.IsOwner(gameObject) && pool != null)
        {
            remaining = pool.Pool.Length;
        }
        ApplyState();
    }

    // 貸出ボタンから呼ぶ。誰が押しても動く
    public void RequestLend()
    {
        SendCustomNetworkEvent(NetworkEventTarget.Owner, nameof(LendBall));
    }

    // 返却ボタンから呼ぶ
    public void RequestReturnAll()
    {
        SendCustomNetworkEvent(NetworkEventTarget.Owner, nameof(ReturnAll));
    }

    // ここから先は、在庫係(プールの所有者)だけが実行する
    [NetworkCallable]
    public void LendBall()
    {
        if (!Networking.IsOwner(gameObject)) return;

        GameObject ball = pool.TryToSpawn();
        if (ball == null)
        {
            Debug.Log("[BallDispenser] 在庫がありません");
            return;
        }

        remaining--;
        ApplyState();
        RequestSerialization();
    }

    [NetworkCallable]
    public void ReturnAll()
    {
        if (!Networking.IsOwner(gameObject)) return;

        GameObject[] balls = pool.Pool;
        for (int i = 0; i < balls.Length; i++)
        {
            GameObject ball = balls[i];
            if (ball == null || !ball.activeSelf) continue;

            // 位置を棚へ戻してから、しまう
            VRCObjectSync sync = ball.GetComponent<VRCObjectSync>();
            if (sync != null) sync.Respawn();

            pool.Return(ball);
        }

        remaining = balls.Length;
        ApplyState();
        RequestSerialization();
    }

    public override void OnDeserialization()
    {
        ApplyState();
    }

    private void ApplyState()
    {
        if (stockText != null) stockText.text = "残り " + remaining + " 個";
    }
}

見どころは3つあります。

TryToSpawn() の戻り値を必ず見ます。 null は「在庫切れ」の合図です。ここを見ないと、出ていないのに残り数だけ減っていきます。

返す前に Respawn() を呼んでいます。 Return() は非表示にするだけで、位置は投げられた場所のままです。戻さないと、次に出したときに部屋の隅や床下から現れます。

残り数は同期変数で持っています。 プール自体の状態はVRChatが同期してくれますが、「残り何個」という表示は自分で用意します。ネットワーク同期入門 と同じ形です。

3. ボタンをつなぐ

PoolRoot に Udon Behaviour を追加し、BallDispenser を指定します。Poolに PoolRoot 自身を、Stock Textに StockText をドラッグします。

Hierarchyから欄へドラッグして割り当てる

2つのボタンには、短い中継のスクリプトを付けます。

using UdonSharp;

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class DispenserButton : UdonSharpBehaviour
{
    [SerializeField] private BallDispenser dispenser;
    [SerializeField] private bool returnMode;   // true なら返却

    public override void Interact()
    {
        if (dispenser == null) return;
        if (returnMode) dispenser.RequestReturnAll();
        else dispenser.RequestLend();
    }
}

LendButton は Return Mode をオフ、ReturnButton はオンにします。Interaction Text も ボールを借りる 全部返す にしておきます。

4. 2人で確かめる

Build & Test で Number of Clients を 2 にして起動します。

順序操作期待する結果
1AとBが入る掲示に「残り 3 個」。ボールは見えない
2Aが貸出ボタンを押す両方の画面でボールが1個現れ、「残り 2 個」になる
3Aがボールを掴んで投げるBの画面でも同じように飛ぶ
4さらに3回押す3個目まで出たあと、4回目は何も起きない。「残り 0 個」
5返却ボタンを押す3個とも消えて、「残り 3 個」に戻る
6Bが入り直すいま出ている数がそのまま見える

4番目が、この記事でいちばん大事な確認です。 在庫がなくなると、押しても静かに何も起きません。エラーは出ません。

実際のワールドでは、ここで「いま貸出中です」と掲示に出すか、ボタン自体を押せなくします。押しても反応がない状態を、そのまま残さない のが親切です。

Sponsored

うまくいかないときは

  • 押しても出ない → 所有者以外が TryToSpawn() を直接呼んでいます。NetworkEventTarget.Owner で依頼する形にします
  • 自分だけ出る → ボールを自分で SetActive しています。出し入れはプールに任せます
  • 人によって出ている数が違う → 同じく、プールの外で有効・無効を触っています
  • 返したあと、変な場所から出てくるReturn() の前に Respawn() を呼んでいません
  • 残り数が合わないTryToSpawn()null を返したときにも減らしています
  • 依頼が届かない[NetworkCallable] が付いているか、メソッドが public か確認します

おまけ:先に知っておくと良いこと

  • 数は最初に決まる: あとから増やせません。想定人数から「同時に何個まで出るか」を決めて、Pool配列に並べておきます
  • 出す物の中身もリセットする: 位置だけでなく、点灯や色を変えていたら、それも初期状態に戻します。前の人の状態が残ったまま次の人に渡ると、気持ちが悪いものです
  • 在庫係は退出しても大丈夫: 所有者が抜けると、VRChatが別の人に割り当てます。残った誰かが在庫係を引き継ぐので、貸出は止まりません
  • 手に直接渡すのは一段むずかしい: 「押した人の手に出す」には、誰が頼んだかを所有者へ伝える必要があります。棚に出して自分で拾ってもらうほうが、ずっと簡単で確実です
  • 使いどころはいろいろ: 貸し出しの道具、打ち上げる花火、シューティングの弾、イベントの名札。どれも「同時に出る数が決まっている物」です

まとめ

VRCObjectPoolは、用意しておいた物を出し入れする棚です。

  • 実行中に増やすのではなく、あらかじめ置いた物を有効・無効で切り替える
  • 出し入れが効くのは所有者だけ。押した人は所有者へ依頼する
  • TryToSpawn()null を返したら在庫切れ。その場合の見せ方まで作る
  • 返す前に位置を戻す。Return() は非表示にするだけ

使う前の問いかけは、「これは、同時に何個まで出ればいいか」 です。数が決まるなら、プールの出番です。

各自の設定を次回まで残すなら PlayerDataで音量を保存する、動画を流す仕掛けなら 動画プレイヤーを設置する へ進んでください。

VRChat このセクションのノート63