同期
書ける形(21)
Section titled “書ける形(21)”| 書き方 | 説明 |
|---|---|
[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, ...) | 引数つきの呼び出し |
書けない形(6)
Section titled “書けない形(6)”| 書き方 | 説明 | エラー | 理由 | 代わりに |
|---|---|---|---|---|
[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] を付けられるのはフィールドだけです。
同期の仕組み
Section titled “同期の仕組み”公式ドキュメントが保証しているのは、最初に入ったプレイヤーが 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() が呼ばれる
同期の注意点
Section titled “同期の注意点”| 箇所 | 注意 |
|---|---|
RequestSerialization() | 呼んだ回数だけ送られるわけではありません。何度呼んでも、次に送られるのは一度です |
| 送信 | 途中で失われることがあります。失われても送信側には通知されません |
OnPostSerialization() | 分かるのは送信を試みたところまでです。相手が受信したかどうかは分かりません |
| 受信側の代入 | 順序は決まっていません。複数の変数がそろった状態を見るには OnDeserialization() まで待ちます |
所有者側で呼ばれるもの
Section titled “所有者側で呼ばれるもの”図 1 の下半分は受け取る側の動きです。所有者では起きません。
OnDeserialization() は値を受信したときに呼ばれます。所有者は自分が送った値を受信しないので、
通常は呼ばれません。ただし公式ドキュメントに「所有者では呼ばれない」という記述はなく、
所有者で呼ばれた事例が報告されています。所有者かどうかの判定には使えません。
FieldChangeCallback は、受信した値をフィールドへ直接入れず、プロパティの設定側を通す仕組みです。
所有者は値を受信しないので、この経路では走りません。一方、所有者がプロパティへ代入すれば、
その代入自体で設定側が走ります。これは同期とは無関係な C# の動作です。
| 代入のしかた | 所有者 | 受け取る側 |
|---|---|---|
Value = 1(プロパティ) | 代入した時点で設定側が走る | 受信時に設定側が走る |
backing = 1(フィールド) | 走らない | — |
両側で同じ処理が走っているように見えますが、経路は別です。所有者がフィールドへ直接代入すると、 所有者側だけ処理が抜けます(所有者側での代入)。
図 2 手動同期と連続同期
Section titled “図 2 手動同期と連続同期”[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 秒あたり何度も呼ばれます。呼ばれる前提で軽くしておけば、どちらでも困りません。
送信量と回数の目安
Section titled “送信量と回数の目安”公式ドキュメントが数値を挙げているのは次の 3 つです。原文でも「約」「およそ」と付いた概数で、 VRChat 側の更新で変わることがあります。
| 何の量か | 目安 |
|---|---|
| 1 秒あたりに送信できる量 | 約 11 キロバイト |
| 1 回の送信の上限 | 手動同期でおよそ 280,496 バイト、連続同期でおよそ 200 バイト |
| 1 つのネットワークイベントを呼べる回数 | 既定で 1 秒あたり 5 回。[NetworkCallable(100)] のように書いて最大 100 回まで |
超えたときの扱いは、送るものによって違います。ネットワークイベントは、超えたぶんが捨てられずに 送信側で順番待ちになります。消えはしませんが、届くのが遅れます。同期する変数は上の表のとおりで、 手動同期は保持して送り直し、連続同期は落ちます。
連続同期の 200 バイトは、そのプログラムに置いた同期する変数の全体に対する上限です。変数を 増やすほど上限に近づきます。
1 回の送信が、ネットワーク上でも 1 つになるとは限りません。1024 バイトを超えると内部で分割される ので、1 回のつもりで呼んだ送信が、自分で決めた回数の上限に当たることがあります。
次の数値は公式ドキュメントに書かれていません。
- 連続同期が 1 秒あたり何回送るか(二次の資料では「約 10 回」とされますが、実測か推定かが示されていません)
- 文字列の長さの上限(1 文字 2 バイトとして、上の上限に収まる範囲、という形でしか言えません)
- 配列の要素数の上限
図 3 所有権の遷移
Section titled “図 3 所有権の遷移”所有権は、プレイヤーがそれぞれ「いま誰が所有者か」を保持する形です。移すと呼んだ側が先に切り替わり、 ほかのプレイヤーへは遅れて伝わります。
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 つずつ作られるオブジェクトです。所有者は本人で
固定されていて、他人へ移せません。ただし保証の範囲は Start と OnDeserialization の実行中で、
常に正しいという意味ではありません。
図 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: 勝ったほうの値を受信する
負けたほうの代入は、勝ったほうの値を受信した時点で消えます。どちらも自分の側では成功するので、 所有権の取得は排他制御になりません。
そのほかに起きること
Section titled “そのほかに起きること”- 衝突や持ち上げによって所有者が変わることがあります。このとき何も呼ばれません
- 所有者が退出すると自動で割り当てられます。割り当て先の規定はありません。
OnOwnershipRequest()は呼ばれず、退出の通知との前後関係も決まっていません - 図 4 の状態から戻らなくなることがあります。通信の遅延が小さい環境ほど起こりやすいようです
よくある勘違い
Section titled “よくある勘違い”| 勘違い | 実際 |
|---|---|
所有者は一人なので、SetOwner() を呼べば排他制御になる | 二人が同時に SetOwner() を呼ぶと、どちらも自分の側では所有者になります。排他制御にはなりません |
OnDeserialization() が呼ばれたということは、自分は所有者ではない | OnDeserialization() が所有者で呼ばれた記録があります。所有者かどうかの判定には使えません |
| 所有者でないまま代入した値は、すぐ元に戻る | エラーは出ません。その値が元に戻るのは、本来の所有者が次に送信したときです。所有者が送信しなければ、その値は残り続けます |
| 途中から入ってきたプレイヤーにも、それまでのネットワークイベントが届く | 同期する変数の値は送られますが、ネットワークイベントは送り直されません |
| 入室した直後は、まだ何も呼ばれない | 入室したところで OnDeserialization() が呼ばれます。そこで音やアニメーションを鳴らしていると、入室した瞬間にまとめて鳴ります |
同期する変数
Section titled “同期する変数”[UdonSynced] を付けたフィールドが、同期の対象になります。
いちばん基本の形です。int のフィールドに付けます。
using UnityEngine;using Tsukimi;
public class AttributesSynced : TsukimiBehaviour{ [UdonSynced] private int value;
void Start() { value = 1; RequestSerialization(); Debug.Log(value); // => 1 }}真偽値の同期
Section titled “真偽値の同期”bool にも付けられます。何も入れなければ false から始まります。
using UnityEngine;using Tsukimi;
public class SyncBool : TsukimiBehaviour{ [UdonSynced] private bool flag;
void Start() { Debug.Log(flag); // => false }}文字列の同期
Section titled “文字列の同期”string にも付けられます。例は初期化子で入れた値をそのまま読んでいます。
using UnityEngine;using Tsukimi;
public class SyncString : TsukimiBehaviour{ [UdonSynced] private string label = "a";
void Start() { Debug.Log(label); // => "a" }}構造体の同期
Section titled “構造体の同期”実行環境が持っている構造体は同期できます。自分で作った struct はできません(下の表)。
using UnityEngine;using Tsukimi;
public class SyncVector3 : TsukimiBehaviour{ [UdonSynced] private Vector3 point = new Vector3(1f, 2f, 3f);
void Start() { Debug.Log(point.x); // => 実行環境の値 }}列挙型の同期
Section titled “列挙型の同期”列挙型は中身が整数なので、そのまま同期の対象にできます。
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(); }}補間つきの同期
Section titled “補間つきの同期”届いた値のあいだを埋める指定です。付けられる型は限られていて、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); }}なめらかな補間
Section titled “なめらかな補間”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; }}同期のイベント
Section titled “同期のイベント”同期に伴って呼ばれるメソッドです。基底が宣言しているので override で書きます。
値が届いたとき
Section titled “値が届いたとき”同期した値が届いたあとに呼ばれます。
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変数ごとの通知
Section titled “変数ごとの通知”同期でこの変数に値が届くと、フィールドへ直接入るのではなく、指定したプロパティの設定側を通ります。
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 }}所有者側での代入
Section titled “所有者側での代入”プロパティへ代入すると、その時点で設定側が走ります。同期とは無関係な 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所有者を調べたり、所有権を移したりする形です。
自分が持っているか
Section titled “自分が持っているか”そのオブジェクトの所有者かどうかを調べられます。
using UnityEngine;using Tsukimi;using VRC.SDKBase;
public class SyncIsOwner : TsukimiBehaviour{ [UdonSynced] private int value;
void Start() { if (Networking.IsOwner(gameObject)) { value = 1; RequestSerialization(); } }}所有権を取る
Section titled “所有権を取る”第 1 引数のプレイヤーを所有者にします。例では自分(Networking.LocalPlayer)を渡しています。
using UnityEngine;using Tsukimi;using VRC.SDKBase;
public class SyncSetOwner : TsukimiBehaviour{ void Start() { Networking.SetOwner(Networking.LocalPlayer, gameObject); }}所有権が移ったとき
Section titled “所有権が移ったとき”所有権が移ったあとに呼ばれます。
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要求を受けるかどうか
Section titled “要求を受けるかどうか”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 }}ネットワークイベント
Section titled “ネットワークイベント”変数の値ではなく、メソッドの呼び出しを送る形です。
呼ばれる側の許可
Section titled “呼ばれる側の許可”この印を付けたメソッドだけが、ネットワーク越しに呼ばれます。
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); }}
// 出力// 1using 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); }}引数つきの呼び出し
Section titled “引数つきの呼び出し”送る側で値を並べると、受け取る側の引数に入ります。引数は 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); }}