1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは。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で、IEnemyMovementIEnemyHPEnemyPresenterに渡し、敵としてのふるまいを組み立てる設計にしています。

敵同士の違いは「どう動くか」だけに絞っているので、IEnemyMovementの実装クラスを差し替えるだけで、簡単に敵の種類を増やせます。

本来 IEnemyMovement は、ロジックだけを持つ純粋なクラスにすることもできます。
今回は「敵の動きを変えたいときに、コンポーネントを差し替えるだけで済ませたい」という理由から、
IEnemyMovement の実装クラスはあえて MonoBehaviour を継承したコンポーネントにしています。

EnemyObjectLifetimeScopeでは、IEnemyMovementを登録する際に
GetComponent<IEnemyMovement>() でコンポーネントを取得して登録するようにしており、
スクリプト側の変更なしで、Inspectorから動きの差し替えができるようにしました。

共通の移動処理は EnemyMovementBase にまとめています。
派生クラスでは GetMovePosition() で「毎フレームどれだけ動くか」だけを返せばよく、あとは基底クラス側で実際の移動処理を行うようにしました。

そのおかげで、新しい敵の動きを作りたいときは、EnemyMovementBaseを継承して、GetMovePosition()を実装するだけで「勝手に動いてくれる」形になっています。

やらかしたこと

スクリプト見た感じ、敵の振る舞いの違いは動きだけだろう。仕様確認してないけどいいか 👈ヨシッ
いい設計思いついたし、とりあえず実装するか 👈ヨシッ
敵のスクリプト色々変えて、いらないファイルも出たけど、他のチームメイトのファイル消して問題起こったら嫌だし放置でいいか 👈ヨシッ

という短絡的な考えで、敵スクリプトが属人化し、無事僕しか触れなくなりました。
さらに、敵によっては攻撃方法が違うということを忘れており、IEnemyMovement が攻撃も持つというやらかしも追加されました。

チームメンバーのプロダクトを勝手に書き換えて、自分勝手に振る舞うのはやめよう。
ちゃんと設計や変更内容を共有して、行動前にチームメンバーに確認を取ろう。
という、チーム開発の大前提みたいな学びを改めて得ることになりました。

まとめ

今回は、敵の実装を DI 化したものの、結果的に「チームのコードを僕しか触れない状態」にしてしまった話をしました。

次回は、敵やプレイヤーのステータスなどをスプレッドシートで管理した話をします。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?