uGUIから読み解くdelegate
UnityでButtonのクリック時のコールバックメソッドをスクリプトから定義する時
button.onClick.AddListener(callbackMethod);
のような感じで書くと思います。調べると今まで曖昧だったデリゲートと関連することが分かったので、調べたことを記事にしたいと思います。
記事を書こうと思ったら、すでに模範解答のような記事を見つけました。
良記事なので、そちらを参照してみても良いかもしれません。
学び後の理想の状態
- 「Delegateって何ですか?」と聞かれた時に、しっかりと答えることができる
- UnityActionとは何かについて理解している
現状
- そもそもDelegateとは何かよく分かっていない
- uGUIのコールバックメソッドのスクリプトからの登録の仕方は分かっているけど、本当のところどういう仕組みで動いているのか分かっていない。
AddListener()とは
AddListenerはUnityEventクラスのパブリックメソッドで
publicvoidAddListener(Event.UnityActioncall)
のように定義されています。ランタイムコールバックの追加をこの関数はしています。
注目すべきは引数にUnityAction型のコールバックメソッドを指定しているところです。
一体UnityActionとは何なのでしょうか。
UnityActionとは
ではUnityActionの定義を見てみましょう。
publicdelegatevoidUnityAction();
となっています。つまり戻り値、引数なしのdelegateにすぎないと分かります。
UnityActionを理解するにはデリゲートについて理解していないとだめですね。
デリゲートとは
デリゲートの定義
まずは言葉の定義から。Microsoft公式ドキュメントより
デリゲートは、特定のパラメーター リストおよび戻り値の型を使用して、メソッドへの参照を表す型です。
デリゲート自体はあくまでメソッドへの参照を表す型。
1つ簡単な例をあげておきます。
usingSystem;namespacedelegateTest{classProgram{// 1. まずはデリゲートを定義します。(デリゲートはあくまでメソッドへの参照を表す型です)publicdelegatevoidSimpleCalc(inta,intb);// SimpleCalcデリゲートと同じ戻り値の型とパラメーターリストであること関数を用意staticvoidAdd(inta,intb){Console.WriteLine($"{a} + {b} = {a+b}");}staticvoidSubtract(inta,intb){Console.WriteLine($"{a} - {b} = {a-b}");}staticvoidMain(string[]args){// 2. デリゲートをインスタンス化。同じ戻り値の型、パラメーターリストを持つ関数を代入SimpleCalcdel_calc=Add;// 3. デリゲートインスタンスを通じて関数(この場合Add)を呼び出すdel_calc(20,10);// 2, 3繰り返すdel_calc=Subtract;del_calc(20,10);}}}
20 + 10 = 30
20 - 10 = 10
定義したデリゲートをインスタンス化する時、同じ戻り値の型、引数リストを持つ関数を参照することができ、参照された関数はそのデリゲートインスタンスを通して呼び出されるということですね。他にもデリゲートはクラスメソッド、インスタンスメソッドのどちらも参照することができます。
また便利な機能として、マルチキャストデリゲートと言って複数のメソッドを代入することができます。
デリゲートの使い所
どんな状況でデリゲートの力が発揮されるのか分かれば、デリゲートへの理解も深まると思います。(実際、僕自身デリゲートをわざわざ使う理由が中々分かりませんでした。)
「++C++; // 未確認飛行 C」のデリゲートの利用例を参考にすると、
- 述語(条件式を外から挿す)
- コールバック(非同期処理の終了通知)
- イベント処理
などが主な利用例だそうです。今回理解したいUnityActionがイベント処理に用いられているのもここからわかりますね。後のサンプルを見ると、よりデリゲートのイメージが湧くかもしれません。
結局デリゲートとは
デリゲートを引数や戻り値として用いることで、デリゲートを通して関数をもっと自由に扱えるようになる(関数を呼ぶタイミングを調整できたり、実行する関数を状況に応じて変更できたり)ことがデリゲートを使う利点なのではないでしょうか。
サンプル
サンプルとして、ボタンを押したらwebから画像を引っ張ってきて、それをシーン上のimageUIに貼り付けるということをしてみます。
canvasに以下のUIController.csというスクリプトを貼り付けます。
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;usingUnityEngine.UI;usingUnityEngine.Networking;publicclassUIController:MonoBehaviour{[SerializeField]stringimagePath;publicButtonbutton;publicImageimage;privatevoidStart(){button.onClick.AddListener(OnButtonClicked);}privatevoidOnButtonClicked(){StartCoroutine(LoadImage(imagePath,DisplayImage));}privatevoidDisplayImage(Texture2Dtex2D){Rectrect=newRect(0f,0f,tex2D.width,tex2D.height);image.sprite=Sprite.Create(tex2D,rect,Vector2.zero);}publicdelegatevoidImageProcessing(Texture2Dtexture);IEnumeratorLoadImage(string_imagePath,ImageProcessing_callback){UnityWebRequestrequest=UnityWebRequest.Get(_imagePath);yieldreturnrequest.SendWebRequest();if(request.isNetworkError){Debug.LogError(request.error);}else{Texture2Dtex2D=newTexture2D(400,200);tex2D.LoadImage(request.downloadHandler.data);_callback(tex2D);}}}
まずは、
button.onClick.AddListener(OnButtonClicked);
UnityAction型のデリゲートにコールバック関数としてOnButtonClickedを渡す。
そうすることでボタンがクリックされた時に、OnButtonClickedが呼ばれることになります。
特に解説するようなことは何も書いていないのですが、LoadImage関数の引数を見ると、ここでも自分で定義したデリゲートを使用しています。
// public delegate void ImageProcessing(Texture2D texture)IEnumeratorLoadImage(string_imagePath,ImageProcessing_callback)
第二引数のコールバック関数には、戻り値の型と引数リストさえ合っていれば良いので、
privatevoidSave(Texture2Dtexture){File.WriteAllBytes(Application.dataPath+"/savedImage.png",texture.EncodeToPNG());}
のような画像を保存するような関数を渡したりもできますね。
少し発展
AddListenerで登録したOnButtonClicked()関数なのですが、
privatevoidOnButtonClicked(){StartCoroutine(LoadImage(imagePath,DisplayImage));}
わざわざコルーチンを実行するためだけに関数を定義したくないというのが本音です。
そこで匿名関数(匿名メソッドとラムダ式があるが、今回はラムダ式)を用いると、インラインで処理内容を記述できるようになります。
button.onClick.AddListener(()=>StartCoroutine(LoadImage(imagePath,DisplayImage)));
匿名関数、ラムダ式に関しては【LINQの前に】ラムダ式?デリゲート?Func?な人へのまとめ【知ってほしい】を参照してくだされば分かるかと。
また、LoadImage関数でも
// public delegate void ImageProcessing(Texture2D texture);IEnumeratorLoadImage(string_imagePath,ImageProcessing_callback)
と引数にImageProcessingというデリゲートを指定していますが、このためだけにデリゲートを作るのもあまり格好が良いとは言えませんし、そもそもデリゲートって型なので名前をつけるのちょっと難しいんですよね。今回それを解決してくれるのが、 Action<T>型のデリゲートです。(Func, Action系のデリゲートに関しても先ほどのリンクをみていただけるとわかります。)
Action<T>型を用いて変更すると、
usingSystem;// デリゲートをわざわざ定義しなくて良いIEnumeratorLoadImage(string_imagePath,Action<Texture2D>_callback){}
これまたスッキリ。
まとめ
今回はbuttonを扱いましたが、sliderとかでも基本一緒だと思います。
デリゲートはまだまだ奥が深く勉強しがいがありそうです。以下のことを今後調べたいと思います。
- Func, Action系デリゲート
- 匿名関数
- LINQ
Qiita書いたのほぼ初めてだったのですが、学んだことを言語化して記事にするのって中々大変なのだなと感じました。
いつも読むばかりだったので、これからは継続的に学んだことのアウトプットとして記事を書いていけたらなと思います。
最後の方は、体力切れてしまったので、少しあやふやな部分が多いです。もし誤り等あればご指摘お願いします。また、以下の観点からコメント頂けると嬉しく思います!
- 「デリゲートの使い方、こんなのあるよ!」
- 「デリゲート周りだと、〜とか調べてみたら面白いよ!」
参考資料
以下今回調べるに当たってお世話になった参考資料です。
ありがとうございました。インターネット万歳。
(MSDN)
Delegate Class
Delegate(C# Programming Guide)
MulticastDelegate Class
(++C++; // 未確認飛行 C)
デリゲート
デリゲートの利用例
デリゲートの内部
(Qiita)
【LINQの前に】ラムダ式?デリゲート?Func?な人へのまとめ【知ってほしい】
(Unity Documentation)
Unity Action
Button.onClick
UnityEvent.AddListener