# C#の「イベント」（event）

## 概念

ここまで、C＃で学んだ主な概念は

- メンバー変数：あるインスタンス（オブジェクト）のデータを保持するもの 
    - 体力
    - アイテムの一覧
    - 速度
    - 色
    - など
- メソッド：あるインスタンス（オブジェクト）が「できる」こと。データを処理する「関数」 
    - 走る
    - ジャンプ
    - 攻撃
    - フレーム毎の更新（Update）
    - 初期化（Start）
    - など
- クラス：インスタンスの「図形」であり、`new` (一般のC#) または `Instantiate`（Unity の `Monobehavior` / プレハブ）で具体化し、インスタンスになる。メンバー変数とメソッドのかたまりとも考えらる。 
    - プレーヤー
    - 敵
    - 罠
    - 武器
    - 回復アイテム
    - など

ここで、新しい C#の機能「イベント」を学ぶ。イベントは「<span style="color: rgb(22, 145, 121);">**あるインスタンスが通知できること**</span>」と考えらる。次の例について考えてみてください。

格闘ゲームを開発中に、「体力」（`HitPoints`）を実装しようとする。ここで：

- 人間プレーヤーの体力がゼロになると、プレーヤーの負けになる（ゲームオーバー）
- CPUプレーヤーの体力がゼロになると、プレーヤーの勝ちになる（ゲームクリア）

体力は変わらないので、Enemy と Player のゲームオブジェクトは全く同じ体力を使うので、同じ「`HitPoints`」のスクリプトをアタッチする

```c#
using UnityEngine;

// 体力クラス
public class HitPoints : MonoBehaviour
{
    // 体力の値
    private int hp;

    private void Start()
    {
        hp = 100;
    }

    // 現在の体力を返す
    public int GetHP()
    {
        return hp;
    }

    // ダメージを受ける
    public void Hit(int damage)
    {
        hp = Mathf.Max(0, hp -damage);
    }
}
```

こうすると、ゲーム監督クラス「GameManager」では、どれの勝ちなのか確認したいので、Update で定期的に問い合わせるしかない。

```c#
using UnityEngine;

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // 人間プレーヤーの体力
    [SerializeField] 
    private HitPoints playerHP;

    // CPUプレーヤーの体力
    [SerializeField] 
    private HitPoints cpuHP;

    private void Update()
    {
        // プレーヤーの負け
        if (playerHP.GetHP() == 0)
        {
            Debug.Log("YOU LOSE!");
        }

        // CPUの負け
        if (cpuHP.GetHP() == 0)
        {
            Debug.Log("YOU WIN!");
        }
    }
}
```

この作り方で問題なく、ゲームが動くが、体力が**<span style="color: rgb(186, 55, 42);">変わっていないのに、何度も問い合わせるのは無駄な処理</span>**になる。逆に、体力が**<span style="color: rgb(22, 145, 121);">変わった瞬間</span>**にGameManagerに知らせ、**<span style="color: rgb(22, 145, 121);">処理すれば効率が良い</span>**！

<table border="1" id="bkmrk-update%E3%81%A7%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B-%C2%A0-even" style="border-collapse: collapse; width: 100%; border-width: 0px;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr><td style="border-width: 0px;">**Updateで問い合わせる**

<table border="1" style="border-collapse: collapse; width: 100.374%; border-width: 0px; background-color: rgb(255, 255, 255);"><colgroup><col style="width: 99.7519%;"></col></colgroup><tbody><tr><td style="border-width: 0px;">**![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/pv2image.png)**</td></tr><tr><td style="border-width: 0px;">**![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/ax6image.png)**</td></tr><tr><td style="border-width: 0px;">**![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/4animage.png)**</td></tr></tbody></table>

</td><td style="border-width: 0px;">**event で連絡待ち**

<table border="1" style="border-collapse: collapse; width: 100%; border-width: 0px; background-color: rgb(255, 255, 255);"><colgroup><col style="width: 99.7525%;"></col></colgroup><tbody><tr><td style="border-width: 0px;">**[![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/cZCimage.png)](https://class.illogic.games/uploads/images/gallery/2026-06/cZCimage.png)**</td></tr><tr><td style="border-width: 0px;">**[![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/iznimage.png)](https://class.illogic.games/uploads/images/gallery/2026-06/iznimage.png)**</td></tr><tr><td style="border-width: 0px;">**![image.png](https://class.illogic.games/uploads/images/gallery/2026-06/scaled-1680-/4animage.png)**

</td></tr></tbody></table>

</td></tr></tbody></table>

## 仕組み

イベントを実装するには、２つのクラスが関連する：「イベントが発生する側」と「イベントを聞き取る側」

### イベントを発生する側

イベントを発生するクラスは「通知ができる」ことを**<span style="color: rgb(35, 111, 161);">宣言</span>**しないといけない。または、イベントの条件が満たしたら、イベントを**<span style="color: rgb(35, 111, 161);">発生</span>**しなければならない。

#### 宣言

クラスの先頭に「event」キーワードを使い、イベントを宣言する

<span style="text-decoration: underline;">書式（引数なし）</span>

```c#
public event System.Action イベント名;
```

<span style="text-decoration: underline;">書式（引数あり）</span>

```c#
public event System.Action<引数型1, 引数型2, ...> イベント名;
```

今回の格闘ゲームだと：

```c#
// 体力クラス
public class HitPoints : MonoBehaviour
{
    // 体力がゼロになった時に発生するイベント。
    // 引数はゼロになった体力コンポーネントの参照
    public event System.Action<HitPoints> ZeroHP;
        
    // 体力の値
    private int hp;

    // （後は同じ）
}
```

ここの例だと：

- 「`ZeroHP`」というイベントを宣言した。このイベントは体力がゼロになった瞬間に発生するべき
- 引数の型は「`HitPoints`」の参照である。これでどの体力コンポーネントゼロになったかを引数として渡すできる。

（※）引数は任意であり、必要なときに使ってください。不要なら「引数なし」の宣言方法を利用してください。

#### 発生

条件を満たしたら、イベントを発生しなければならない。これは、「`Invoke`」で行う。

<span style="text-decoration: underline;">書式（引数なし）</span>

```c#
イベント名?.Invoke();
```

<span style="text-decoration: underline;">書式（引数あり）</span>

```c#
イベント名?.Invoke(引数1, 引数2, ...);
```

今回の格闘ゲームだと：

```c#
// ダメージを受ける
public void Hit(int damage)
{
    hp = Mathf.Max(0, hp -damage);
   
    // 体力がゼロになったら、イベントを発生
    // 引数は「this」（自分の参照）
    if (hp == 0)
        ZeroHP?.Invoke(this);
}
```

ここの例だと：

- ダメージを喰ったら、体力がゼロになったのかを確認する
- もしゼロだったら、`ZeroHp` のイベントを発生する。引数は自分（`this`）の参照

### イベントを聞き取る側

イベントを発生するのはただ半分であり、聞き取る側もある。ここで、イベントに**<span style="color: rgb(35, 111, 161);">登録・登録解除</span>**とそのイベントを<span style="color: rgb(35, 111, 161);">**処理**</span>するメソッドを作成しなければならない。

#### 登録・登録解除

まず、定めたイベントを処理するため、先に登録しなければならない。また、イベントが不要になった場合、必ず登録解除してください。

<span style="text-decoration: underline;">書式：登録</span>

```c#
オブジェクト名.イベント名 += イベントを処理するメソッド;
```

<span style="text-decoration: underline;">書式：登録解除</span>

```c#
オブジェクト名.イベント名 -= イベントを処理するメソッド;
```

#### 処理

処理は上記の「イベントを処理するメソッド」で行う。**<span style="color: rgb(186, 55, 42);">引数はイベント宣言の引数を一致しなければならない</span>**。

今回の格闘ゲームだと：

```c#
using UnityEngine;

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // 人間プレーヤーの体力
    [SerializeField] 
    private HitPoints playerHP;

    // CPUプレーヤーの体力
    [SerializeField] 
    private HitPoints cpuHP;

    private void Start()
    {
        // 人間、またはCPUプレーヤーの体力がゼロになるイベントに登録
        playerHP.ZeroHP += OnZeroHP;
        cpuHP.ZeroHP += OnZeroHP;
    }

    private void OnDestroy()
    {
        // ゲーム監督クラス廃止のとき、イベントに登録解除する
        playerHP.ZeroHP -= OnZeroHP;
        cpuHP.ZeroHP -= OnZeroHP;
    }
    
    // イベントを処理
    // sender: ゼロになった体力コンポーネント
    private void OnZeroHP(HitPoints sender)
    {
        // プレーヤーの負け
        if (sender == playerHP)
        {
            Debug.Log("YOU LOSE!");
        }

        // CPUの負け
        if (sender == cpuHP)
        {
            Debug.Log("YOU WIN!");
        }
    }
    
    // Updateは要らなくなった
    private void Update()
    {
    }
}
```

もちろん、別々の処理メソッドを作れるので、以下の書き方も可能：

```c#
using UnityEngine;

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // 人間プレーヤーの体力
    [SerializeField] private HitPoints playerHP;

    // CPUプレーヤーの体力
    [SerializeField] private HitPoints cpuHP;

    private void Start()
    {
        // 人間、またはCPUプレーヤーの体力がゼロになるイベントを登録
        playerHP.ZeroHP += OnPlayerZeroHP;
        cpuHP.ZeroHP += OnCPUZeroHP;
    }

    private void OnDestroy()
    {
        // ゲーム監督クラス廃止のとき、イベントの登録解除を行う
        playerHP.ZeroHP -= OnPlayerZeroHP;
        cpuHP.ZeroHP -= OnCPUZeroHP;
    }

    // イベントを処理（プレーヤーの負け）
    // sender: ゼロになった体力コンポーネント
    private void OnPlayerZeroHP(HitPoints sender)
    {
        Debug.Log("YOU LOSE!");
    }

    // イベントを処理（CPUの負け）
    // sender: ゼロになった体力コンポーネント
    private void OnCPUZeroHP(HitPoints sender)
    {
        Debug.Log("YOU WIN!");
    }

    // Updateは要らなくなった
    private void Update()
    {
    }
}

```

## 練習

まず、[このプロジェクト](https://class.illogic.games/attachments/15)をダウンロードし、Unityに追加してください。このミニゲームでは、車が走行し、「ゴール」を通過すれば、`Debug.Log` でメッセージが表示される。

ここで、`event` を使用し、画面上でレースの結果を表示しましょう。

1. 発生する側：車の通過を検知する「CarDetector」にイベントを宣言し、車を通過したらイベントを発生
2. 聞き取る側：UIManager で CarDetector のイベントに登録し、車を通過したら、表示する

<details id="bkmrk-cardetector-%E3%81%AE%E7%AD%94%E3%81%88-%2F%2F-%E8%BB%8A"><summary>CarDetector の答え</summary>

```c#
// 車を検知する
public class CarDetector : MonoBehaviour
{
    // 車を検知したイベントを宣言
    // 引数：通過した車
    public event System.Action<RaceCar> CarDetected; 
    
    private void OnTriggerEnter(Collider other)
    {
        // RaceCarのコンポーネントがついている？
        RaceCar raceCar = other.GetComponent<RaceCar>();
        if (raceCar　== null)
            return;

        // 検知しました！
        CarDetected?.Invoke(raceCar);
    }
}
```

</details><details id="bkmrk-uimanager-%E3%81%AE%E7%AD%94%E3%81%88-%2F%2F-ui%E3%82%92"><summary>UIManager の答え</summary>

```c#
// UIを管理する
public class UIManager : MonoBehaviour
{
    // 車の順位を表示
    [SerializeField] private TextMeshProUGUI[] carPosition;

    // 車の通過を検知するため
    [SerializeField] private CarDetector detector;

    // 車の順位
    private int position = 0;
    
    private void Start()
    {
        // すべて非表示する
        foreach (var item in carPosition)
            item.gameObject.SetActive(false);
        
        // イベントに登録
        detector.CarDetected += OnCarDetected;
    }

    private void OnDestroy()
    {
        // イベントに解除
        detector.CarDetected -= OnCarDetected;
    }

    // ゲームを再開
    public void RestartGame() => 
        SceneManager.LoadScene("Racer");

    // 車が通過したイベントを受け取る
    private void OnCarDetected(RaceCar raceCar)
    {
        ShowCar(raceCar, position);
        position++;
    }

    // 車を定めた位置に表示
    private void ShowCar(RaceCar raceCar, int index)
    {
        var item = carPosition[index];
        item.gameObject.SetActive(true);
        item.color = raceCar.Color;
        item.text = raceCar.name;
    }
}

```

</details>もちろん、聞き取る側がクラス１つだけではなく、どのクラスでもイベントを受け取ることができる。紙吹雪のエフェクトを実装されているので、Confetti クラスでも CarDetector のイベントに登録し、車を通過したら、車の色の紙吹雪を飛ばすようにしましょう。

<details id="bkmrk-confetti-%E3%81%AE%E7%AD%94%E3%81%88-%2F%2F-%E7%B4%99%E5%90%B9%E9%9B%AA%E3%81%AE"><summary>Confetti の答え</summary>

```c#
// 紙吹雪のエフェクトを再生
public class Confetti : MonoBehaviour
{
    // パーティクルエフェクト
    [SerializeField] private ParticleSystem[] effects;

    // 車の検知
    [SerializeField] private CarDetector detector;

    private void Start()
    {
        // イベントに登録
        detector.CarDetected += OnCarDetected;
    }

    private void OnDestroy()
    {
        // 登録解除
        detector.CarDetected -= OnCarDetected;
    }

    // イベントを受け取る
    private void OnCarDetected(RaceCar raceCar)
    {
        Play(raceCar.Color);
    }

    // エフェクト再生
    private void Play(Color color)
    {
        foreach (var effect in effects)
        {
            var main = effect.main;
            main.startColor = color;
            effect.Play();
        }
    }
}
```

</details>