🎫
【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>
はint
→string
)
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