こんばんは。ハツェです。
お久しぶりです。
今回は、最近Udonに実装された変数の値が変わった時に発生するイベントOnValueChangedについて取り上げます。
今回も、この記事はコードを基礎的に書ける方を対象としています。予めご了承ください。
動作環境は、Unity : 2019.4.29f1、VRCSDK : 2021.08.11.15.16、U# : v0.20.2です。
目次
対象読者
以下を理解している方を前提に書いています。あらかじめご了承ください。
もし、Udonの同期処理の仕組みを知らないという方は、以下の記事を一度ご参照の上、当記事をお読みいただければと思います。
hatuxes.hatenablog.jp
今回の概要
今回は、Udon入門②にて取り上げた同期変数の例をOnValueChangedで書き換えるという話題について取り上げます。
OnValueChangedとは
OnValueChangedとは、指定した変数の値が代入もしくはSetProgramVariableで変更された時に呼び出されるイベントです。
OnValueChanged自体は、同期されているかどうかに関わらず、全ての変数に適用出来ます。すいません。OnValueChangedは同期変数にのみ対応していました。
OnValueChangedは、変数ごとに個別の処理を記述出来ること、OnDeserializationよりも更新速度が速いことがメリットになっています。
docs.vrchat.com
OnValueChangedをU#で実装する
OnValueChangedの実装方法は、UdonGraphとU#で異なります。UdonGraphの実装方法は割愛します。
U#では、C#のプロパティのような機能を用いて実装します。
おおよそ以下の通りです。
[UdonSynced, FieldChangeCallback(nameof(SyncedVariable))] private int _syncedVariable; public int SyncedVariable { get => _syncedToggle; set { _syncedVariable = value; // 以下、任意の処理 } }
必須な実装は以下の通りです。
- OnValueChangedイベントを適用する変数にFieldChangeCallback属性を付ける
- 適用する変数に対応させるプロパティを記述する
- 適用する変数のFieldChangeCallback属性の引数に、2番目で作ったプロパティの名前を設定する
これで、OnValueChangedイベントが適用した変数にて発生するようになります。
そのため、もし変数が変わった時に動作させたい処理があるなら、setterの部分に記述することで動作するようになります。
感覚としては、OnDeserializationのようにsetterを扱うイメージです。
公式の例については、以下を参照してください。
github.com
実際に改変してみる
ここからは、実際に改変した例を紹介します。
Ownerに処理を渡す例の改変例
元のスクリプトは以下のものです。
hatuxes.hatenablog.jp
これをOnValueChangedを用いた処理に改変してみました。少し変数名とかを変えて見やすくしています。
using UdonSharp; using UnityEngine.UI; using VRC.SDKBase; [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class ViaOwner_Countup_System_WithOnValueChanged : UdonSharpBehaviour { [UdonSynced(UdonSyncMode.None), FieldChangeCallback(nameof(CountData))] private int _countData; // データ本体 public Text DisplayDataText; // データを表示するText public Text OptionText; // 誰がOwnerかを表示するText // OnValueChanged用のプロパティ public int CountData { get => _countData; set { _countData = value; DisplayCountData(); // Owner以外のデータ表示処理 } } private void Start() { // Onwerかどうかを表示 SetOptionalText(Networking.LocalPlayer); } // Cubeをインタラクトしたときに呼ばれる public override void Interact() { if (Networking.LocalPlayer.IsOwner(this.gameObject)) { // Ownerが押したら、純粋にカウントアップ CountUp(); } else { // Owner以外が押したら、Ownerにカウントアップさせるように命令する SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, nameof(CountUp)); } } // Ownerが値を+1する処理 public void CountUp() { CountData++; // データ更新 RequestSerialization(); // 同期更新 } // 同期変数の値をUIに表示する処理 public void DisplayCountData() { DisplayDataText.text = _countData.ToString(); // データ表示更新 } // プレイヤーがOwnerかどうかをテキストで表示させる処理 public void SetOptionalText(VRCPlayerApi player) { if (player.IsOwner(this.gameObject)) { // Owner側の処理 OptionText.text = $"<color=red>{player.displayName} is Owner!</color>"; } else { // Owner以外の処理 OptionText.text = $"{player.displayName} isn't Owner"; } } }
改変としては、OnDeserializationをOnValueChangedに書き換えただけです。
この方がコードとしても見やすくていいですね。ゴチャゴチャしてない感じ。
Ownerを移行して処理する例の改変例
元のスクリプトは以下のものです。
hatuxes.hatenablog.jp
これを同様に改変してみました。以下の通りです。
using UdonSharp; using UnityEngine.UI; using VRC.SDKBase; [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class OwnerTransfer_Countup_System_WithOnValueChanged : UdonSharpBehaviour { [UdonSynced(UdonSyncMode.None), FieldChangeCallback(nameof(CountData))] private int _countData; // データ本体 public Text DisplayDataText; // データを表示するText public Text OptionText; // 誰がOwnerかを表示するText // OnValueChanged用のプロパティ public int CountData { get => _countData; set { _countData = value; DisplayCountData(); // Owner以外のデータ表示処理 } } private void Start() { // Onwerかどうかを表示 SetOptionalText(Networking.LocalPlayer); } // Cubeをインタラクトした時に呼ばれる public override void Interact() { // 新にOwnerになる var player = Networking.LocalPlayer; Networking.SetOwner(player, this.gameObject); if (player.IsOwner(this.gameObject)) { // カウントアップ処理 CountUp(); } } // Ownerが移行した際の処理 // このイベントはインスタンスにいる全員に発行されるので、Network.LocalPlayerを用いる // 引数のplayerは新たなオーナーを指す public override void OnOwnershipTransferred(VRCPlayerApi player) { SetOptionalText(Networking.LocalPlayer); } // Ownerが値を+1する処理 public void CountUp() { CountData++; // データ更新 RequestSerialization(); // 同期更新 } // 同期変数の値をUIに表示する処理 public void DisplayCountData() { DisplayDataText.text = _countData.ToString(); // データ表示更新 } // プレイヤーがOwnerかどうかをテキストで表示させる処理 public void SetOptionalText(VRCPlayerApi player) { if (player.IsOwner(this.gameObject)) { // Owner側の処理 OptionText.text = $"<color=red>{player.displayName} is Owner!</color>"; } else { // Owner以外の処理 OptionText.text = $"{player.displayName} isn't Owner"; } } }
改変としては先ほど同様、OnDeserializationをOnValueChangedに書き換えただけです。
一つ良くなった点として、OnDeserializationで書いていた時と違い、RequestSerialization()
を遅延しなくてもしっかりと動作出来たということがありました。
これは、OnValueChangedが速く同期出来るという恩恵によるものです。
まとめ
結論として、OnValueChangedは結構早く同期してくれるよということでした。
余談ですが、以前に私がUdonのツイートをした際に、CyanLaserさんから「同期された変数を用いる場合は、SendCustomNetworkEvent
を用いない方がいいよ」というリプライを頂きました。
With OnVariableChanged everything can be handled in two events. When dealing with Synced variables, OnDeserialization or OnVariableChange should be used and not SendCustomNetworkEvent. This causes race conditions as synced data may not arrive in time.
— CyanLaser (@CyanLaser) 2021年8月16日
Translated with DeepL.
SendCustomNetworkEvent
を用いてしまうと、同期変数とSendCustomNetworkEvent
で同期頻度が異なることから、予想し得ない処理が発生する可能性があるため、極力SendCustomNetworkEvent
内で同期変数を処理しない方が良いみたいです。現段階で私がこの新しい設計に慣れていなかったので、今回この記事を書くことで理解を深められたらなと思い、記事を書くことにしました。
この記事が、誰かの役に立つといいなとは同時に思っています。
今回も、サンプルプロジェクトはGithubに置いておきます。
2.3のシーンが今回の内容のものになっているので、詳しく見たい方はご覧ください。
github.com