はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】PlayerLoopを使って毎フレーム実行される関数を追加する

はじめに

今回はPlayerLoopについて紹介し、実際に独自の処理を追加してみたいと思います。

PlayerLoopとは

PlayerLoopについて公式ドキュメントでは以下のように記載されています。

This class contains functions for interacting with the player loop in the core of Unity. You can use this class to get the update order of all native systems and set a custom order with new script entry points inserted.

LowLevel.PlayerLoop - Unity スクリプトリファレンス

説明がPlayerLoopのクラスの説明なので分かりずらいですが、重要な箇所だけざっと日本語訳すると

PlayerLoopクラスを使うことで、ネイティブシステムの更新順番を取得できたり、新しいスクリプトのエントリーポイントを挿入して独自の順序にすることができます。


つまりはUpdateといった毎フレーム実行される処理を好きなようにいじることができるという意味です。

f:id:hanaaaaaachiru:20210819115351p:plain
PlayerLoopを使った独自処理の追加

PlayerLoopの細かいイベントたちはUniTaskの作者の方がGitHubに挙げてくれていたので貼っておきます。(一部UniTaskのイベントが入っていますが今回は無視してください)


またUnityのバージョンで違いがあるよう(具体的にはUNITY_2020_2_OR_NEWER)なので、自身のPlyerLoopを知りたい場合は以下のメソッドを使うことで分かります。

// 現在のPlayerLoopを調べる
public static LowLevel.PlayerLoopSystem GetCurrentPlayerLoop ();

// デフォルトのPlayerLoopを調べる
public static LowLevel.PlayerLoopSystem GetDefaultPlayerLoop ();

LowLevel.PlayerLoop-GetCurrentPlayerLoop - Unity スクリプトリファレンス

PlayerLoopSystem

PlayerLoopを扱う上でPlayerLoopSystem構造体は切っても切り離せません。
LowLevel.PlayerLoopSystem - Unity スクリプトリファレンス

PlayerLoopSystemには5つの変数を保持しますが、その中でも重要な3つを抜粋しました。

名前 意味
subSystemList PlayerLoopSystem[] PlayerLoopで一緒に実行されるSubSystemのリスト
type Type ネイティブシステムでの識別子として用いる
updateDelegate UpdateFunction 追加するデリゲート

LowLevel.PlayerLoopSystem - Unity スクリプトリファレンス

日本語訳がかなり怪しいですが雰囲気が伝われば幸いです。

このsubSystemListが結構重要な変数で、PlayerLoopSystemが変数としてPlayerLoopSystemの配列を保持しています。

これはいわゆる木構造を表現していて、冒頭で書いたデフォルトのPlayerLoopを表現すると以下のようになります。

f:id:hanaaaaaachiru:20210819193741p:plain
PlayerLoopSytem


これらすべてのノード(木の各節点)にupdateDelegateという実行するメソッドが設定でき、以下の順番で実行されていきます。

f:id:hanaaaaaachiru:20210819194508p:plain
実行順番

また木のルートにあるupdateDelegateは実行されないことに注意してください。

PlayerLoopの中身自体を書き換える

デフォルトのPlayerLoopに変更を加えるためにはSetPlayerLoopというメソッドを利用します。

public static void SetPlayerLoop (LowLevel.PlayerLoopSystem loop);

LowLevel.PlayerLoop-SetPlayerLoop - Unity スクリプトリファレンス

独自の処理を追加する前に、PlayerLoopの中身を丸ごと書き換えてみましょう。

using UnityEngine;
using UnityEngine.LowLevel;

public class PlayerLoopTest
{
    public struct MyUpdate { }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Init()
    {
        var mySystem = new PlayerLoopSystem
        {
            subSystemList = new PlayerLoopSystem[]
            {
                new PlayerLoopSystem
                {
                    updateDelegate = CustomUpdate,
                    type = typeof(MyUpdate),
                }
            }
        };

        PlayerLoop.SetPlayerLoop(mySystem);
    }

    private static void CustomUpdate()
    {
        Debug.Log("my update.");
    }
}
f:id:hanaaaaaachiru:20210819151918p:plain
様子

ゲームビューにあるCubeにはRigidbodyがアタッチされているのですが、うんともすんとも動きません。不思議な世界です。

またsubSystemList = new PlayerLoopSystem[]のように書いているのがミソで、以下のように書くと動作しませんでした。(テラシュールブログさんは以下のコードでミスってたみたいです)

var mySystem = new PlayerLoopSystem
{
    type = typeof(MyUpdate),
    updateDelegate = CustomUpdate,
};


途中でも紹介しましたが、木のルートにupdateDelegateを指定しても動作しません。
ちなみに変化球ですが、以下のようにしても動作しました。

var mySystem = new PlayerLoopSystem
{
    subSystemList = new PlayerLoopSystem[]
            {
                new PlayerLoopSystem
                {
                    subSystemList = new PlayerLoopSystem[]
                    {
                        new PlayerLoopSystem
                        {
                            subSystemList = new PlayerLoopSystem[]
                            {
                                new PlayerLoopSystem
                                {
                                    type = typeof(MyUpdate),
                                    updateDelegate = CustomUpdate,
                                }
                            }
                        }
                    }
                }
            }
};

独自の処理を追加する

実際にUpdate前に独自のイベント関数を追加してみたいと思います。

public class PlayerLoopTest
{
    public struct MyUpdate { }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Init()
    {
        var mySystem = new PlayerLoopSystem
        {
            type = typeof(MyUpdate),
            updateDelegate = CustomUpdate,
        };

        var playerloop = PlayerLoop.GetDefaultPlayerLoop();

        for (var i = 0; i < playerloop.subSystemList.Length; i++)
        {
            if (playerloop.subSystemList[i].type == typeof(Update))
            {
                playerloop.subSystemList[i] = new PlayerLoopSystem
                {
                    type = playerloop.subSystemList[i].type,
                    updateDelegate = playerloop.subSystemList[i].updateDelegate,
                    subSystemList = playerloop.subSystemList[i].subSystemList.Prepend(mySystem).ToArray(),      // subSystemListの先頭にmySystem追加
                    updateFunction = playerloop.subSystemList[i].updateFunction,
                    loopConditionFunction = playerloop.subSystemList[i].loopConditionFunction,
                };
                break;
            }
        }

        PlayerLoop.SetPlayerLoop(playerloop);
    }

    private static void CustomUpdate()
    {
        Debug.Log("my update.");
    }
}

こうすると毎回デバッグが出力されながらも、ちゃんとCubeが落下していきました。

さいごに

PlayerLoopですが、PlayerLoopSystemのデフォルトの木構造さえ理解できればすごい難しいというわけではない気がします。

ただPlayerLoopSystem.typeがいまいちつかめていなく、struct(classでもいけた)の型を入れる動作原理を理解できていません。

公式ドキュメントには以下のように書かれていました。

This property is used to identify which native system this belongs to, or to get the name of the managed system to show in the profiler.

ネイティブシステムでの識別子として用いられていて、プロファイラーで見れるそうです。

f:id:hanaaaaaachiru:20210819162606p:plain
プロファイラーでの表示

確かにプロファイラにMyUpdateという型の名前が使われていました。

ただわざわざ型でなくともstringでもいいんじゃないのかなとも思わなくないですが、コンパイルとかの制約とかの影響なんですかね?

何か知ってる方がいましたら、コメント等で教えてくださると嬉しいです。

ではまた。