Skip to content
第08回 GameManagerとゲーム状態

2 / 5 Enemyの撃破を通知する ​

このステップの目的

DamageReceiverとEnemyから、死亡したことをイベントで通知します。

第04回では、特定のDamageReceiverを購読する例を学びました。処理の登録、通知、登録の解除という基本は同じです。

今回はEnemyの死亡通知をGameManagerでまとめて受け取り、撃破数を数えます。そのために、インスタンスごとのイベントと、全Enemyで共有するstaticイベントの違いを確認します。

Enemyからの死亡通知 ​

今回のゲームは、一定数の敵を倒すとクリアになります。GameManagerが毎フレームすべてのEnemyを探し、生きているか調べる方法は非効率です。GameManagerがEnemyの内部の状態を知る必要もあり、設計上も好ましくありません。

そこで、Enemyが死亡した瞬間にイベントを発行します。GameManagerはその通知を受け取り、撃破数を集計します。

EnemyからGameManagerへの撃破通知
Enemy.cs
staticイベントを発行する
Enemy.Defeated
Enemy.Defeated
イベントを購読する
GameManager.cs
GameManager.cs
通知を受け取る
OnEnemyDefeated(Enemy enemy)

通知先の登録と解除 ​

第04回と同じく、通知を受ける側で+=を使って処理を登録し、-=で解除します。今回は全Enemyで一つのイベントを共有します。GameManagerがそのイベントを購読すれば、各Enemyの通知をまとめて受け取れます。

補足:Actionとeventの仕組みを詳しく確認する

Actionは戻り値を持たないデリゲート型 ​

C#には、よく使うデリゲート型としてActionが用意されています。

型登録できるメソッド
Action引数がなく、戻り値がvoidのメソッド
Action<int>intを1つ受け取り、戻り値がvoidのメソッド
Action<Enemy>Enemyを1つ受け取り、戻り値がvoidのメソッド

今回使うAction<Enemy>には、次の形のメソッドを登録できます。

cs
private void OnEnemyDefeated(Enemy enemy)
{
    // 倒されたEnemyを引数として受け取る
}

メソッド名は自由ですが、引数と戻り値の型は一致している必要があります。引数がないメソッドやintを受け取るメソッドを登録すると、コンパイルエラーになります。

eventで通知の発行権限を制限する ​

デリゲートにeventを付けることで、クラスの外からできる操作を購読と解除に制限できます。

publicのデリゲートには、クラスの外から値を代入したり、Invoke()で呼び出したりできます。外から呼べることが利点になる場合もあります。

Enemyの死亡通知は、Enemyだけが発行する必要があります。eventを付けると、外部からの発行を禁止できます。デリゲートを使うときは、可能な限りeventも付けてください。

操作Enemyクラスの内側Enemyクラスの外側
+=で購読するできるできる
-=で解除するできるできる
Invoke()で発行するできるできない
=で購読者を置き換えるできるできない

Defeatedという名前のイベントを次のように宣言します。publicにより、ほかのクラスから購読と解除ができます。

cs
public static event Action<Enemy> Defeated;
  • static:通知口を共有
  • event:外部の操作を制限
  • Action<Enemy>:Enemyを受け取る

イベントを使う処理は、購読、発行、解除の3段階になります。

cs
// GameManager: 通知されたときに呼ぶメソッドを登録する
Enemy.Defeated += OnEnemyDefeated;

// Enemy: 死亡した自分自身を引数として通知する
Defeated?.Invoke(this);

// GameManager: 不要になった登録を外す
Enemy.Defeated -= OnEnemyDefeated;

?.Invoke(this)の?.は、null条件演算子です。購読者がいる場合だけデリゲートを呼び出し、誰も購読していなければ何もしません。次のコードと同じ意味になります。

cs
if (Defeated != null)
{
    Defeated.Invoke(this);
}

撃破数を増やし、ゲームクリアを判定するのは、通知を受け取るGameManagerです。Enemyは自分が倒されたことだけを通知し、撃破数や勝敗を判断しません。

:::

第03回から使っているEnemy.csの先頭へ、using System;を追加してください。続いて、Enemyクラスの内側へイベントの宣言を追加します。

Enemy.cs(ファイル先頭)cs
using System;
using UnityEngine;
Enemy.cs(クラス内)cs
public class Enemy : MonoBehaviour
{
    public static event Action<Enemy> Defeated;

Action<Enemy>は、Enemyを一つ引数に取り、戻り値を返さないデリゲートです。eventを付けると、他のクラスからは発行できません。Enemy自身が倒されたことを通知するためのイベントとして使います。

DamageReceiverにDiedイベントを追加してください。生存状態から死亡状態へ変わった瞬間を通知するイベントです。

DamageReceiver.cs(クラス内)cs
public class DamageReceiver : MonoBehaviour
{
    public event Action Died;

ReceiveDamage()では、ダメージを反映する前の生死を記録します。HPが変わった後で死亡したかを調べるためです。

DamageReceiver.cscs
var wasAlive = !IsDead;

health.TakeDamage(damage);

if (CurrentHealth == previousHealth)
    return;

Damaged?.Invoke(
    previousHealth - CurrentHealth,
    MaxHealth);

if (wasAlive && IsDead)
    Died?.Invoke();

Diedを発行するのは、生存から死亡へ変わった場合だけです。HPが0のまま追加のダメージ通知を受けても、死亡を繰り返し通知しません。

EnemyはDamageReceiver.Diedを購読し、死亡通知を受けたときに撃破イベントを発行します。

Enemy.cscs
private void OnEnable()
{
    _damageReceiver.Damaged += OnDamaged;
    _damageReceiver.Healed += OnHealed;
    _damageReceiver.Died += OnDied;
}

private void OnDisable()
{
    _damageReceiver.Damaged -= OnDamaged;
    _damageReceiver.Healed -= OnHealed;
    _damageReceiver.Died -= OnDied;
}

Enemy.csのクラス内に、次のOnDied()を定義してください。これまでOnDamaged()に書いていたHPによる死亡判定は削除します。Destroy(gameObject, destroyDelay)もOnDamaged()から削除し、破棄する処理をOnDied()へ移してください。

HP表示など、生存中にも必要な更新はOnDamaged()に残します。

Enemy.cscs
private void OnDied()
{
    Defeated?.Invoke(this);
    Destroy(gameObject, destroyDelay);
}

EnemyではHPを調べず、DamageReceiverから届いた死亡通知を受けてOnDied()を実行します。Defeated?.Invoke(this)は、購読者がいる場合だけ撃破を通知する呼び出しです。引数のthisで、倒されたEnemy自身を渡します。

Defeatedはstaticイベントなので、Sceneに何体Enemyがいても全Enemyで共有します。GameManagerは各Enemyへの参照を持たずに、撃破通知を受け取れます。ただし、staticイベントはSceneを切り替えても残る可能性があります。購読した処理は必ず解除してください。

ここまでの編集で、Unityにコンパイルエラーがないことを確認します。GameManagerの購読と終了時の停止処理は、続く二つのステップで実装します。動作確認は、その両方を用意してから行ってください。

チェックリスト
  • EnemyがGameManagerを直接参照せず、死亡した瞬間を一度だけ通知できる。