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