プレイヤーがダメージを受けたら、体力バーを更新して、悲鳴を再生して、カメラを揺らして、ゲームオーバー判定もして——気づけばプレイヤーのスクリプトが UIManager も AudioManager も CameraManager も全部知っている。「1つ直すと全部壊れる」コードのできあがりです。
この「全員が全員を知っている」状態(密結合)を断ち切るのが、C#の デリゲート (delegate) と イベント (event) を使った イベント駆動設計 です。プレイヤーは「ダメージを受けた!」と叫ぶだけ。誰がそれを聞いているかは知らなくていい——この記事では、その仕組みと実装方法を解説します。
この記事でわかること
- 直接参照だらけの「密結合」が抱える3つの問題
- デリゲート=「メソッドへの参照」、イベント=「安全なラッパー」という関係
OnEnableで購読・OnDisableで解除、という鉄則の理由- 体力システムを題材にした疎結合な実装例
なぜ直接参照だとダメなのか?
最も素朴な実装は、プレイヤーのスクリプトがUIManager、AudioManager、CameraManager、GameManagerなどをすべて直接参照し、それぞれのメソッドを呼び出す方法です。しかしこのアプローチ(密結合)には、多くの問題があります。
- 依存関係の複雑化: プレイヤースクリプトが、本来知る必要のないUIやオーディオといった他の多くのシステムの存在を前提にしてしまいます。
- 拡張性の低下: 新しく「実績システムに通知する」という処理を追加したい場合、プレイヤースクリプトを修正する必要があります。
- 再利用性の低下: このプレイヤーの仕組みを敵キャラクターに流用しようとしても、UIやカメラなど、敵には不要な依存関係が絡んできて簡単には再利用できません。
イベント駆動設計の基本的な考え方は、「何か(イベント)が起きたことを宣言する側」と、「そのイベントが起きたら何か(処理)をしたい側」を完全に分離することです。

デリゲート (Delegate) とは?
デリゲートは、一言で言えば「メソッドへの参照を保持できる型」です。デリゲートを使うと、メソッドをあたかも変数であるかのように、他のメソッドに渡したり、クラスのフィールドとして保持したりできます。
// まず、デリゲートの「型」を定義します
// これは「戻り値がvoidで、int型の引数を一つ取るメソッド」の型定義です
public delegate void MyDelegate(int number);
public class DelegateExample
{
public void Run()
{
// デリゲート型の変数に、メソッドを代入する
MyDelegate myDelegate = PrintNumber;
myDelegate += PrintDoubleNumber; // マルチキャストデリゲート:複数のメソッドを登録できる
// デリゲートを呼び出す(登録されたメソッドがすべて呼ばれる)
myDelegate(5);
// 出力:
// Number: 5
// Double Number: 10
}
void PrintNumber(int num) { Debug.Log($"Number: {num}"); }
void PrintDoubleNumber(int num) { Debug.Log($"Double Number: {num * 2}"); }
}
補足: 実際の開発では、
delegateを毎回自分で定義する代わりに、.NET標準の汎用デリゲート型Action(戻り値なし)やFunc(戻り値あり)を使うのが主流です。Action<int>は「int引数1つ・戻り値なしのメソッド」を表します。
イベント (Event) とは?
デリゲートは強力ですが、publicなデリゲート変数は、クラスの外部から自由に呼び出したり(myDelegate(5))、登録されたメソッドをまるごと消したり(myDelegate = null)できてしまい、カプセル化が不十分です。
イベント (event) は、このデリゲートをラップし、クラスの外部からはメソッドの登録(+=)と解除(-=)しかできないように制限する仕組みです。イベントの発行(呼び出し)は、そのイベントを宣言したクラスの内部からしか行えません。これにより、安全なイベント通知パターンを実装できます。
Unityでの実践例:疎結合な体力システム
プレイヤーの体力システムをイベント駆動で実装してみましょう。コードの前に、まず全体像を図で掴んでください。
