Unity のイベント3種を使い分ける(Observer パターン)
疎結合の要になる Observer パターンを、Unity の3つの実現方法——C# event / UnityEvent / ScriptableObject イベント——で比較し、使い分けの基準を示す。
「HP が減ったら UI を更新する」のように、通知する側が受け取る側を直接知らずに済ませるのが Observer パターンです。Unity にはこれを実現する方法が3つあり、それぞれ得意が違います。この記事では C# event / UnityEvent / ScriptableObject イベントを比較し、使い分けの基準を示します。
3つの選択肢
まず全体像です。同じ「通知」でも、コードで完結させたいのか、デザイナーが Inspector で繋ぎたいのか、シーンやプレハブを跨いで疎結合にしたいのかで選択が変わります。
| 方法 | 繋ぐ場所 | 向く場面 |
|---|---|---|
| C# event | コードのみ | 開発者内で完結、最速・最軽量 |
| UnityEvent | Inspector | デザイナー連携、プレハブ内の配線 |
| ScriptableObject イベント | アセット | シーン/プレハブを跨ぐ疎結合 |
C# event —— まずこれ
純粋な C# の event / delegate がそのまま使えます。最速・最軽量で、開発者間で閉じるなら第一候補です。
public class Health : MonoBehaviour
{
public event Action<int> OnDamaged; // 通知する側
public void Damage(int amount)
{
OnDamaged?.Invoke(amount); // 購読者を知らずに通知
}
}
public class HealthUI : MonoBehaviour
{
[SerializeField] Health health;
void OnEnable() => health.OnDamaged += Refresh;
void OnDisable() => health.OnDamaged -= Refresh; // 解除を忘れない
void Refresh(int amount) { /* UI 更新 */ }
}
⚠️ 購読解除を忘れるとリークする。破棄済みオブジェクトが購読に残り、通知のたびに例外や多重実行を招く。
OnEnable/OnDisableで+=と-=を対にするのが鉄則。
🧭 これは純粋な C# の
eventそのもの。Unity 特有なのは「解除をオブジェクトの寿命に紐づける」点で、UniRx のAddToや UniTask のキャンセルと同じ発想(UniRx 入門)。
UnityEvent —— Inspector で繋ぐ
UnityEvent はシリアライズ可能で Inspector に表示され、コードを書かずに配線できます。デザイナーやレベルデザイナーとの連携に向きます。
using UnityEngine.Events;
public class Button : MonoBehaviour
{
[SerializeField] UnityEvent onPressed; // Inspector に一覧で出る
void Press() => onPressed.Invoke();
}
💡 C# event より低速でアロケーションもある。毎フレーム大量に呼ぶ用途には向かない。「たまに起きるイベントを Inspector で繋ぐ」のが得意分野。
🧭 UnityEvent は純粋な C# に対応物がない、Unity 独自のシリアライズ可能なイベント。デザイナーが GUI で購読先を指定できるのが最大の利点。
ScriptableObject イベント —— 跨いで繋ぐ
イベントを ScriptableObject アセットとして作り、それをチャンネルにする方式です。通知側も購読側も「そのアセット」だけを参照するので、互いを直接参照せずシーンやプレハブを跨いで繋がります。
[CreateAssetMenu(menuName = "Events/GameEvent")]
public class GameEvent : ScriptableObject
{
readonly List<Action> listeners = new();
public void Raise() { foreach (var l in listeners) l(); }
public void Register(Action l) => listeners.Add(l);
public void Unregister(Action l) => listeners.Remove(l);
}
PlayerDied のようなアセットを1つ作れば、敵・UI・サウンドが互いを知らずに同じイベントへ集まれます。プレハブ単体でも成立するので、大きめのプロジェクトの疎結合に効きます。
⚠️ ここでも
Register/Unregisterの対は必須。ScriptableObject はシーンをまたいで生き続けるため、解除漏れは C# event 以上に尾を引く。
使い分けのまとめ
- Observer パターンは「通知側が購読側を直接知らない」疎結合の基本形。Unity では実現方法が3つある。
- C# event: 最速・最軽量。開発者内で完結するならまずこれ。
OnEnable/OnDisableで解除を対にする。 - UnityEvent: Inspector で配線でき、デザイナー連携に強い。低速なので高頻度呼び出しには不向き。
- ScriptableObject イベント: アセットをチャンネルにし、シーン/プレハブを跨いで疎結合にできる。中〜大規模向け。
- どれも共通して 購読解除をオブジェクトの寿命に紐づけるのが要。
次にやること: このシリーズの他パターン(Singleton / Object Pool / State machine)と組み合わせると、状態変化を ScriptableObject イベントで放送する、といった構成に発展できる。