うら干物書き

ゲームを作っています。

【Unity】UnityEventの用法と用量

 C#だとコールバック関数を簡単に設定できて便利ですよね。
 C++を学生時代に使っていた際に関数ポインタなどでつまづいた覚えがあります。
 C#ではデリゲートを使うことで簡単に設定できます。

 さて、UnityではuGUIが出来たころからUnityActionUnityEventというものが追加されました。
 今まで.NETSystem.Actionを使用していたところを特に考えもなしに置き換えていった記憶があります。

 先日UnityEventの使い方についての質問をされた際に、UnityEventを詳しくわかって使っていないことに気づかされました。
 うーん、よくありませんね。
 というわけで今回はUnityEventについていろいろ調査してみました。


この記事にはUnity5.4.0f3を使用しています。

UnityActionとSystem.Actionの違いはなんなのさ

 UnityActionの話の前に、デリゲートの機能については以下の記事が参考になると思います。
 是非そちらをご参考ください。

kan-kikuchi.hatenablog.com

 UnityActionは名前空間UnityEngine.Eventsにあるデリゲートです。
docs.unity3d.com

 System.Actionは.NETにあるデリゲートです。
Action デリゲート (System)

 この両者の違いって何なのでしょう。存在する名前空間が違うだけなのでしょうか。
 デリゲートの形としても、返り値なしで、引数の型と数はジェネリック型で複数個指定できる点も同じです。

  • UnityActionの種類
namespace UnityEngine.Events
{
    public delegate void UnityAction();
    public delegate void UnityAction<T0>( T0 arg0 );
    public delegate void UnityAction<T0, T1>( T0 arg0, T1 arg1 );
    public delegate void UnityAction<T0, T1, T2>( T0 arg0, T1 arg1, T2 arg2 );
    public delegate void UnityAction<T0, T1, T2, T3>( T0 arg0, T1 arg1, T2 arg2, T3 arg3 );
}
  • System.Actionの種類
namespace System
{
    public delegate void Action();
    public delegate void Action<T>( T obj );
    public delegate void Action<T1, T2>( T1 arg1, T2 arg2 );
    public delegate void Action<T1, T2, T3>( T1 arg1, T2 arg2, T3 arg3 );
    public delegate void Action<T1, T2, T3, T4>( T1 arg1, T2 arg2, T3 arg3, T4 arg4 );
}

 ちなみに.NETのバージョンが4.0以上ならばもっと引数の数が増えるみたいですよ。
 ただ、そんなに多くの引数を使うのかなぁ……。
 まあ確かに、ウィンドウズプログラムをしていた際はこんな数の引数では足りなかったですけど。

 とにかくデリゲートの型としてはUnityActionもSystem.Actionも同じみたいですね。
 既にSystem.Actionとして記述しているコードをわざわざUnityActionに差し替えるほどのことではないかもしれませんね。
 新しくデリゲートを記述するのなら以下のUnityEventと合わせてUnityActionを記述し、コードとしての統一感を出すのがいいのかもしれません。

uGUIのButtonでおなじみのUnityEvent

 今回のお題目であるUnityEventについてです。

 UnityEventってあれでしょ。C#のeventいい感じにクラス化したものでしょ?
 というのが私の見解なわけですが、UnityEventには優れた点があるのですよ。
 eventと違ってUnityEventはクラスのため、Unityエディタのインスペクター上に表示できるのです。あら便利。
 1番よく使われていると思われるのはuGUIのButtonコンポーネントのOnClick部分でしょうか。

f:id:urahimono:20160910193253p:plain

 UnityEventがとても身近な存在に感じられてきました。
 さらにOnClickで見られるこの特殊なエディタ部分が自前でエディタ部分を作成しなくても、UnityEventの変数をシリアライズするだけで表示されるのです。

using UnityEngine;

public class DemoEvent : MonoBehaviour
{
    [SerializeField]
    private UnityEngine.Events.UnityEvent   m_events    = new UnityEngine.Events.UnityEvent();

} // class DemoEvent

f:id:urahimono:20160910193307p:plain

 おお、素晴らしい。
 これでエディタ上でコールバック関数をガンガン設定できそうですね。

エディタ上でUnityEventにはどんな関数が指定できるんだろう

 uGUIのButtonコンポーネントのOnClickにコールバック関数を指定する際に、どんな関数でも指定できるわけではないですよね。
 ではどんな関数ならば指定できるのでしょうか。
 調査してみましょう。

Public関数のみ指定可能

 以下のコンポーネントをUnityEventに登録して表示される関数を見てみましょう。

using UnityEngine;

public class TestObject : MonoBehaviour
{
    public void TestA()
    {
        Debug.LogFormat( "Test A" );
    }

    private void TestB()
    {
        Debug.LogFormat( "Test B" );
    }

    protected void TestC()
    {
        Debug.LogFormat( "Test C" );
    }

    internal void TestD()
    {
        Debug.LogFormat( "Test D" );
    }

} // class TestObject

f:id:urahimono:20160910193321p:plain

 TestA()しか表示されていません。
 指定出来るのはPublic関数のみのようです。

引数は最大でも1つだけ

 引数の数をいろいろ指定してみて実験してみましょう。

using UnityEngine;

public class TestObject : MonoBehaviour
{
    public void TestA()
    {
        Debug.LogFormat( "Test A" );
    }

    public void TestB( int i_arg0 )
    {
        Debug.LogFormat( "Test B" );
    }

    public void TestC( int i_arg0, int i_arg1 )
    {
        Debug.LogFormat( "Test C" );
    }

    public void TestD( int i_arg0, int i_arg1, int i_arg2 )
    {
        Debug.LogFormat( "Test D" );
    }

    public void TestE( int i_arg0, int i_arg1, int i_arg2, int i_arg3 )
    {
        Debug.LogFormat( "Test E" );
    }

} // class TestObject

f:id:urahimono:20160910193331p:plain

 表示されているのはTestA()TestB()ですね。
 コード上では最大で4つまで引数を指定できますが、エディタ上では最大で1つの引数を持つ関数を指定できるようです。

オーバーロードした関数は使用可能

 関数をオーバーロードした場合はどうなるのでしょうか。

using UnityEngine;

public class TestObject : MonoBehaviour
{
    public void TestA()
    {
        Debug.LogFormat( "Test A" );
    }

    public void TestA( int i_arg0 )
    {
        Debug.LogFormat( "Test A" );
    }

    public void TestA( string i_arg0 )
    {
        Debug.LogFormat( "Test A" );
    }

    public void TestA( float i_arg0 )
    {
        Debug.LogFormat( "Test A" );
    }

} // class TestObject

f:id:urahimono:20160910193341p:plain

 各引数の型の関数がちゃんと表示されています。
 オーバーロードした関数でも大丈夫なようです。

デフォルト引数は設定しても無視される

 関数に引数にデフォルト引数を当てはめた場合はどうなるのでしょうか。

using UnityEngine;

public class TestObject : MonoBehaviour
{
    public void TestA( int i_arg0 = 10 )
    {
        Debug.LogFormat( "Test A:{0}", i_arg0 );
    }

} // class TestObject

f:id:urahimono:20160910193349p:plain

 関数は指定できるようです。
 ただデフォルト引数を指定したところで、エディタ上でパラメータを指定しないことはできないため、意味をなしません。

f:id:urahimono:20160910193426p:plain

指定できる引数の型はシリアライズできるものは大抵使える

 最後にどんな型を引数とする関数なら利用できるのでしょうか。
 int, string, floatは今まで実験で使用できました。
 GameObjectTransformなどはどうなのでしょう。
 まとめて調べてみましょう。

using UnityEngine;

public class TestObject : MonoBehaviour
{
    public void Test( int i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( float i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( string i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Transform i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( GameObject i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Animation i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Animator i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( AudioClip i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Camera i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Color i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Collider i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( TestObject i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( ParticleSystem i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Texture i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Sprite i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Renderer i_value )
    {
        Debug.LogFormat( "Test" );
    }

    public void Test( Rigidbody i_value )
    {
        Debug.LogFormat( "Test" );
    }

} // class TestObject

f:id:urahimono:20160910193442p:plain

 思いつく限りの型を指定してみました。
 GameObject, Animation, Camera……大体全部表示されている気が……、
 あっ、Colorがない!
 structはダメなのかもしれません。
 あと、自分で作成したclassやstructは表示されませんでした。

 制限こそありますが、いろいろ使えそうですね。

スクリプト上でUnityEventを使ってみよう

 ではスクリプト上でもUnityEventにコールバック関数を登録し使ってみましょう。
 UnityEventでは以下の4つの関数をよく使うと思います。

  • void AddListener( UnityAction call );
    コールバック関数を登録。
    docs.unity3d.com

  • void RemoveListener( UnityAction call );
    コールバック関数を削除。
    docs.unity3d.com

  • void RemoveListener();
    登録されているコールバック関数をすべて削除。
    docs.unity3d.com

  • void Invoke();
    登録されているコールバック関数を全て実行。
    docs.unity3d.com

 以下のスクリプトのような使い方になると思います。

using UnityEngine;

public class DemoEvent : MonoBehaviour
{
    [SerializeField]
    private UnityEngine.Events.UnityEvent   m_events    = new UnityEngine.Events.UnityEvent();

    void OnEnable()
    {
        m_events.AddListener( CallbackMethod );
    }

    void OnDisable()
    {
        m_events.RemoveListener( CallbackMethod );
    }

    void Update()
    {
        m_events.Invoke();
    }

    void CallbackMethod()
    {
        Debug.LogFormat( "CallbackMethod" );
    }

} // class DemoEvent

 簡単そうですね。
 ただ問題は引数を指定したUnityEventを使う際なのです。

 UnityEventにもUnityAction同様の最大で4つの引数を指定することができます。
 こんな感じですね。
 private UnityEngine.Events.UnityEvent<int> m_events = new UnityEngine.Events.UnityEvent<int>();

 ちなみにこれをやるとコンパイルエラーが発生します。なぜでしょう。
 エラー内容を見てみます。

Error CS0144 Cannot create an instance of the abstract class or interface ‘UnityEvent

 あ、abstract型だとぉ!
 引数を指定するUnityEventの宣言部分を見てみましょう。

namespace UnityEngine.Events
{
    //
    // Summary:
    //     ///
    //     One argument version of UnityEvent.
    //     ///
    public abstract class UnityEvent<T0> : UnityEventBase
    {
        [RequiredByNativeCodeAttribute]
        public UnityEvent();

        public void AddListener( UnityAction<T0> call );
        public void Invoke( T0 arg0 );
        public void RemoveListener( UnityAction<T0> call );
        protected override MethodInfo FindMethod_Impl( string name, object targetObj );
    }
}

 本当だ、abstractになっていやがる。
 そうなんです。引数を指定する型のUnityEventを継承したクラスを作成しそちらを使用する必要があるのです。
 急に面倒くさくなってきたなぁ。

using UnityEngine;

public class DemoEvent : MonoBehaviour
{
    [System.Serializable]
    public class DemoCallback : UnityEngine.Events.UnityEvent<int>
    {

    }

    [SerializeField]
    private DemoCallback    m_events    = new DemoCallback();

    void OnEnable()
    {
        m_events.AddListener( CallbackMethod );
    }

    void OnDisable()
    {
        m_events.RemoveListener( CallbackMethod );
    }

    void Update()
    {
        m_events.Invoke( 100 );
    }

    void CallbackMethod( int i_arg )
    {
        Debug.LogFormat( "CallbackMethod arg={0}", i_arg );
    }

} // class DemoEvent

 この形ならOKです。
 引数ありの場合のUnityEventの場合には、Invoke()にて引数を渡してあげる必要があります。

 ふぅ、長かった。
 えーと、あと気になる点はなかったかな。
 ん?
 いままで調査結果を説明していっておかしいところがあるぞ。

Inspcector上で指定した関数とスクリプトで指定した関数は別扱いになる

 最初Inspcector上で指定できる関数を調査した際のUnityEventの型は引数なしです。
 にも拘わらず、いろいろな型の関数を指定できました。
 そう、UnityEventの恐ろしくもすごいところはこの点です。
 Inspcector上で指定できる関数の型とスクリプトで指定できる関数の型が同じである必要はないのです。

 次の例を見てみましょう。

TestObject.cs

using UnityEngine;

public class TestObject : MonoBehaviour
{
    public void Test( int i_arg )
    {
        Debug.LogFormat( "Test i_arg={0}", i_arg );
    }

    public void Test( float i_arg )
    {
        Debug.LogFormat( "Test i_arg={0}", i_arg );
    }

    public void Test( string i_arg )
    {
        Debug.LogFormat( "Test i_arg={0}", i_arg );
    }
} // class TestObject

DemoEvent.cs

using UnityEngine;

public class DemoEvent : MonoBehaviour
{
    [System.Serializable]
    public class DemoCallback : UnityEngine.Events.UnityEvent<int>
    {

    }

    [SerializeField]
    private DemoCallback    m_events    = new DemoCallback();

    void Start()
    {
        m_events.AddListener( CallbackMethod );
        m_events.Invoke( 100 );
    }

    void CallbackMethod( int i_arg )
    {
        Debug.LogFormat( "CallbackMethod arg={0}", i_arg );
    }

} // class DemoEvent

f:id:urahimono:20160910193703p:plain

 TestObjectにint, float, string型の引数を指定する関数を作成し、Inspector上でUnityEventに登録します。
 スクリプト上でもint型の引数を指定する関数を登録します。
 Inspctor上でint型を指定する関数には5を引数として渡すように指定し、スクリプト上のInvoke()では100を渡して実行します。
 結果としては……

f:id:urahimono:20160910193712p:plain

Test i_arg = 5
Test i_arg = 1.8
Test i_arg = Hello
CallbackMethod arg = 100

 この結果から、Inspector上で指定された関数に対して、スクリプト上で引数を渡すことができないことがわかります。
 Inspector上で指定した関数に渡される引数は、Inspector上で指定した引数のみが有効なのです。
 そのためInspector上でのみUnityEventに関数を指定する場合は、引数なしのUnityEventで十分だということですね。

 と、そんなふうに考えていた時期が僕にもありました。

Dynamic と StaticParameters

 以下 2016/12/06 に追記分

 この結果から、Inspector上で指定された関数に対して、スクリプト上で引数を渡すことができないことがわかります。
Inspector上で指定した関数に渡される引数は、Inspector上で指定した引数のみが有効なのです。
そのためInspector上でのみUnityEventに関数を指定する場合は、引数なしのUnityEventで十分だということですね。

 上記の文に対して、「そんなことないよ!」とコメントをいただきました。
 情報ありがとうございます。

 どうやら私の調査ミスがあったみたいです。
 では以下のコードを使ってテストをしてみましょう。

イベントを登録し、呼び出すコンポーネント
EventComponent.cs

using UnityEngine;
using UnityEngine.Events;

[System.Serializable]
public class StringEvent : UnityEvent< string >
{

}

public class EventComponent : MonoBehaviour
{
    [SerializeField]
    private UnityEvent  m_defaultEvent  = null;

    [SerializeField]
    private StringEvent m_stringEvent   = null;

} // class TestComponent

呼び出される関数を持つコンポーネント
TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    public void Method( string i_message )
    {
        Debug.LogFormat( "TestComponent:{0}", i_message );
    }

} // class TestComponent

 EventComponentクラスには、UnityEventクラスをそのまま変数として使用するm_defaultEventとUnityEventを継承したStringEventクラスを変数として使用するm_stringEventを用意してあります。

 この二種の変数にTestComponentクラスのMethod()関数をInspector上で登録してみましょう。
 どのような違いがあるのでしょうか。

 まずは、m_defaultEventに登録する場合は以下の画像のようになります。
f:id:urahimono:20161206222040p:plain
 まあ、いつも通りですね。

 次にm_stringEventに登録してみましょう。
f:id:urahimono:20161206222047p:plain

 Dynamicという先ほどにはなかった項目が追加されていることが確認できます。
 m_stringEventの方はDynamicの方を選択し登録しましょう。

f:id:urahimono:20161206222051p:plain
 Dynamicで登録した方にはInspector上で引数を指定することが出来ません。

 頂いた情報によると、Dynamicで登録した関数に対しては、スクリプト上で引数を指定できるということです。
 早速試してみましょう。

 EventComponentクラスに以下の関数を追加しましょう。

void Start()
{
    Debug.Log( "DefaultEvent Invoke" );
    m_defaultEvent.Invoke();

    Debug.Log( "StringEvent Invoke" );
    m_stringEvent.Invoke( "Test" ); 
}

 実行してみます。
f:id:urahimono:20161206222107p:plain

DefaultEvent Invoke
TestComponent:Inspector
StringEvent Invoke
TestComponent:Test

 m_stringEvent.Invoke()の方は、スクリプト内で指定した文字列が表示されていることが確認できます。
 では、m_stringEventにStaticParametersとしてもInspector上で関数を登録して実行してみましょう。
 どうなるでしょうか。
f:id:urahimono:20161206222103p:plain
f:id:urahimono:20161206222110p:plain
f:id:urahimono:20161206222113p:plain

DefaultEvent Invoke
TestComponent:Inspector
StringEvent Invoke
TestComponent:Test
TestComponent:Event

 StaticParametersとして登録した場合は、Inspector上で指定した文字列が表示されていることが確認できました。

 これを使いこなせれば、いろいろなことが出来そうですね!

 それにしても随分と長文になってしまった。
 用法用量を正しく守ってUnityEventを使ってくださいねー。