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

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

このステップの目的

DamageReceiverとEnemyから死亡の事実をイベントで通知できるようにします。

Enemyからの死亡通知

今回のゲームクリア条件は敵の一定数撃破としています。ですがGameManagerが全Enemyを毎フレーム検索して生存状況を確認するのは非効率です。GameManagerがEnemyの内部事情を知らなければならず、設計上も好ましくありません。

今回はEnemyが死亡した瞬間にイベントを発行してGameManagerに通知することで、撃破数を集計します。

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

デリゲートとは

通常のメソッド呼び出しでは、呼び出す側が相手のクラスとメソッドを直接指定します。

cs
gameManager.OnEnemyDefeated(this);

この形では、EnemyがGameManagerの存在とメソッドを知る必要があります。Enemyを別のSceneで利用するときにもGameManagerへの依存が残ってしまいます。

デリゲートは、呼び出すメソッドを値として保持できるC#の型です。呼び出し元は登録された具体的なメソッド名を知らなくても処理を実行できます。

通常の呼び出し
Enemy
メソッドを直接指定する
GameManager
デリゲートを使う通知
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()で発行するできるできない
=で購読者を置き換えるできるできない
cs
public static event Action<Enemy> Defeated;

この宣言は、次の要素で構成されています。

部分意味
publicほかのクラスから購読と解除ができる
staticScene上のEnemy全体で通知口を共有する
event外部からの発行と代入を禁止する
Action<Enemy>通知先がEnemyを1つ受け取る
Defeatedイベントの名前

イベントを使う処理は、購読、発行、解除の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に次のコードを追加します。

Enemy.cscs
using System;

public static event Action<Enemy> Defeated;

Action<Enemy>は「Enemyを1つ引数として受け取り、戻り値を返さないデリゲート」です。eventもセットで定義することで他のクラスは勝手にイベントを発行できなくなります。これでこのデリゲートはEnemyが倒された事実を通知するだけの役割に限定されます。

DamageReceiverへ、生存状態から死亡状態へ変化した瞬間を通知するDiedイベントを追加します。

DamageReceiver.cscs
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は同じオブジェクトについて一度しか通知されません。

EnemyDamageReceiver.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;
}

死亡時の処理をOnDied()へ分離します。

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

EnemyはHPを調べず、DamageReceiverから届いた死亡の事実に反応します。?.Invoke(this)は購読者がいる場合だけ、自分自身を引数として通知します。

staticなデリゲートなのでScene上に何体Enemyが存在しても通知口は共通です。GameManagerは各Enemyへの参照を持たずに通知を受け取れます。一方でstaticイベントはSceneをまたいで残る可能性があるため、購読と解除を必ず対にします。

チェックリスト

EnemyがGameManagerを直接参照せず、死亡した瞬間を一度だけ通知できる。