ハツェの真時代傾向璋

興味を持ったことを書いていく鱗片的な場所から先の未来の場

入門 ②の処理をOnValueChangedを用いて書いてみる

こんばんは。ハツェです。
お久しぶりです。
今回は、最近Udonに実装された変数の値が変わった時に発生するイベントOnValueChangedについて取り上げます。
今回も、この記事はコードを基礎的に書ける方を対象としています。予めご了承ください。
動作環境は、Unity : 2019.4.29f1、VRCSDK : 2021.08.11.15.16、U# : v0.20.2です。

目次


対象読者

以下を理解している方を前提に書いています。あらかじめご了承ください。

  • Udonの同期処理の仕組みの基礎を理解している方
  • C#の基礎的な知識及びプロパティについて理解している方

もし、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;
        // 以下、任意の処理
    }
}


必須な実装は以下の通りです。

  1. OnValueChangedイベントを適用する変数にFieldChangeCallback属性を付ける
  2. 適用する変数に対応させるプロパティを記述する
  3. 適用する変数の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を用いない方がいいよ」というリプライを頂きました。

SendCustomNetworkEventを用いてしまうと、同期変数とSendCustomNetworkEventで同期頻度が異なることから、予想し得ない処理が発生する可能性があるため、極力SendCustomNetworkEvent内で同期変数を処理しない方が良いみたいです。
現段階で私がこの新しい設計に慣れていなかったので、今回この記事を書くことで理解を深められたらなと思い、記事を書くことにしました。
この記事が、誰かの役に立つといいなとは同時に思っています。

今回も、サンプルプロジェクトはGithubに置いておきます。
2.3のシーンが今回の内容のものになっているので、詳しく見たい方はご覧ください。
github.com