9. イベントドリブンECS
はじめに
EnemySpawnSystemで敵を生成しますが、そのときにCollisionSystemで衝突設定をする必要があります。
現在は、EnemySpawnSystemでcreateEnemy関数を呼び、createEnemyからCollisionSystemのregisterEnemyを呼び出していますが、 System間の依存が増えてしまうので、あまりきれいな処理ではありません。
そこでイベントを使って、敵を生成したときにイベントを発生(Emit)し、CollisionSystemでそのイベントを購読(Subscribe)して衝突設定を行うという方法に変えることにしました。
イベントに向いているもの
- 敵の生成
- 弾の生成
- プレイヤー被弾
- 敵の破壊
- レベルアップ
- アイテム取得
- ゲームオーバー
といった「たまに発生する瞬間」をイベントで配信するのが良い。
毎フレーム発生するようなイベントはパフォーマンス低下につながるので避ける。
実装方法
Phaser標準のEventEmitterを使います。
https://docs.phaser.io/phaser/concepts/events
例として、HealthSystemで敵の破壊イベント(EnemyDestroyed)を発生し、ScoreSystemで受け取る場合の処理です。
// HealthSystem.ts
if (health.hp <= 0) {
this.scene.events.emit("EnemyDestroyed", { entityId });
}
// ScoreSystem.ts
this.scene.events.on("EnemyDestroyed", (data) => {
const enemyId = data.entityId;
// enemyIdの敵が倒されたので、それに応じたスコアを計算
});このとき、Payload(data引数)はできるだけシンプルにします。 EntityId(number)のみにしておいて、必要なデータはComponentから取得し、IDなど小さなデータのみをイベントで配信するようにします。
購読解除
Scene終了時に removeAllListenersしてメモリリークを防止します。
// GameScene.ts
this.events.once("shutdown", () => this.events.removeAllListeners());グローバルイベント
イベントはシーンごとに独立しているため、UISceneからはGameSceneのイベントを購読できません。 シーンをまたいで使うには、グローバルなイベントを使う必要があります。
// GameScene.ts
this.game.events.emit("HPChanged", {hp});
// UIScene.ts
this.game.events.on("HPChanged", onHPChanged);メリット
イベントにより、どこで関数の呼び出しを行うか、という悩みが減り、拡張が楽になります。
例えば、敵が破壊されたときに効果音を出すなら、SoundSystemでEnemyDestroyedイベントを購読して、効果音を鳴らせばいいだけです。
まとめ
イベントを使って設計を見直しました。