コンテンツにスキップ

同期

書き方説明
[UdonSynced] private int value;整数の同期
[UdonSynced] private bool flag;真偽値の同期
[UdonSynced] private string label;文字列の同期
[UdonSynced] private Vector3 point;構造体の同期
[UdonSynced] private Mode mode;列挙型の同期
[UdonSynced] private int[] values;配列の同期
[UdonSynced(UdonSyncMode.Linear)]補間つきの同期
[UdonSynced(UdonSyncMode.Smooth)]なめらかな補間
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]手動同期
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]連続同期
public override void OnDeserialization()値が届いたとき
[FieldChangeCallback(nameof(Value))]変数ごとの通知
Value = 1;所有者側での代入
public override void OnPreSerialization()送る直前
public override void OnPostSerialization(SerializationResult result)送ったあと
Networking.IsOwner(gameObject)自分が持っているか
Networking.SetOwner(Networking.LocalPlayer, gameObject)所有権を取る
public override void OnOwnershipTransferred(VRCPlayerApi player)所有権が移ったとき
public override bool OnOwnershipRequest(...)要求を受けるかどうか
[NetworkCallable]呼ばれる側の許可
SendCustomNetworkEvent(NetworkEventTarget.All, ...)引数つきの呼び出し
書き方説明エラー理由代わりに
[UdonSynced] private Pair pair;自分で作った struct を同期するTUKI0103未定成分ごとに分けて同期する
[UdonSynced] private int[][] grid;配列の配列を同期するTUKI0103環境1 本の配列にして添字を計算する(配列
[UdonSynced] private GameObject target;オブジェクトへの参照を同期するTUKI0103環境編集画面で全員に同じ参照を差し込むか、対象に番号を振ってその番号を同期する
[UdonSynced(UdonSyncMode.Linear)] private string label;補間できない型に補間を指定するTUKI0106環境補間なしにする
[UdonSynced] public int Value { get; set; }プロパティに付けるCS0592環境裏のフィールドに付けて、[FieldChangeCallback] でプロパティにつなぐ
[UdonSynced] int v = 1;ローカル変数に付ける(メソッドの中)CS7014環境フィールドにする

[UdonSynced] を付けられるのはフィールドだけです。

公式ドキュメントが保証しているのは、最初に入ったプレイヤーが master になることと、 PlayerObject の所有者が本人で固定されることの 2 つだけです。値が届くこと・送った順に届くこと・ 一度だけ届くことは、いずれも保証されていません。以下の図は保証ではなく、公式ドキュメントの説明に もとづきます。

所有権は、同期するオブジェクトごとに「いまこのプレイヤーのもの」と決めておく印です。対象は GameObject で、Networking.IsOwner(gameObject) のように GameObject を渡して調べます。

所有権が必要なのは、同期する変数へ代入できるのが所有者だけだからです。所有者でないプレイヤーが 代入しても、その値はほかのプレイヤーへ送られません。

最初に入ったプレイヤーが master になり、SetOwner() を一度も呼んでいないオブジェクトの所有者は master です。このうち保証されているのは master の決まり方だけで、そのオブジェクトの所有者が master であることは保証されていません。

1 つのオブジェクトの所有者は 1 人、というのが公式ドキュメントの説明です。ただし保証はされておらず、 所有者が二人居る状態も、誰も所有していない状態も報告されています。

図 1 値を同期させたときの動き(手動同期)

Section titled “図 1 値を同期させたときの動き(手動同期)”
sequenceDiagram
    autonumber
    participant O as 所有者
    participant N as ネットワーク
    participant P as 受け取る側

    O->>O: 同期する変数へ代入する
    O->>O: RequestSerialization() を呼ぶ
    O->>O: OnPreSerialization() が呼ばれる
    O->>N: そのプログラムの同期する変数を、まとめて一度に送る
    O->>O: OnPostSerialization() が呼ばれる
    N->>P: 受信する
    P->>P: 受信した値を、変数へ 1 つずつ代入する
    P->>P: 代入のたびに FieldChangeCallback が呼ばれる
    P->>P: すべて代入し終えてから OnDeserialization() が呼ばれる
箇所注意
RequestSerialization()呼んだ回数だけ送られるわけではありません。何度呼んでも、次に送られるのは一度です
送信途中で失われることがあります。失われても送信側には通知されません
OnPostSerialization()分かるのは送信を試みたところまでです。相手が受信したかどうかは分かりません
受信側の代入順序は決まっていません。複数の変数がそろった状態を見るには OnDeserialization() まで待ちます

図 1 の下半分は受け取る側の動きです。所有者では起きません。

OnDeserialization() は値を受信したときに呼ばれます。所有者は自分が送った値を受信しないので、 通常は呼ばれません。ただし公式ドキュメントに「所有者では呼ばれない」という記述はなく、 所有者で呼ばれた事例が報告されています。所有者かどうかの判定には使えません。

FieldChangeCallback は、受信した値をフィールドへ直接入れず、プロパティの設定側を通す仕組みです。 所有者は値を受信しないので、この経路では走りません。一方、所有者がプロパティへ代入すれば、 その代入自体で設定側が走ります。これは同期とは無関係な C# の動作です。

代入のしかた所有者受け取る側
Value = 1(プロパティ)代入した時点で設定側が走る受信時に設定側が走る
backing = 1(フィールド)走らない

両側で同じ処理が走っているように見えますが、経路は別です。所有者がフィールドへ直接代入すると、 所有者側だけ処理が抜けます(所有者側での代入)。

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] を付けると手動同期、 [UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)] を付けると連続同期になります。 図 1 は手動同期の場合です。

sequenceDiagram
    participant O as 所有者
    participant N as ネットワーク

    O->>O: 同期する変数へ代入する

    alt 手動同期
        O->>O: RequestSerialization() を呼ぶ
        O->>N: 送信する
        O->>O: もう一度、同期する変数へ代入する
        Note over O: 代入しただけでは送信されない
    else 連続同期
        O->>N: 送信する
        O->>N: 送信する(値が変わっていなくても繰り返す)
        Note over O: RequestSerialization() は働かない
    end
手動同期(Manual連続同期(Continuous
送信のタイミング所有者による RequestSerialization() の呼び出し時自動。値が変わっていなくても繰り返し送る
同期する変数への代入時送信されない。次に RequestSerialization() を呼ぶまで、その値は所有者の手元にとどまるそのままにしておけば、次の送信に載る
RequestSerialization() の呼び出し時送信が予約される何も起きない。送信は増えない
送信の詰まり保持して送り直す落ちて、ログにエラーが出る
1 回の送信の上限およそ 280,496 バイトおよそ 200 バイト
向いているもの得点や状態など、変わったときだけ送る値位置や回転など、変わり続ける値

連続同期で OnPreSerialization()OnPostSerialization() が呼ばれるかどうかは、公式ドキュメントに 記述がありません。一般には、送信のたびに呼ばれるものとして扱われています。連続同期は繰り返し送るので、 そのつもりなら 1 秒あたり何度も呼ばれます。呼ばれる前提で軽くしておけば、どちらでも困りません。

公式ドキュメントが数値を挙げているのは次の 3 つです。原文でも「約」「およそ」と付いた概数で、 VRChat 側の更新で変わることがあります。

何の量か目安
1 秒あたりに送信できる量約 11 キロバイト
1 回の送信の上限手動同期でおよそ 280,496 バイト、連続同期でおよそ 200 バイト
1 つのネットワークイベントを呼べる回数既定で 1 秒あたり 5 回。[NetworkCallable(100)] のように書いて最大 100 回まで

超えたときの扱いは、送るものによって違います。ネットワークイベントは、超えたぶんが捨てられずに 送信側で順番待ちになります。消えはしませんが、届くのが遅れます。同期する変数は上の表のとおりで、 手動同期は保持して送り直し、連続同期は落ちます。

連続同期の 200 バイトは、そのプログラムに置いた同期する変数の全体に対する上限です。変数を 増やすほど上限に近づきます。

1 回の送信が、ネットワーク上でも 1 つになるとは限りません。1024 バイトを超えると内部で分割される ので、1 回のつもりで呼んだ送信が、自分で決めた回数の上限に当たることがあります。

次の数値は公式ドキュメントに書かれていません。

  • 連続同期が 1 秒あたり何回送るか(二次の資料では「約 10 回」とされますが、実測か推定かが示されていません)
  • 文字列の長さの上限(1 文字 2 バイトとして、上の上限に収まる範囲、という形でしか言えません)
  • 配列の要素数の上限

所有権は、プレイヤーがそれぞれ「いま誰が所有者か」を保持する形です。移すと呼んだ側が先に切り替わり、 ほかのプレイヤーへは遅れて伝わります。

sequenceDiagram
    participant R as 要求した人
    participant O as いまの所有者
    participant P as ほかのプレイヤー

    R->>R: SetOwner(自分) を呼ぶ
    R->>R: OnOwnershipRequest() が呼ばれる
    O->>O: 同じ OnOwnershipRequest() が、こちらでも呼ばれる
    R->>R: 受けるなら、その場で所有者は自分になる
    R->>R: OnOwnershipTransferred() が呼ばれる
    R-->>P: ほかのプレイヤーへ伝わる
    P->>P: ほかのプレイヤーから見た所有者も、要求した人になる
    P->>P: OnOwnershipTransferred() が呼ばれる

OnOwnershipRequest() は、要求した人といまの所有者の両方で呼ばれます。戻り値が割れると食い違いが 残るので、判定には両者から同じに見えるもの(同期した変数など)を使います。いまの所有者が退出済みの ときは呼ばれず、自動で割り当てられます。

要求した人が切り替わってからほかのプレイヤーへ伝わるまでのあいだは、所有者が二人居るように見えます。

PlayerObject は、入室したプレイヤーごとに 1 つずつ作られるオブジェクトです。所有者は本人で 固定されていて、他人へ移せません。ただし保証の範囲は StartOnDeserialization の実行中で、 常に正しいという意味ではありません。

図 4 二人が同時に所有権を取る

Section titled “図 4 二人が同時に所有権を取る”
sequenceDiagram
    participant X as 一人目
    participant N as ネットワーク
    participant Y as 二人目

    X->>X: SetOwner(自分) を呼ぶ
    Y->>Y: SetOwner(自分) を呼ぶ
    X->>X: 一人目から見た所有者は、一人目
    Y->>Y: 二人目から見た所有者は、二人目
    X->>N: 同期する変数へ代入して送る
    Y->>N: 同期する変数へ代入して送る
    N->>N: 先に着いたほうが所有者になる
    N->>X: 勝ったほうの値を受信する
    N->>Y: 勝ったほうの値を受信する

負けたほうの代入は、勝ったほうの値を受信した時点で消えます。どちらも自分の側では成功するので、 所有権の取得は排他制御になりません。

  • 衝突や持ち上げによって所有者が変わることがあります。このとき何も呼ばれません
  • 所有者が退出すると自動で割り当てられます。割り当て先の規定はありません。 OnOwnershipRequest() は呼ばれず、退出の通知との前後関係も決まっていません
  • 図 4 の状態から戻らなくなることがあります。通信の遅延が小さい環境ほど起こりやすいようです
勘違い実際
所有者は一人なので、SetOwner() を呼べば排他制御になる二人が同時に SetOwner() を呼ぶと、どちらも自分の側では所有者になります。排他制御にはなりません
OnDeserialization() が呼ばれたということは、自分は所有者ではないOnDeserialization() が所有者で呼ばれた記録があります。所有者かどうかの判定には使えません
所有者でないまま代入した値は、すぐ元に戻るエラーは出ません。その値が元に戻るのは、本来の所有者が次に送信したときです。所有者が送信しなければ、その値は残り続けます
途中から入ってきたプレイヤーにも、それまでのネットワークイベントが届く同期する変数の値は送られますが、ネットワークイベントは送り直されません
入室した直後は、まだ何も呼ばれない入室したところで OnDeserialization() が呼ばれます。そこで音やアニメーションを鳴らしていると、入室した瞬間にまとめて鳴ります

[UdonSynced] を付けたフィールドが、同期の対象になります。

いちばん基本の形です。int のフィールドに付けます。

using UnityEngine;
using Tsukimi;
public class AttributesSynced : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
value = 1;
RequestSerialization();
Debug.Log(value); // => 1
}
}

bool にも付けられます。何も入れなければ false から始まります。

using UnityEngine;
using Tsukimi;
public class SyncBool : TsukimiBehaviour
{
[UdonSynced] private bool flag;
void Start()
{
Debug.Log(flag); // => false
}
}

string にも付けられます。例は初期化子で入れた値をそのまま読んでいます。

using UnityEngine;
using Tsukimi;
public class SyncString : TsukimiBehaviour
{
[UdonSynced] private string label = "a";
void Start()
{
Debug.Log(label); // => "a"
}
}

実行環境が持っている構造体は同期できます。自分で作った struct はできません(下の表)。

using UnityEngine;
using Tsukimi;
public class SyncVector3 : TsukimiBehaviour
{
[UdonSynced] private Vector3 point = new Vector3(1f, 2f, 3f);
void Start()
{
Debug.Log(point.x); // => 実行環境の値
}
}

列挙型は中身が整数なので、そのまま同期の対象にできます。

using UnityEngine;
using Tsukimi;
public class SyncEnum : TsukimiBehaviour
{
private enum Mode { Off, On }
[UdonSynced] private Mode mode;
void Start()
{
Debug.Log((int)mode); // => 0
}
}

配列も同期できます。ただし配列の配列はできません(下の表)。

using UnityEngine;
using Tsukimi;
public class ArraysFieldSynced : TsukimiBehaviour
{
[UdonSynced] private int[] values = new int[4];
void Start()
{
values[0] = 10;
RequestSerialization();
}
}

届いた値のあいだを埋める指定です。付けられる型は限られていて、float は通り、string は通りません。

using UnityEngine;
using Tsukimi;
public class AttributesSyncedInterpolated : TsukimiBehaviour
{
[UdonSynced(UdonSyncMode.Linear)] private float value;
void Start()
{
Debug.Log(value); // => 0
}
}
using UnityEngine;
using Tsukimi;
public class R_sync_interp : TsukimiBehaviour
{
[UdonSynced(UdonSyncMode.Linear)] private string label = "a";
void Start() { Debug.Log(label); }
}

Linear と同じく届いた値のあいだを埋めますが、補間の方法が違います。

using UnityEngine;
using Tsukimi;
public class SyncSmooth : TsukimiBehaviour
{
[UdonSynced(UdonSyncMode.Smooth)] private float amount;
void Start()
{
Debug.Log(amount); // => 0
}
}

[UdonBehaviourSyncMode] で、そのプログラムの同期の方式を決めます。

手動同期にします。RequestSerialization() を呼んだときに送られます。

using UnityEngine;
using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class AttributesSyncModeManual : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
value = 1;
RequestSerialization();
}
}

連続同期にします。RequestSerialization() を呼ばなくても送られます。

using UnityEngine;
using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]
public class AttributesSyncModeContinuous : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
value = 1;
}
}

同期に伴って呼ばれるメソッドです。基底が宣言しているので override で書きます。

同期した値が届いたあとに呼ばれます。

using UnityEngine;
using Tsukimi;
public class SyncOnDeserialization : TsukimiBehaviour
{
[UdonSynced] private int value;
public override void OnDeserialization()
{
Debug.Log(value);
}
void Start()
{
Debug.Log(0);
}
}
// 出力
// 0

同期でこの変数に値が届くと、フィールドへ直接入るのではなく、指定したプロパティの設定側を通ります。

using UnityEngine;
using Tsukimi;
public class AttributesFieldChangeCallback : TsukimiBehaviour
{
[UdonSynced, FieldChangeCallback(nameof(Value))] private int backing;
public int Value
{
get { return backing; }
set { backing = value; }
}
void Start()
{
Value = 1;
Debug.Log(Value); // => 1
}
}

プロパティへ代入すると、その時点で設定側が走ります。同期とは無関係な C# の動作です。 フィールドへ直接代入したときは走りません。

using UnityEngine;
using Tsukimi;
public class SyncFieldChangeCallbackOnWrite : TsukimiBehaviour
{
[UdonSynced, FieldChangeCallback(nameof(Value))] private int backing;
public int Value
{
get { return backing; }
set
{
backing = value;
Debug.Log(100 + value); // => 101
}
}
void Start()
{
Value = 1;
backing = 2;
Debug.Log(backing); // => 2
}
}

送る値を、送る直前に組み立てるためのエントリポイントです。

using UnityEngine;
using Tsukimi;
public class SyncOnPreSerialization : TsukimiBehaviour
{
[UdonSynced] private int value;
public override void OnPreSerialization()
{
value = 1;
}
void Start()
{
RequestSerialization();
Debug.Log(value); // => 0
}
}

送れたかどうかを受け取れます。

using UnityEngine;
using Tsukimi;
using VRC.Udon.Common;
public class SyncOnPostSerialization : TsukimiBehaviour
{
public override void OnPostSerialization(SerializationResult result)
{
Debug.Log(result.success);
}
void Start()
{
Debug.Log(0);
}
}
// 出力
// 0

所有者を調べたり、所有権を移したりする形です。

そのオブジェクトの所有者かどうかを調べられます。

using UnityEngine;
using Tsukimi;
using VRC.SDKBase;
public class SyncIsOwner : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
if (Networking.IsOwner(gameObject))
{
value = 1;
RequestSerialization();
}
}
}

第 1 引数のプレイヤーを所有者にします。例では自分(Networking.LocalPlayer)を渡しています。

using UnityEngine;
using Tsukimi;
using VRC.SDKBase;
public class SyncSetOwner : TsukimiBehaviour
{
void Start()
{
Networking.SetOwner(Networking.LocalPlayer, gameObject);
}
}

所有権が移ったあとに呼ばれます。

using UnityEngine;
using Tsukimi;
using VRC.SDKBase;
public class SyncOnOwnershipTransferred : TsukimiBehaviour
{
public override void OnOwnershipTransferred(VRCPlayerApi player)
{
Debug.Log(0);
}
void Start()
{
Debug.Log(1);
}
}
// 出力
// 1

true を返すと要求を受け、false を返すと断ります。

using UnityEngine;
using Tsukimi;
using VRC.SDKBase;
public class SyncOnOwnershipRequest : TsukimiBehaviour
{
public override bool OnOwnershipRequest(VRCPlayerApi requester, VRCPlayerApi newOwner)
{
return true;
}
void Start()
{
Debug.Log(1); // => 1
}
}

変数の値ではなく、メソッドの呼び出しを送る形です。

この印を付けたメソッドだけが、ネットワーク越しに呼ばれます。

using UnityEngine;
using Tsukimi;
using VRC.SDK3.UdonNetworkCalling;
public class AttributesNetworkCallable : TsukimiBehaviour
{
[NetworkCallable]
public void Receive(int n)
{
Debug.Log(n);
}
void Start()
{
Debug.Log(1);
}
}
// 出力
// 1
using UnityEngine;
using Tsukimi;
using VRC.Udon.Common.Interfaces;
public class R_sync_no_callable : TsukimiBehaviour
{
public void Receive(int n) { Debug.Log(n); }
void Start()
{
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Receive), 1);
}
}

送る側で値を並べると、受け取る側の引数に入ります。引数は 8 個まで書けます。

using UnityEngine;
using Tsukimi;
using VRC.Udon.Common.Interfaces;
using VRC.SDK3.UdonNetworkCalling;
public class ArraysSendAsNetworkEvent : TsukimiBehaviour
{
private int[] scores;
void Start()
{
scores = new int[] { 1, 2, 3 };
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Receive), scores);
}
[NetworkCallable]
public void Receive(int[] values)
{
Debug.Log(values.Length);
}
}