C#だとコールバック関数を簡単に設定できて便利ですよね。
C++を学生時代に使っていた際に関数ポインタなどでつまづいた覚えがあります。
C#ではデリゲートを使うことで簡単に設定できます。
さて、UnityではuGUIが出来たころからUnityAction
とUnityEvent
というものが追加されました。
今まで.NETのSystem.Action
を使用していたところを特に考えもなしに置き換えていった記憶があります。
先日UnityEvent
の使い方についての質問をされた際に、UnityEvent
を詳しくわかって使っていないことに気づかされました。
うーん、よくありませんね。
というわけで今回はUnityEvent
についていろいろ調査してみました。
この記事にはUnity5.4.0f3を使用しています。
- UnityActionとSystem.Actionの違いはなんなのさ
- uGUIのButtonでおなじみのUnityEvent
- エディタ上でUnityEventにはどんな関数が指定できるんだろう
- スクリプト上でUnityEventを使ってみよう
- Inspcector上で指定した関数とスクリプトで指定した関数は別扱いになる
- Dynamic と StaticParameters
UnityActionとSystem.Actionの違いはなんなのさ
UnityActionの話の前に、デリゲートの機能については以下の記事が参考になると思います。
是非そちらをご参考ください。
UnityActionは名前空間UnityEngine.Events
にあるデリゲートです。
docs.unity3d.com
System.Actionは.NETにあるデリゲートです。
https://msdn.microsoft.com/ja-jp/library/system.action(v=vs.110).aspx
この両者の違いって何なのでしょう。存在する名前空間が違うだけなのでしょうか。
デリゲートの形としても、返り値なしで、引数の型と数はジェネリック型で複数個指定できる点も同じです。
- 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
部分でしょうか。
UnityEventがとても身近な存在に感じられてきました。
さらにOnClick
で見られるこの特殊なエディタ部分が自前でエディタ部分を作成しなくても、UnityEventの変数をシリアライズするだけで表示されるのです。
using UnityEngine; public class DemoEvent : MonoBehaviour { [SerializeField] private UnityEngine.Events.UnityEvent m_events = new UnityEngine.Events.UnityEvent(); } // class DemoEvent
おお、素晴らしい。
これでエディタ上でコールバック関数をガンガン設定できそうですね。
エディタ上で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
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
表示されているのは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
各引数の型の関数がちゃんと表示されています。
オーバーロードした関数でも大丈夫なようです。
デフォルト引数は設定しても無視される
関数に引数にデフォルト引数を当てはめた場合はどうなるのでしょうか。
using UnityEngine; public class TestObject : MonoBehaviour { public void TestA( int i_arg0 = 10 ) { Debug.LogFormat( "Test A:{0}", i_arg0 ); } } // class TestObject
関数は指定できるようです。
ただデフォルト引数を指定したところで、エディタ上でパラメータを指定しないことはできないため、意味をなしません。
指定できる引数の型はシリアライズできるものは大抵使える
最後にどんな型を引数とする関数なら利用できるのでしょうか。
int, string, floatは今まで実験で使用できました。
GameObjectやTransformなどはどうなのでしょう。
まとめて調べてみましょう。
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
思いつく限りの型を指定してみました。
GameObject, Animation, Camera......大体全部表示されている気が……、
あっ、Colorがない!
structはダメなのかもしれません。
あと、自分で作成したclassやstructは表示されませんでした。
制限こそありますが、いろいろ使えそうですね。
スクリプト上でUnityEventを使ってみよう
ではスクリプト上でもUnityEventにコールバック関数を登録し使ってみましょう。
UnityEventでは以下の4つの関数をよく使うと思います。
void AddListener( UnityAction call );
コールバック関数を登録。
docs.unity3d.comvoid RemoveListener( UnityAction call );
コールバック関数を削除。
docs.unity3d.comvoid RemoveListener();
登録されているコールバック関数をすべて削除。
docs.unity3d.comvoid 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
TestObjectにint, float, string型の引数を指定する関数を作成し、Inspector上でUnityEventに登録します。
スクリプト上でもint型の引数を指定する関数を登録します。
Inspctor上でint型を指定する関数には5を引数として渡すように指定し、スクリプト上のInvoke()
では100を渡して実行します。
結果としては……
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
に登録する場合は以下の画像のようになります。
まあ、いつも通りですね。
次にm_stringEvent
に登録してみましょう。
Dynamicという先ほどにはなかった項目が追加されていることが確認できます。
m_stringEvent
の方はDynamicの方を選択し登録しましょう。
Dynamicで登録した方にはInspector上で引数を指定することが出来ません。
頂いた情報によると、Dynamicで登録した関数に対しては、スクリプト上で引数を指定できるということです。
早速試してみましょう。
EventComponent
クラスに以下の関数を追加しましょう。
void Start() { Debug.Log( "DefaultEvent Invoke" ); m_defaultEvent.Invoke(); Debug.Log( "StringEvent Invoke" ); m_stringEvent.Invoke( "Test" ); }
実行してみます。
DefaultEvent Invoke
TestComponent:Inspector
StringEvent Invoke
TestComponent:Test
m_stringEvent.Invoke()
の方は、スクリプト内で指定した文字列が表示されていることが確認できます。
では、m_stringEventにStaticParametersとしてもInspector上で関数を登録して実行してみましょう。
どうなるでしょうか。
DefaultEvent Invoke
TestComponent:Inspector
StringEvent Invoke
TestComponent:Test
TestComponent:Event
StaticParametersとして登録した場合は、Inspector上で指定した文字列が表示されていることが確認できました。
これを使いこなせれば、いろいろなことが出来そうですね!
それにしても随分と長文になってしまった。
用法用量を正しく守ってUnityEventを使ってくださいねー。