2 / 5 Enemyの撃破を通知する
DamageReceiverとEnemyから、死亡したことをイベントで通知します。
第04回では、特定のDamageReceiverを購読する例を学びました。処理の登録、通知、登録の解除という基本は同じです。
今回はEnemyの死亡通知をGameManagerでまとめて受け取り、撃破数を数えます。そのために、インスタンスごとのイベントと、全Enemyで共有するstaticイベントの違いを確認します。
Enemyからの死亡通知
今回のゲームは、一定数の敵を倒すとクリアになります。GameManagerが毎フレームすべてのEnemyを探し、生きているか調べる方法は非効率です。GameManagerがEnemyの内部の状態を知る必要もあり、設計上も好ましくありません。
そこで、Enemyが死亡した瞬間にイベントを発行します。GameManagerはその通知を受け取り、撃破数を集計します。
通知先の登録と解除
第04回と同じく、通知を受ける側で+=を使って処理を登録し、-=で解除します。今回は全Enemyで一つのイベントを共有します。GameManagerがそのイベントを購読すれば、各Enemyの通知をまとめて受け取れます。
補足:Actionとeventの仕組みを詳しく確認する
Actionは戻り値を持たないデリゲート型
C#には、よく使うデリゲート型としてActionが用意されています。
| 型 | 登録できるメソッド |
|---|---|
Action | 引数がなく、戻り値がvoidのメソッド |
Action<int> | intを1つ受け取り、戻り値がvoidのメソッド |
Action<Enemy> | Enemyを1つ受け取り、戻り値がvoidのメソッド |
今回使うAction<Enemy>には、次の形のメソッドを登録できます。
private void OnEnemyDefeated(Enemy enemy)
{
// 倒されたEnemyを引数として受け取る
}メソッド名は自由ですが、引数と戻り値の型は一致している必要があります。引数がないメソッドやintを受け取るメソッドを登録すると、コンパイルエラーになります。
eventで通知の発行権限を制限する
デリゲートにeventを付けることで、クラスの外からできる操作を購読と解除に制限できます。
publicのデリゲートには、クラスの外から値を代入したり、Invoke()で呼び出したりできます。外から呼べることが利点になる場合もあります。
Enemyの死亡通知は、Enemyだけが発行する必要があります。eventを付けると、外部からの発行を禁止できます。デリゲートを使うときは、可能な限りeventも付けてください。
| 操作 | Enemyクラスの内側 | Enemyクラスの外側 |
|---|---|---|
+=で購読する | できる | できる |
-=で解除する | できる | できる |
Invoke()で発行する | できる | できない |
=で購読者を置き換える | できる | できない |
Defeatedという名前のイベントを次のように宣言します。publicにより、ほかのクラスから購読と解除ができます。
public static event Action<Enemy> Defeated;static:通知口を共有event:外部の操作を制限Action<Enemy>:Enemyを受け取る
イベントを使う処理は、購読、発行、解除の3段階になります。
// GameManager: 通知されたときに呼ぶメソッドを登録する
Enemy.Defeated += OnEnemyDefeated;
// Enemy: 死亡した自分自身を引数として通知する
Defeated?.Invoke(this);
// GameManager: 不要になった登録を外す
Enemy.Defeated -= OnEnemyDefeated;?.Invoke(this)の?.は、null条件演算子です。購読者がいる場合だけデリゲートを呼び出し、誰も購読していなければ何もしません。次のコードと同じ意味になります。
if (Defeated != null)
{
Defeated.Invoke(this);
}撃破数を増やし、ゲームクリアを判定するのは、通知を受け取るGameManagerです。Enemyは自分が倒されたことだけを通知し、撃破数や勝敗を判断しません。
:::
第03回から使っているEnemy.csの先頭へ、using System;を追加してください。続いて、Enemyクラスの内側へイベントの宣言を追加します。
using System;
using UnityEngine;public class Enemy : MonoBehaviour
{
public static event Action<Enemy> Defeated;Action<Enemy>は、Enemyを一つ引数に取り、戻り値を返さないデリゲートです。eventを付けると、他のクラスからは発行できません。Enemy自身が倒されたことを通知するためのイベントとして使います。
DamageReceiverにDiedイベントを追加してください。生存状態から死亡状態へ変わった瞬間を通知するイベントです。
public class DamageReceiver : MonoBehaviour
{
public event Action Died;ReceiveDamage()では、ダメージを反映する前の生死を記録します。HPが変わった後で死亡したかを調べるためです。
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を購読し、死亡通知を受けたときに撃破イベントを発行します。
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()に残します。
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を直接参照せず、死亡した瞬間を一度だけ通知できる。