🎫

【Unity,C#】イベント駆動入門【GIFアリ】

に公開

関連記事

背景と補足

  • R3 を導入したいが、そもそもリアクティブプログラミングをあまり知らない
  • 学習するわよ
  • 本記事のコードでは UniRx, R3 を用いずC#の仕様で完結している

イベント駆動とは

用語解説
  • 発行(発火)

    • イベントを起こす側の処理
    • C#では event?.Invoke()OnXxx?.Invoke() のように書く
    • 通知を送るイメージ
  • 購読

    • イベント受け取りを登録する側の処理
    • event += 処理 で書く
    • 「通知がきたら、この関数を呼ぶ」と登録するイメージ
  • 解除

    • イベントを受け取らないようにする処理
    • event -= 処理 で書く
    • Unityでは OnEnable, OnDisable で登録,解除をよくセットにする
  • 出来事に「反応して処理を起こす」のがイベント駆動
  • ポーリングと違い、発生時だけ動作するのが特徴

C#の仕様で何が使えるか

今回解説していないもの
用途
Predicate<T> 条件判定(戻り値はbool)
Comparison<T> 並び順の比較用(戻り値はint)
Converter<TInput, TOutput> 型変換処理
MulticastDelegate 複数の関数を1つに束ねる内部構造(直接使うことは稀)

event

  • この処理は外部から発行できない」ことを明示するために使用
  • Actionなどのデリゲート型と組み合わせて使う
  • 外部からは +=, -= で購読,解除はできるが、発行(Invoke()) は不可
public event Action OnPressed;

public void Press()
{
    OnPressed?.Invoke();
}

Action, Func

  • Action:戻り値ナシの関数参照 (例:Action<int>int を引数に取る)
Action greet = () => Console.WriteLine("Hello!");
greet();
  • Func:戻り値アリの関数参照 (例:Func<int, string>intstring)
Func<int, int> square = x => x * x;
int result = square(3); //9

delegate

  • 関数ポインタのようなもの
  • 独自の構成の関数型を定義できる
public delegate int Calc(int x, int y);

Calc add = (a, b) => a + b;
int result = add(2, 3); //5

Unityの仕様で何が使えるか

  • Unityにもイベントのように使える仕組みがある

メッセージ関数

  • Start, Update, OnTriggerEnterなど様々
  • Unity内部から呼ばれるため、自分で発行することは少ない

I〇〇Handler系 (UnityEngine.EventSystems)

  • イベント駆動的にUI処理を実装できる
  • これもUnity内部から呼ばれ、自分で発行することは少ない
  • 「カーソルが乗った、離れた…」等の処理を簡単に書け、個人的にお気に入り
public class MyClickable : MonoBehaviour, IPointerClickHandler
{
    public void OnPointerClick(PointerEventData eventData)
    {
        Debug.Log("クリックされた");
    }
}

実装例

  • 今回は「画像がクリックされたときに処理を実行する」機能を実装する
  • クリックされたら画像の色をランダムに変更する

ポーリングで実装した場合

public class PollingClickChecker : MonoBehaviour
{
    [SerializeField] private Camera cam;
    [SerializeField] private GameObject spriteObj;
    [SerializeField] private Renderer sprite;

    private void Update()
    {
        if (Input.GetMouseButtonDown(0) == false) return;

        Ray r = cam.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(r, out var hit) &&
            hit.transform.gameObject == spriteObj)
        {
            sprite.material.color = Random.ColorHSV();
        }
    }
}
  • 毎フレーム、入力を監視している

イベント駆動で実装した場合

  • 入力検知側
public class ClickBroadcaster : MonoBehaviour, IPointerClickHandler
{
    public static event Action OnButtonClicked;

    public void OnPointerClick(PointerEventData eventData)
    {
        OnButtonClicked?.Invoke();
    }
}
  • UI側
public class ColorChanger : MonoBehaviour
{
    [SerializeField] private Image img;

    private void OnEnable()
    {
        ClickBroadcaster.OnButtonClicked += ChangeColor;
    }

    private void OnDisable()
    {
        ClickBroadcaster.OnButtonClicked -= ChangeColor;
    }

    private void ChangeColor()
    {
        img.material.color = Random.ColorHSV();
    }
}
  • InputManager がイベント発行、ButtonSystem が購読を行う

比較

  • イベント駆動は、反応時の処理を増やしやすい
  • 各処理がイベントを購読する形になるので、責務が分離されて見通しがよくなる

イベント駆動のメリットまとめ

  • 機能を追加しても、既存コードの変更が少ない
    • += で購読するだけで、機能を足せる
    • 発行側のコードは変更なしでも拡張可能
  • 疎結合になり、再利用やテストがしやすい
    • 発行者と購読者の間に直接的な依存関係がない

参考

C#

Unity

余談

  • ポーリング vs イベント駆動と比較してきたが、実は両者、内部構造が結構違う
    • ポーリングの例:Raycast を使って 3D空間上のオブジェクト を直接判定
    • イベント駆動の例:IPointerClickHandler を使用(uGUI専用の仕組み)
  • つまり、同じことを別の書き方で実装したに見えて、実は使ってるシステムが違う
  • 設計の違いの理解にはそう支障がないことと修正が面倒だったことからそのままにした

Discussion