はじめに
こんにちは。Streamerioでゲーム側を担当していた guu です。
アドベントカレンダーも12日目ということで、そろそろ折り返しですね。
4回にわたって書いてきたDIの話も、今日でいったん締めになります。
今回は、敵の処理をDI化したときの設計と、その中でやらかした失敗談を紹介します。
DIやVContainerの概要は、過去の記事をご覧ください。
方針
もともと、僕が触る前から敵の処理自体は完成していました。
しかし、
- 敵を大量生成したときにメモリを使いすぎる
- DIの設計になっておらず、差し替えがしづらい
といった問題がありました。
そこで今回は、
- 敵の生成をオブジェクトプールで行う
-
MonoBehaviourを減らして軽量化する - クラスの差し替えだけで複数種類の敵を実装できるようにする
という方針を立てて作り直していきました。
オブジェクトプール
オブジェクトプールとは、オブジェクトを使うたびに毎回生成・破棄するのではなく、一度生成したものを保持しておき、必要に応じて再利用する設計パターンです。
今回は、敵オブジェクトの生成処理をUnityのObjectPoolクラスを使って実装しました。
構成は以下のようになっています。
EnemyPoolは、1種類の敵オブジェクトのプールを管理するクラスです。
中で、ObjectPool<IEnemy>を持っていて、未使用の敵がいればそれを返し、すべて使用中なら新しく生成して返します。
EnemySpawnerは「どの種類の敵を生成するか」を受け取り、内部のDictionaryから対応するEnemyPoolを探して、敵の取得を委譲します。
まだ対応するEnemyPoolがなければ、その場で新しくEnemyPoolを作成し、Dictionaryに登録します。
敵のプレファブ自体の情報は、IEnemyObjectRepository が持っていて、IEnemySpawner がそこからプレファブを取り出し、EnemyPool作成時に渡すようにしています。
この構成により、必要になったタイミングでだけ新規生成を行い、それ以外は既存の敵を再利用する形になりました。
使う側は、IEnemySpawner.Spawn(MasterEnemyType type)を呼ぶだけなので、「どの敵を出したいか」だけを気にすればよくなり、呼び出し側のコードもかなりすっきりしたと思います。
敵オブジェクト
-
MonoBehaviourを減らして軽量化する - クラスの差し替えだけで複数種類の敵を実装できるようにする
という方針の下、DIで以下の設計をしました。
敵オブジェクトは、大きく次の3つの要素で構成されています。
- 敵の動きを担当する
IEnemyMovement - 敵の体力管理を担当する
IEnemyHP - それらをまとめて処理する
EnemyPresenter
EnemyObjectLifetimeScopeで、IEnemyMovementとIEnemyHPをEnemyPresenterに渡し、敵としてのふるまいを組み立てる設計にしています。
敵同士の違いは「どう動くか」だけに絞っているので、IEnemyMovementの実装クラスを差し替えるだけで、簡単に敵の種類を増やせます。
本来 IEnemyMovement は、ロジックだけを持つ純粋なクラスにすることもできます。
今回は「敵の動きを変えたいときに、コンポーネントを差し替えるだけで済ませたい」という理由から、
IEnemyMovement の実装クラスはあえて MonoBehaviour を継承したコンポーネントにしています。
EnemyObjectLifetimeScopeでは、IEnemyMovementを登録する際に
GetComponent<IEnemyMovement>() でコンポーネントを取得して登録するようにしており、
スクリプト側の変更なしで、Inspectorから動きの差し替えができるようにしました。
共通の移動処理は EnemyMovementBase にまとめています。
派生クラスでは GetMovePosition() で「毎フレームどれだけ動くか」だけを返せばよく、あとは基底クラス側で実際の移動処理を行うようにしました。
そのおかげで、新しい敵の動きを作りたいときは、EnemyMovementBaseを継承して、GetMovePosition()を実装するだけで「勝手に動いてくれる」形になっています。
やらかしたこと
スクリプト見た感じ、敵の振る舞いの違いは動きだけだろう。仕様確認してないけどいいか 👈ヨシッ
いい設計思いついたし、とりあえず実装するか 👈ヨシッ
敵のスクリプト色々変えて、いらないファイルも出たけど、他のチームメイトのファイル消して問題起こったら嫌だし放置でいいか 👈ヨシッ
という短絡的な考えで、敵スクリプトが属人化し、無事僕しか触れなくなりました。
さらに、敵によっては攻撃方法が違うということを忘れており、IEnemyMovement が攻撃も持つというやらかしも追加されました。
チームメンバーのプロダクトを勝手に書き換えて、自分勝手に振る舞うのはやめよう。
ちゃんと設計や変更内容を共有して、行動前にチームメンバーに確認を取ろう。
という、チーム開発の大前提みたいな学びを改めて得ることになりました。
まとめ
今回は、敵の実装を DI 化したものの、結果的に「チームのコードを僕しか触れない状態」にしてしまった話をしました。
次回は、敵やプレイヤーのステータスなどをスプレッドシートで管理した話をします。