UnityでButtonのクリック時のコールバックメソッドをスクリプトから定義する時
button.onClick.AddListener(callbackMethod);
のような感じで書くと思います。調べると今まで曖昧だったデリゲートと関連することが分かったので、調べたことを記事にしたいと思います。
記事を書こうと思ったら、すでに模範解答のような記事を見つけました。
良記事なので、そちらを参照してみても良いかもしれません。
学び後の理想の状態
- 「Delegateって何ですか?」と聞かれた時に、しっかりと答えることができる
- UnityActionとは何かについて理解している
現状
- そもそもDelegateとは何かよく分かっていない
- uGUIのコールバックメソッドのスクリプトからの登録の仕方は分かっているけど、本当のところどういう仕組みで動いているのか分かっていない。
AddListener()とは
AddListenerはUnityEventクラスのパブリックメソッドで
public void AddListener(Event.UnityAction call)
のように定義されています。ランタイムコールバックの追加をこの関数はしています。
注目すべきは引数にUnityAction型のコールバックメソッドを指定しているところです。
一体UnityActionとは何なのでしょうか。
UnityActionとは
ではUnityActionの定義を見てみましょう。
public delegate void UnityAction();
となっています。つまり戻り値、引数なしのdelegateにすぎないと分かります。
UnityActionを理解するにはデリゲートについて理解していないとだめですね。
デリゲートとは
デリゲートの定義
まずは言葉の定義から。Microsoft公式ドキュメントより
デリゲートは、特定のパラメーター リストおよび戻り値の型を使用して、メソッドへの参照を表す型です。
デリゲート自体はあくまでメソッドへの参照を表す型。
1つ簡単な例をあげておきます。
using System;
namespace delegateTest
{
class Program
{
// 1. まずはデリゲートを定義します。(デリゲートはあくまでメソッドへの参照を表す型です)
public delegate void SimpleCalc(int a, int b);
// SimpleCalcデリゲートと同じ戻り値の型とパラメーターリストであること関数を用意
static void Add(int a, int b)
{
Console.WriteLine($"{a} + {b} = {a + b}");
}
static void Subtract(int a, int b)
{
Console.WriteLine($"{a} - {b} = {a - b}");
}
static void Main(string[] args)
{
// 2. デリゲートをインスタンス化。同じ戻り値の型、パラメーターリストを持つ関数を代入
SimpleCalc del_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というスクリプトを貼り付けます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
public class UIController : MonoBehaviour
{
[SerializeField] string imagePath;
public Button button;
public Image image;
private void Start()
{
button.onClick.AddListener(OnButtonClicked);
}
private void OnButtonClicked()
{
StartCoroutine(LoadImage(imagePath, DisplayImage));
}
private void DisplayImage(Texture2D tex2D)
{
Rect rect = new Rect(0f, 0f, tex2D.width, tex2D.height);
image.sprite = Sprite.Create(tex2D, rect, Vector2.zero);
}
public delegate void ImageProcessing(Texture2D texture);
IEnumerator LoadImage(string _imagePath, ImageProcessing _callback)
{
UnityWebRequest request = UnityWebRequest.Get(_imagePath);
yield return request.SendWebRequest();
if (request.isNetworkError)
{
Debug.LogError(request.error);
}
else
{
Texture2D tex2D = new Texture2D(400, 200);
tex2D.LoadImage(request.downloadHandler.data);
_callback(tex2D);
}
}
}
まずは、
button.onClick.AddListener(OnButtonClicked);
UnityAction型のデリゲートにコールバック関数としてOnButtonClickedを渡す。
そうすることでボタンがクリックされた時に、OnButtonClickedが呼ばれることになります。
特に解説するようなことは何も書いていないのですが、LoadImage関数の引数を見ると、ここでも自分で定義したデリゲートを使用しています。
// public delegate void ImageProcessing(Texture2D texture)
IEnumerator LoadImage(string _imagePath, ImageProcessing _callback)
第二引数のコールバック関数には、戻り値の型と引数リストさえ合っていれば良いので、
private void Save(Texture2D texture)
{
File.WriteAllBytes(Application.dataPath + "/savedImage.png", texture.EncodeToPNG());
}
のような画像を保存するような関数を渡したりもできますね。
少し発展
AddListenerで登録したOnButtonClicked()関数なのですが、
private void OnButtonClicked()
{
StartCoroutine(LoadImage(imagePath, DisplayImage));
}
わざわざコルーチンを実行するためだけに関数を定義したくないというのが本音です。
そこで匿名関数(匿名メソッドとラムダ式があるが、今回はラムダ式)を用いると、インラインで処理内容を記述できるようになります。
button.onClick.AddListener( () => StartCoroutine(LoadImage(imagePath, DisplayImage)) );
匿名関数、ラムダ式に関しては【LINQの前に】ラムダ式?デリゲート?Func?な人へのまとめ【知ってほしい】を参照してくだされば分かるかと。
また、LoadImage関数でも
// public delegate void ImageProcessing(Texture2D texture);
IEnumerator LoadImage(string _imagePath, ImageProcessing _callback)
と引数にImageProcessingというデリゲートを指定していますが、このためだけにデリゲートを作るのもあまり格好が良いとは言えませんし、そもそもデリゲートって型なので名前をつけるのちょっと難しいんですよね。今回それを解決してくれるのが、 Action<T>型のデリゲートです。Action<T>型のデリゲートは標準ライブラリが用意してくれているデリゲートです。標準ライブラリが用意してくれている主に利用するデリゲートには以下のようなものがあります。
デリゲート型 | 概要 | 構文一例 |
---|---|---|
Action | 値を返さないメソッド | public delegate void Action() |
Func | TResult型の値を返すメソッド | public TResult Func<out TResult>() |
Comparison | 型が同じ2つのオブジェクトを比較するメソッド | public delegate int Comparison<T>() |
Converter | オブジェクトを別の型に変換するメソッド | public delegate TOutput Converter<TInput, TOutput>(TInput input) |
Predicate | 一連の基準を定義し、指定されたオブジェクトがこれらの基準を満たしているかどうかを判断するメソッド | public delegate bool Predicate<in T>(T obj) |
ここではAction<T>型を用いて変更すると、
using System;
// デリゲートをわざわざ定義しなくて良い
IEnumerator LoadImage(string _imagePath, Action<Texture2D> _callback)
{
}
これまたスッキリ。
まとめ
今回はbuttonを扱いましたが、sliderとかでも基本一緒だと思います。
デリゲートはまだまだ奥が深く勉強しがいがありそうです。以下のことを今後調べたいと思います。
- Action, Funcなどの標準ライブラリで用意されているデリゲート
- 匿名関数
- LINQ
Qiita書いたのほぼ初めてだったのですが、学んだことを言語化して記事にするのって中々大変なのだなと感じました。
いつも読むばかりだったので、これからは継続的に学んだことのアウトプットとして記事を書いていけたらなと思います。
最後の方は、体力切れてしまったので、少しあやふやな部分が多いです。もし誤り等あればご指摘お願いします。また、以下の観点からコメント頂けると嬉しく思います!
- 「デリゲートの使い方、こんなのあるよ!」
- 「デリゲート周りだと、〜とか調べてみたら面白いよ!」
参考資料
以下今回調べるに当たってお世話になった参考資料です。
ありがとうございました。インターネット万歳。
(MSDN)
Delegate Class
Delegate(C# Programming Guide)
MulticastDelegate Class
(++C++; // 未確認飛行 C)
デリゲート
デリゲートの利用例
デリゲートの内部
(Qiita)
【LINQの前に】ラムダ式?デリゲート?Func?な人へのまとめ【知ってほしい】
(Unity Documentation)
Unity Action
Button.onClick
UnityEvent.AddListener
(書籍)
『独習C#新版 山田 祥寛著 』