ハツェの真時代傾向璋

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

一定間隔で処理をするやつの正攻法と裏技

こんばんは。ハツェです。
今回はミニtipsみたいなお話です。
SDK2時代のOnTimerをUdonならどう実装するかみたいな話です。
動作環境は、Unity : 2019.4.30f1、VRCSDK : 2021.09.03.09.25、U# : v0.20.3です。

目次


今回の概要

一定間隔で処理を行う方法の一例を紹介します。
参考程度に見てもらえればと思います。

通常の例

だいたい「Unity 一定間隔」とかで検索すると、Updateで時間ごとに処理を走らせるコードが出てくると思います。

[SerializeField] private float _timeInterval;
private float _timeElapsed;

private void Update()
{
    _timeElapsed += Time.deltaTime;
    
    if (_timeElapsed  > _timeInterval)
    {
        /*-- 一定間隔で実行したい処理 --*/

        // 経過時間を元に戻す
        _timeElapsed = 0f;
    }
}

これ自体はよくある実装例であり、かつ単純なコードで済みます。

DelayedSecondsを使った例

一方で、コルーチンを使った方法というのもまたUnityに存在しています。
ですが、Udonではコルーチンを使うことは現状出来ません。
しかし、SendCustomEventDelayedSecondsを使うことで似たようなことが出来ます。

[SerializeField] private float _timeInterval;

private void Start()
{
    DoFuction();
}

public void DoFuction()
{
    /*-- 一定間隔で実行したい処理 --*/

    if (_timeInterval > 0)
    {
        SendCustomEventDelayedSeconds(nameof(DoFuction), _timeInterval);
    }
}

この方法を使うことで、Updateの毎フレーム処理を軽減することが出来ます。
しかし、SendCustomEventDelayedSecondsは中断機能などを備えていないため、一度実行させてしまったら、対象の関数は絶対実行されます。
なので、やっぱり途中でやめる!みたいなものを追加したい場合は、if文に条件を加えたりする必要があると思います。

あとがき

今回は、SendCustomEventDelayedSecondsを使ったOnTimer代替実装を紹介しました。
おそらく非同期実装だと思っているので、複雑なものを実装するときは少し躊躇するかなとも思っています。
他の方法としては過去に、Sphereをバウンドさせ、着地した時に処理を走らせることで、一定間隔ごとに処理を行わせるみたいなテクニックもありました。
それ以外にも面白い方法とかありましたら、是非ともコメントにて教えて頂けると記事ネタにもなるので助かります。
以上です!ありがとうございました。

入門 ②の処理を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は、変数ごとに個別の処理を記述出来ること、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