delegate, event, System.Action を使ったイベント駆動型プログラミング

今回のテーマ

  1. デリゲート(委任)
  2. event
  3. Action (System.Action), Func (System.Func)

準備

  1. LearnDelegateAndEvent.unitypackage をダウンロードして Unity のプロジェクトにインポートする
  2. Cinemachine パッケージをプロジェクトに追加する(既に入っているならこれは必要ない)

デリゲート

デリゲートを一言で言うと「メソッドを表す型」である。型であるから、その型の変数を宣言することができる。デリゲート型の変数には「メソッド」を入れることができる。変数に入れておいたメソッドは、後で呼び出すことができる。

デリゲートを使うには、以下のように書く。

// 定義
delegate void MyDelegateMethod();
public delegate void MyPublicDelegateMethod(int i);

// 宣言
MyDelegateMethod dm = default;
public MyPublicDelegateMethod pdm = default;

// メソッドの宣言
void Method1()
{
    Debug.Log("Method1 called.");
}

void Method2(int num)
{
    Debug.Log(num);
}

void Start()
{
    // デリゲート型変数へのメソッドの追加
    dm += Method1;
    pdm += Method2;

    // 変数に入っているメソッドを呼び出す
    dm();
    pdm(5);

    // デリゲート型変数からメソッドを削除する
    dm -= Method1;
    pdm -= Method2;
}

ここで以下のような注意点がある。

  1. デリゲートは型なので、変数に入れる関数は型が一致している必要がある。型として一致している必要があるのは「戻り値の型・引数の型・引数の数」である。(※)
  2. デリゲート型の変数にはメソッドを複数入れることができる。上記の例では演算子 += で代入しているが、これは「追加」である。= で代入すると、既に追加されたメソッドを全部取り除いて代入する。
  3. デリゲート型の変数にメソッドが複数入っている時に、変数に入っているメソッドを呼び出すと、入っている全てのメソッドが呼び出される。これを「マルチキャストデリゲート」という。(『独習 C#』 10.1.3 マルチキャストデリゲート を参照)
  4. 演算子 -= を使うと、デリゲート型の変数に入っているメソッドを削除することができる
  5. デリゲート型を宣言する時は戻り値の型を void にすることが多い。戻り値を返すこともできるが、注意点がある。(『独習 C#』 10.1.3 マルチキャストデリゲート の補足「戻り値のあるメソッドの挙動」を参照)

(※)従って以下のコードのようなことはできない。「一致するオーバーロードがない」としてコンパイルエラーになる。

delegate void MyDelegateMethod();   // 引数なし
MyDelegateMethod dm = default;

void Method1(int i) // 引数あり
{
    Debug.Log(i);
}

void Start()
{
    dm += Method1;  // 引数なしのデリゲート型に引数ありのメソッドを入れようとしている(コンパイル エラーになる)
}

参考資料

  • 独習 C# - 10.1.1 デリゲートの基本 ~ 10.1.3 マルチキャストデリゲート

使用例1 - ゲームを一時停止する

1 Delegate/Pause シーンを実行し、ESC キーを押すとボールが一時停止する。もう一度押すと再開する。

ボールには BallController3D コンポーネントがアタッチされているが、この中でデリゲートを使って「一時停止・再開」する関数を PauseManager3D コンポーネントに登録している(PauseManager3D が持つデリゲート型の変数に、関数を追加している)。BallController3D は、ESC が押された時に登録された関数を呼び出している。

試してみましょう

メソッドを削除しない

BallController3D スクリプトの OnDisable にある _pauseManager.OnPauseResume -= PauseResume; をコメントアウトして実行してみましょう。何が起きるか観察し、なぜそれが起きたのかを考えましょう。

メソッドを追加するのではなく、置き換えてみる

BallController3D スクリプトの OnEnable にある

_pauseManager.OnPauseResume += PauseResume;

_pauseManager.OnPauseResume = PauseResume;

に書き変えて実行してみましょう。何が起きるか観察し、なぜそれが起きたのかを考えましょう。

課題 1 ゲームを一時停止する

このシーンでは ESC キーを押すと、デリゲートの仕組みを使った Pause 型(bool 型の引数を一つ受け取る)の関数が呼ばれてボールが止まり、画面 (UI) に "PAUSE" と表示される。もう一度 ESC キーを押すと再開する。

しかし、シーン内に「止まっていない」ものがいくつかある。これをボールや UI と同じようにデリゲートの仕組みを使って止めるように機能を追加せよ。

デリゲートを使う意味

デリゲートを使うと、オブジェクトの参照を減らすことができる。また、「呼ばれる側」から任意の関数を登録し、呼んでもらうことができる。結果的に処理を減らすことができる。

特に、オブジェクト同士が相互に参照している関係は相互依存関係となり、設計上はよろしくない。そういう関係になってしまった場合は、デリゲートを使って「疎」な関係にできないか検討しましょう。

event と System.Action

delegate の宣言時 event キーワードを追加すると、デリゲート型の変数に関数を「追加 (+=)」することしかできなくなり、「置換 (=)」することができなくなる。これによって誤って関数を置換してしまうことを防げる。その他の event キーワードの効果は『独習 C#』11.4.2 イベント/デリゲートの相違点 を参照せよ。

System.Action を使うと、戻り値を返さないメソッドに対するデリゲート型の定義と変数宣言を一度に行うことができる。(戻り値を返したい場合は System.Func を使う)なお、System.Action, System.Func はクラスではなくデリゲートである。

System.Action は以下のように使う。

// 引数なしの場合

/// <summary>ターン開始時に呼ばれるメソッド</summary>
public event Action OnBeginTurn;

/// <summary>
/// ターン開始時に呼ぶ
/// </summary>
public static void BeginTurn()
{
    OnBeginTurn();
}

// 引数あり(一つ)の場合

/// <summary>回復時に呼ばれるメソッド</summary>
public event Action<int> OnHeal; // 引数の型を <> の中に書く

/// <summary>
/// HP を回復する
/// </summary>
/// <param name="healHp">回復する HP の値</param>
public void Heal(int healHp)
{
    // 追加されたメソッドを引数を渡して呼び出す
    OnHeal(healHp);
}

// 引数が n 個の場合は、<> の中に型を列挙する

public event Action<int, string, ... , Tn> OnGameOver;

ゲームプログラミングでは、マネージャーから「何かが起きた時(イベント)」に「たくさんの GameObject」に対して命令をしたい事がよくあり、event と System.Action を組み合わせるパターンはよく使う。

System.Func

System.Action と System.Func の違いは、Action は引数なしだが、Func は引数があり、その型を含んでいる、という点である。System.Func はこの次に扱う「ラムダ式」で頻繁に使われる。

System.Action<T0, T1, T2, ...> の Tn は全て「引数の型」だが、System.Func<T0, T1, T2, ..., Tn, TResult> については「Tn は引数の型で、TResult は戻り値の型」というルールを覚えておくこと。

参考資料

  • 独習 C#
    • 10.1.4 匿名メソッド - 表 10.2 (System.Action, System.Func 等について)
    • 11.4 イベント

使用例2- ローグライクのターン管理

Cinemachine が入っていなかったらインストールしておくこと(インストールされていないと画面がスクロールしない)。2 Event/RougueLikeTurnBasedGridMovement シーンを開き、実行する。WASD で移動できる。自分が一つ行動すると、敵も一つ行動することを確認する。また、End Turn ボタンをクリックすると、移動せずに自分のターンを終了できる。

考えてみましょう

使用例1で使った PauseManager3D と、使用例2で使った TurnManager は、どちらもデリゲートを管理している。しかし、前者は MonoBehaviour を継承し GameObject に追加しているが、後者は MonoBehaviour を継承せず、GameObject に追加されていない。なぜ前者は GameObject に追加しているのか、なぜ後者は追加していないのか。

課題 2 - ターン数を表示する

RougueLikeTurnBasedGridMovement シーンに手を加えて、以下のように、画面に「ターン数」が表示されるように機能を追加せよ。

課題 3 - delegate を System.Action に書き変える

「使用例 1 - ゲームを一時停止する」で使った 1 Delegate/Pause シーンの処理で delegate を使っている所をすべて System.Action に置き換えよ。

その他

System.Action とまったく同じ機能を持った UnityEngine.Events.UnityAction というデリゲートがある。この2つは機能的な違いはないので、どちらを使ってもよい。

キーワード

  1. delegate
  2. マルチキャスト デリゲート
  3. event キーワード
  4. System.Action
  5. System.Func

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です