C#
Unity

UnityEventのリスナー数の取得が出来ない

概要

UnityEventにはUnityAction型のリスナーを登録して使用します。
これらのリスナーは登録方法によって挙動が若干違います。
今回は、登録したリスナー数の取得が出来なかったこと、その原因、対策をまとめてみます。

UnityEventのリスナーの種類

UnityEventには
* 永続的リスナー
* 非永続的リスナー
の2種類が存在します。

永続的リスナー

永続的リスナーはインスペクタ上から登録します。
uGUIのButtonのOnClickとかで、登録したことがあるかと思います。
スクリーンショット 2018-03-11 0.55.48.png
自分で作成したクラスでも、シリアライズされるUnityEventであればインスペクタ上から登録可能です。
引数付きUnityEvent(UnityEvent<T0>とか)を継承したクラスは、SerializableAttributeを付けるとOKです。
この永続的リスナーは実行中に登録・削除は出来ませんが、Stateが存在していて、それによってON/OFFしたりすることが出来ます。
https://docs.unity3d.com/jp/current/ScriptReference/Events.UnityEventCallState.html

非永続的リスナー

非永続的リスナーはスクリプトから登録します。
event.AddListener(action);
という形で登録します。
逆に
event.RemoveListener(action)
でリスナーを削除できます。

リスナー数の取得ができない

スクリプトから登録されているリスナー数を取得し、リスナーが存在する時はリスナーの処理を、存在しない時は別の処理を行いたいと考えたのですが、常に0となり、リスナー数が取得できませんでした。。

原因

GetPersistentEventCountメソッドを使用することで、リスナー数を取得できますが、これは永続的リスナーの数を取得し、非永続的リスナーの数を取得することは出来ません。出来ません。無理です。

対処

どうしても取得したい場合は、UnityEventに登録する際に登録数をカウントするようなラップ関数やクラスを作成するのが良いかなと。
ただ、非永続的リスナーは登録する際は重複をチェックせず、削除する際は重複分全てを削除し、追加、削除の成功、失敗などを取得することも出来ないので、HashSet的に別途Actionを管理するような仕組みも必要になりそうです。

まとめ

  • UnityEventのリスナーにはインスペクタ上から登録する永続的リスナー、スクリプトから登録する非永続的リスナーが存在する。
  • 永続的リスナーの数はスクリプトから取得できるが、非永続的リスナーの数は取得できない。
  • 非永続的リスナーの登録数はラップクラスなどを作成し、自前でカウントしていく必要がありそう。

追記(3/11 22:43) リフレクションによる取得

無理やりリフレクションで取得してみました。
パフォーマンス的に自分のやりたかったことにはマッチしていませんが、Editor上で使うとかケースを絞れば使えなくも無い気もします。

public static int GetRuntimeEventCount(this UnityEventBase unityEvent) 
{
    Type unityEventType = typeof(UnityEventBase);

    Assembly libAssembly = Assembly.GetAssembly(unityEventType);

    Type invokableCallListType = libAssembly.GetType("UnityEngine.Events.InvokableCallList");
    Type baseInvokableCallType = libAssembly.GetType("UnityEngine.Events.BaseInvokableCall");
    Type listType = typeof(List<>);
    Type baseInvokableCallListType = listType.MakeGenericType(baseInvokableCallType);

    FieldInfo callsField = unityEventType.GetField("m_Calls", BindingFlags.Instance | BindingFlags.NonPublic);
    FieldInfo runtimeCallsField = invokableCallListType.GetField("m_RuntimeCalls", BindingFlags.Instance | BindingFlags.NonPublic);

    var calls = callsField.GetValue(unityEvent);
    var runtimeCalls = runtimeCallsField.GetValue(calls);
    PropertyInfo countProperty = baseInvokableCallListType.GetProperty("Count");

    return (int)countProperty.GetValue(runtimeCalls, null);
}