おもちゃラボ

Unityで遊びを作ってます

【Unity】C#のデリゲートとコールバック超入門

C#で時々使われるDelegateは、調べてみると委譲とかラムダ式とかコールバックとか、更に難しい単語が出てきて混乱しますね。そこで、ここではC#のDelegateについて簡潔に解説してみたいと思います!

Delegateとは

Delegateは一言でいうと「メソッドを代入できる型」です。int型の変数には整数を代入できるし、string型には文字列を代入できますね。これと同じでDelegateで作った型にはメソッドを代入できます。

f:id:nn_hokuson:20210901212135p:plain:w350

メソッドを代入できる型があって何が嬉しいかというと、コールバック(Callback)という仕組みがこれで実現できるようになるのです。

コールバックとは

例えばTimerクラスと、それを呼び出すSystemクラスがあり、Timerのカウント0になったらSystemクラスにそれを伝えたい場合を考えます。このカウントが終わったことを伝える矢印が、コールバックと呼ばれる仕組みになります。

これを実現するには、予めTimerクラスのdelegateにカウントダウン後に呼び出したいメソッドを設定しておきます。カウントが0になったときにdelegateを通してこのメソッドを実行することで、コールバックが実現できるというわけです。

f:id:nn_hokuson:20210901232621p:plain:w550

理屈だけではわかりにくいので、実際にプログラムを書いて確かめてみましょう。

タイマーを作ってみる

Timerクラスを作る

では実際にDelegateを使ってコールバックの仕組みを作ってみましょう。まずはTimerクラスは次のようになります。

public class Timer 
{
    public delegate void OnCompleteDelegate();
    public OnCompleteDelegate onComplete;
    
    public void CountDown()
    {
        // 本当はちゃんと3秒かけてカウントする
        Debug.Log(3);
        Debug.Log(2);
        Debug.Log(1);
        
        // コールバック
        onComplete();
    }
}

ここではTimerクラスの中でdelegateを使って「OnCompleteDelegate」という型を作っています。 デリゲート型の定義はdelegate キーワードを用いて次のように記述します。

delegate 戻り値の型 デリゲートの型名(引数);

イメージ的にはClassやStructで型を作るのと同じですね。次の行では、いま作ったOnCompleteDelegate型を使ってonComplete変数を作成しています。CountDownメソッドの中では、カウントが0になったタイミングでonCompleteに登録したメソッドを呼び出しています。

呼び出し元のクラスを作る

次に呼び出し元のクラスを作ります。

public class System : MonoBehaviour
{
    void Start()
    {
        Timer timer = new Timer();
        timer.onComplete += Alarm;
        timer.CountDown();
    }

    void Alarm()
    {
        Debug.Log("カウントダウン終了");
    }
}

ここではタイマーのインスタンスを作成し、デリゲート(onComplete)にAlarmメソッドを登録しています。その後CountDownメソッドを呼び出すことで、カウントダウンが開始され、最後にonCompleteデリゲートに登録したAlarmメソッドが呼び出されます。

実行してみる

これを実行すると、コンソールには次のように表示されます。

f:id:nn_hokuson:20210901233355p:plain:w300

UnityEventを使って作り直す

Delegateを使うとコールバックの仕組みが作れることが分かりました。ただ、Delegateで型を宣言してから、変数を作るところが少しメンドウですね・・・。そこでUnityにはUnityEventという仕組みが用意されていて、コールバックを簡単に記述できるようになっています。

UnityEventを使って書き換えてみましょう。まずTimerクラスは次のようになります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events; // 必要!

public class Timer 
{
    public UnityEvent onComplete = new UnityEvent();
    
    public IEnumerator CountDown()
    {
        // 本当はちゃんと3秒かけてカウントする
        Debug.Log(3);
        Debug.Log(2);
        Debug.Log(1);

        // コールバック
        onComplete.Invoke();
    }
}

まずUnityEventクラスを使うため、「using UnityEngine.Events;」を追加しています。またdelegateの代わりにUnityEventクラスの変数を作成しています。カウントダウン後はonComplete変数に登録したメソッドを呼び出すため、UnityEngineのInvokeメソッドを実行しています。

また、Timerを呼び出す側のSystemクラスは次のようになります。

public class System : MonoBehaviour
{
    void Start()
    {
        Timer timer = new Timer();
        timer.onComplete.AddListener(Alarm);
        timer.CountDown();
    }

    void Alarm()
    {
        Debug.Log("カウントダウン終了");
    }
}

delegatreを使ったときはonCompleteにメソッドを直接代入していましたが、UnityEventを使う場合はAddListenerメソッドを使ってコールバックに使うメソッドを指定します。

実行してみて、カウントダウン後に「カウントダウン終了」と表示されるかを確かめてみてください!