【VRChat】同期するサイコロ:乱数は所有者が決めて配る

作成: 2026-09-09

押すと全員に同じ目が出るサイコロを作ります。各自が乱数を引くと食い違う理由と、所有者が1回だけ決めて配るという考え方を、実際に作りながら確かめます。

ボードゲームのワールドを作っています。サイコロを置いて、押すと目が出るようにしました。

自分の画面では動いています。友達と入ってみると、自分の画面では3、相手の画面では5 が出ています。同じサイコロを見ているはずなのに、目が違います。

原因は単純です。それぞれのパソコンが、それぞれサイコロを振っている からです。この記事では、全員に同じ目が出るサイコロ を作ります。

1人が振ったサイコ��ロの目が、全員の画面で同じになる

この記事でわかること

  • 各自で乱数を引くと食い違う理由
  • 所有者が決めて配るという考え方
  • 途中から来た人にも目が伝わること
  • 転がる演出との組み合わせ方

所有権を理解する を読んだ状態から始めます。

Sponsored


振るのは1人でいい

まず、うまくいかない書き方を見ます。

// これでは食い違う
public override void Interact()
{
    int value = Random.Range(1, 7);
    diceText.text = value.ToString();
}

このコードは、押した人の画面でしか動きません。 しかも、仮に全員で実行したとしても、それぞれ違う数が出ます。

全員が振ると別々の目が出る。1人が振って配れば同じになる

乱数は、呼ぶたびに違う数を返します。 だから、複数のパソコンで別々に呼べば、当然ばらばらになります。

正しい形は、現実のボードゲームと同じです。

サイコロを振るのは1人。出た目を、みんなで見る。

押す・所有権を取る・1回振る・全員へ配る

コードにすると、こうなります。

  1. 押した人が 所有権を取る
  2. 所有者が Random.Range()1回だけ 呼ぶ
  3. その結果を 同期変数 に入れて配る
  4. 受け取った各自が、その数字を表示する

これは新しい仕組みではありません。 ネットワーク同期入門 の照明スイッチと、まったく同じ形です。持っている値が真偽から整数に変わっただけです。

「決めるのは1人、配るのは仕組み」 という形は、乱数以外でも使えます。この記事のいちばんの持ち帰りはここです。

Sponsored

実践:全員に同じ目が出るサイコロ

押すと1から6の目が出て、全員の画面でそろうサイコロを作ります。

1. サイコロと表示を置く

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

名前作り方と設定
DiceCube。(0, 1, 2)、Scale (0.3, 0.3, 0.3)
DiceCanvasUI Canvas(World Space)。(0, 1.6, 2)、Scale (0.005, 0.005, 0.005)
DiceTextDiceCanvas の子に TextMeshPro。大きめの文字、中央そろえ
サイコロと目を出す掲示の配置

DiceText には、あらかじめ - と入れておきます。まだ振っていない状態です。

2. コードを書く

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

using UdonSharp;
using UnityEngine;
using TMPro;
using VRC.SDKBase;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class SyncedDice : UdonSharpBehaviour
{
    [SerializeField] private TextMeshProUGUI diceText;
    [SerializeField] private float cooldownSeconds = 1f;

    [UdonSynced] private int diceValue;   // 全員で共有する目

    private float nextAllowedTime;

    private void Start()
    {
        ApplyState();
    }

    public override void Interact()
    {
        // 連打を弾く
        if (Time.time < nextAllowedTime) return;
        nextAllowedTime = Time.time + cooldownSeconds;

        if (!Utilities.IsValid(Networking.LocalPlayer)) return;

        // 1. 振る人になる
        if (!Networking.IsOwner(gameObject))
        {
            Networking.SetOwner(Networking.LocalPlayer, gameObject);
        }
        if (!Networking.IsOwner(gameObject)) return;

        // 2. 1回だけ振る。ここが唯一の乱数
        diceValue = Random.Range(1, 7);   // 上限は含まれないので 7

        // 3. 自分の画面に反映して、送信を頼む
        ApplyState();
        RequestSerialization();

        Debug.Log("[SyncedDice] " + diceValue);
    }

    // 値が届いたときに呼ばれる
    public override void OnDeserialization()
    {
        ApplyState();
    }

    // 値を見て、表示を作る
    private void ApplyState()
    {
        if (diceText == null) return;
        diceText.text = diceValue == 0 ? "-" : diceValue.ToString();
    }
}

見どころは3つです。

Random.Range() を呼んでいるのは1か所だけ です。しかも所有権を確かめたあとにあります。他の人のパソコンでは、この行は動きません。

Random.Range(1, 7)7 に注目してください。整数版は上限が含まれないので、6 と書くと5までしか出ません。全員が一度は踏むところです。

構造は同期入門とまったく同じです。 所有権を取って、値を変えて、ApplyState() して、RequestSerialization()。受け取る側は OnDeserialization() から同じ ApplyState() を呼びます。

3. 割り当てて確かめる

Dice に Udon Behaviour を追加して、SyncedDice を指定します。Dice Textに DiceText をドラッグし、Interaction Text を サイコロを振る にします。

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

順序操作期待する結果
1AとBが入るどちらの画面も -
2Aが振る両方の画面に、同じ数字が出る
3Bが振る両方の画面が、同じ新しい数字に変わる
4Aが連打する1秒に1回しか変わらない
5Bが退出して入り直す最後に出た目が、そのまま見えている
6何度も振る1から6が出る。7や0は出ない

2番目と5番目が、この記事の答え合わせです。

2番目では、Bは何もしていないのに同じ目を見ています。振ったのはAだけ だからです。

5番目では、途中から来たBに最後の目が届いています。同期変数を使っているので、あとから入った人にも最新の値が配られます。

6番目で、7 が出ないことも確かめてください。Random.Range(1, 7) が正しく書けている証拠です。

転がる演出を足す

数字がぱっと切り替わるだけでは味気ありません。転がるアニメーションを足したくなります。

ここで、順番を取り違えないでください。

結果は振った瞬間に決まっていて、演出はあとから見せるだけ
順番正しいか
転がす → 止まった目を読む → 配る間違い(各自の物理がずれるので食い違う)
振った瞬間に決める → 配る → 各自が演出を見せる正しい

答えは先に決まっていて、演出はそれを見せているだけです。 テレビの生中継のように見えて、実は録画を流している、という形になります。

作り方は簡単です。ApplyState() の中で、すぐ数字を出す代わりに演出を始めます。

private void ApplyState()
{
    if (diceValue == 0) return;

    // 転がるアニメーションを再生する
    if (diceAnimator != null) diceAnimator.SetTrigger("Roll");

    // 1秒後に、決まっている目を出す
    SendCustomEventDelayedSeconds(nameof(ShowResult), 1f);
}

public void ShowResult()
{
    if (diceText != null) diceText.text = diceValue.ToString();
}

各自の画面でそれぞれ演出が走りますが、最後に出る数字は同じです。 転がり方が少し違っても、結果はそろいます。

待たせ方は 時間差で処理を呼ぶ、アニメーションの作り方は AnimatorとUdonで扉を動かす と同じです。

Sponsored

うまくいかないときは

  • 人によって目が違う → 各自が Random.Range() を呼んでいます。所有者だけが呼ぶ形にします
  • 6が出ないRandom.Range(1, 6) と書いています。上限は含まれないので 7 にします
  • 自分の画面だけ変わる → 所有権を取っていないか、RequestSerialization() を呼んでいません
  • 途中参加の人に伝わらないStart()ApplyState() を呼んでいないか、ネットワークイベントだけで伝えています
  • 連打で連続して変わる → 間隔を空ける処理を入れます
  • 演出の途中で数字が見えてしまう → 表示を出すタイミングを、演出が終わったあとにします

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

  • 同じ形が使い回せる: 抽選、カードのシャッフル、ランダムなBGMの選曲、当たりくじ。「1人が決めて、みんなで見る」ものはすべて同じ書き方です
  • 2個目のサイコロは別の変数で: 2個振るなら、同期変数を2つ持ちます。1つの変数に無理やり詰め込む必要はありません
  • 誰が振ったかも配れる: 同期変数をもう1つ足せば、「振った人の名前」も一緒に配れます。ボードゲームでは手番の管理に使えます
  • 完全な公平さを求めない: 所有者のパソコンが結果を決めているので、理屈のうえでは細工の余地があります。友達と遊ぶワールドなら十分ですが、賭けの成立するものには向きません
  • 使いどころはいろいろ: すごろく、ガチャ、ルーレット、ランダムなお題出し。どれもこの形1つで作れます

まとめ

同期する乱数は、決める人を1人に絞る話です。

  • 各自で引くと食い違う。振るのは所有者だけ
  • 結果は同期変数で配る。構造は同期入門と同じ
  • Random.Range(1, 7) の上限は含まれない
  • 演出は、決まった答えをあとから見せるだけ

作るときの問いかけは、「これは誰が決めて、誰が見るのか」 です。決める人が1人なら、あとは配るだけです。

決まった数の物を貸し出すなら VRCObjectPool入門、一瞬の合図を送るなら Network Events へ進んでください。

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