コンテンツにスキップ

同期

書き方説明注意
[UdonSynced] private int value;整数の同期
[UdonSynced] private bool flag;真偽値の同期
[UdonSynced] private string label;文字列の同期
[UdonSynced] private Vector3 point;構造体の同期
[UdonSynced] private Pair pair;自分で作った構造体の同期
[UdonSynced] private Mode mode;列挙型の同期
[UdonSynced] private int[] values;配列の同期
[UdonSynced(UdonSyncMode.Linear)]補間つきの同期
[UdonSynced(UdonSyncMode.Smooth)]なめらかな補間
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]手動同期
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]連続同期
[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]変数を同期しない方式
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 Bag bag;(中に配列がある)配列を持つ struct を同期するTUKI0103未実装配列だけ別のフィールドに出して、それぞれ同期する
[UdonSynced] private Nothing nothing;(フィールドが無い)中身の無い struct を同期するTUKI0103設計同期したい値をフィールドとして持たせる
[UdonSynced(UdonSyncMode.Linear)] private Tagged tagged;(中に文字列がある)補間できない型を持つ struct に補間を指定するTUKI0106環境補間なしにするか、補間できる型だけの struct に分ける
[UdonSynced] private int[][] grid;配列の配列を同期するTUKI0103環境1 本の配列にして添字を計算する(配列)
[UdonSynced] private GameObject target;オブジェクトへの参照を同期するTUKI0103環境Inspector で全員に同じ参照を差し込むか、対象に番号を振ってその番号を同期する
[UdonSynced(UdonSyncMode.Linear)] private string label;補間できない型に補間を指定するTUKI0106環境補間なしにする
[UdonSynced] public int Value { get; set; }プロパティに付けるCS0592環境バッキングフィールドに付けて、[FieldChangeCallback] でプロパティにつなぐ
[UdonSynced] int v = 1;ローカル変数に付ける(メソッドの中)CS7014環境フィールドにする
[UdonSynced] private static int v;static フィールドに付けるTUKI0110環境static を外して普通のフィールドにする
[UdonSynced] private const int v = 3;const フィールドに付けるTUKI0110環境同上
[UdonSynced] private readonly int v;readonly フィールドに付けるTUKI0110環境readonly を外す
[UdonBehaviourSyncMode(Continuous)] と [UdonSynced] int[]送り続ける方式で配列を同期するTUKI0107環境Manual にする
[UdonBehaviourSyncMode(Manual)] と [UdonSynced(Linear)]呼んだときだけ送る方式で補間を指定するTUKI0107環境Continuous にする
[UdonBehaviourSyncMode(None)] と [UdonSynced]同期しない方式で同期する変数を持つTUKI0107設計方式を変えるか、属性を外す
[UdonBehaviourSyncMode(NoVariableSync)] と [UdonSynced]変数を同期しない方式で同期する変数を持つTUKI0107設計方式を変えるか、属性を外す

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

書いても既定と同じになる綴り

Section titled “書いても既定と同じになる綴り”

移植したコードに出てくる次の 3 つは、そのまま通ります。どれも書かなかったときと同じ意味なので、 残しても外しても動きは変わりません。

綴り意味
[UdonBehaviourSyncMode(BehaviourSyncMode.Any)]方式を指定しません。付けなかったときと同じで、コンポーネント側の設定がそのまま使われます
[UdonSynced(UdonSyncMode.None)]値をそのまま運びます。[UdonSynced] だけを書いたときと同じです
[UdonSynced(UdonSyncMode.NotSynced)]同期しません。[UdonSynced] を付けなかったときと同じで、同期する変数になりません

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

何を決める仕組みか同期するオブジェクトごとに「いまこのプレイヤーのもの」と決めます。対象は GameObject で、Networking.IsOwner(gameObject) のように渡して調べます
何のために必要か同期する変数へ代入できるのは所有者だけです。所有者でないプレイヤーが代入しても、その値はほかのプレイヤーへ送られません
最初の所有者最初に入ったプレイヤーが master になり、SetOwner() を一度も呼んでいないオブジェクトの所有者は master です。保証されているのは master の決定規則だけで、そのオブジェクトの所有者が master であることは保証されていません
人数公式ドキュメントは 1 人と説明しています。ただし保証はされておらず、所有者が 2 人いる状態も、誰も所有していない状態も報告されています

図 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() が呼ばれるかどうかは、公式ドキュメントに 記述がありません(一般には送信のたびに呼ばれるものとして扱われています。呼ばれる前提で軽くしておけば、 どちらでも困りません)。

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

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

連続同期の約 200 バイトについては、VRChat の担当者が別の場所で約 256 バイトと述べていて、公式 ドキュメントの数値と食い違っています。小さいほうで見積もるのが安全です。

超えたとき(ネットワークイベント)捨てられずに送信側で順番待ちになります。消えはしませんが、届くのが遅れます
超えたとき(同期する変数)上の表のとおりで、手動同期は保持して送り直し、連続同期は欠落します
全体の上限自分で指定したレートとは別なので、指定に収まっていてもこちらに当たることがあります
連続同期の 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()要求した人といまの所有者の両方で、それぞれのローカルで実行されます。戻り値が割れると食い違いが残るので、判定に使ってよいのは両者から確実に同じに見えるものだけです(実行中に変わらない静的な情報と、引数で渡ってくるプレイヤーの識別子。同期する変数は該当しません)
いまの所有者が退出済みのとき呼ばれず、自動で割り当てられます
移している最中要求した人が切り替わってからほかのプレイヤーへ伝わるまでのあいだは、所有者が 2 人いるように見えます
PlayerObject入室したプレイヤーごとに 1 つずつ作られるオブジェクトです。所有者は本人で固定されていて、他人へ移せません(保証の範囲は Start と OnDeserialization の実行中で、常に正しいという意味ではありません)
sequenceDiagram
    participant X as 1 人目
    participant N as ネットワーク
    participant Y as 2 人目

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

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

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

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

最も基本的な書き方です。

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

bool にも付けられます。

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"
}
}

実行環境が持っている構造体は同期できます。

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

自分で作った struct も、中のフィールドがすべて同期できる型なら同期できます。

using UnityEngine;
using Tsukimi;
public struct Pair { public int A; public float B; }
public class S_sync_struct : TsukimiBehaviour
{
[UdonSynced] private Pair pair;
void Start() { pair.A = 3; pair.B = 1.5f; Debug.Log(pair.A); } // => 3
}

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

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();
}
}

届いた値のあいだを埋める指定です。

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] で、そのプログラムの同期の方式を決めます。

手動同期にします。

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

連続同期にします。

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

同期する変数を持たない宣言です。

using UnityEngine;
using Tsukimi;
using VRC.Udon.Common.Interfaces;
[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
public class AttributesSyncModeNoVariableSync : TsukimiBehaviour
{
private int hits;
public override void Interact()
{
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Ping));
}
public void Ping()
{
hits = hits + 1;
Debug.Log(hits);
}
}
using UnityEngine;
using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
public class SyncNoVariableSyncWithSynced : TsukimiBehaviour
{
[UdonSynced] private int count;
public override void Interact()
{
count = 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
}
}

プロパティへ代入すると、その時点で設定側が実行されます。

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 引数のプレイヤーを所有者にします。

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);
}
}

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

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);
}
}