i-school - 汎用的なオブジェクトプールの実装例
 オブジェクトプールのベースとなる機能を作成しておき、それを継承することで様々な用途に応用できる実装例です。

 敵の生成、アイテムの生成、ボタンの生成、フロート表示の生成など、オブジェクトプールの活用できる範囲は広いため、
それらに対して汎用的に実装を行うための設計になっています。



1.説明


 記事では、以下の内容に焦点を当てて解説します。

弾生成クラスの基本設計


 弾の生成と弾に対しての事前の設定など、弾の基本的な生成機能をどのように設計するかについて説明します。
オブジェクト指向の原則を適用して、共通の振る舞いを抽象クラスやインターフェースとして実装し、再利用性を高めるアプローチを探ります。


2.IGeneratable インターフェースの作成


 IGeneratable インターフェースは、シューティングゲームやアクションゲームなどで頻繁に使用される弾の生成を抽象化するために設計されています。
このインターフェースは、弾を生成するための基本的な操作を定義しており、具体的な弾の種類や挙動に依存せず共通のインターフェースを提供します。



IGeneratable.cs

<= クリックしたら開きます。


 定義しているメソッドは GenerateBullet メソッドのみです。
同名のメソッドを2つ、引数の情報を異なる状態にして定義しています。

 インターフェースのメソッドは特性上、自動的に public 扱いになります。



 具体的には、IGeneratable インターフェースは以下の役割を果たします:


1.弾の生成機能の定義

 
 IGeneratable インターフェースは、GenerateBulletメソッドを定義しています。
このメソッドは弾を生成するための基本的な手続きを表しており、生成に必要な発射方向をパラメータとして受け取ります。

 また引数のオーバーロードにより、同名のメソッドには追加の引数を受け取る方法も用意しています。


2.クラス間の共通性を確保


 IGeneratableインターフェースを実装するさまざまなクラスは、異なる種類の弾を生成する機能を提供することができます。
これにより、ゲーム内の異なる要素(プレイヤーキャラクター、敵キャラクター、砲台など)が共通の弾生成機能を実行できるようになります。


3.クラス間の疎結合性の促進


 IGeneratableインターフェースを介して弾生成の機能を定義することで、クラス間の疎結合性が向上します。
クラスがインターフェースを実装するだけで、他のクラスとの連携が容易になります。


4.拡張性と保守性の確保


 ゲーム内に新しい種類の弾を追加する際に、IGeneratableインターフェースを実装するだけで新しい弾の生成機能を追加できます。
これにより、新しい要素を簡単に統合し、保守性の高いコードを実現できます。



 総合すると、IGeneratableインターフェースは、ゲーム内で弾を生成する機能を共通化し、柔軟性と保守性を向上させるための基盤を提供する役割を果たしています。


3.GeneratorBase クラスの作成


 抽象クラスとして GeneratorBase クラスを作成し、IGeneratable インターフェースを実装します。

 実装にあたり、GenerateBullet メソッドを抽象メソッドとして定義します。
抽象メソッドも、あくまでもメソッドを定義するのみで、振る舞いについてはサブクラスに委ねています。

 通常のメソッドでインターフェースのメソッドを実装してしまうと、このクラス内での定義となり、サブクラスでの上書きが任意になってしまいますが、
抽象メソッドとして定義しておくことで、このクラスでは内部の実装をせず、サブクラスでの実装を強制することができます。



GeneratorBase.cs

<= クリックしたら開きます。


 インターフェースのメソッドを定義する順番に指定はありません。
例えば、GenerateBullet メソッドの定義をしてから SetUp メソッドの定義を行うこともできます。



 GeneratorBase クラスは、アクションゲームにおける弾の生成機能の基盤となる親クラスです。

 以下はその設計と実装についての説明です。


1.抽象クラスとしての GeneratorBase


 GeneratorBase クラスは抽象クラスとして実装されます。
抽象クラスは、一部のメソッドが未実装(抽象メソッド)であり、具体的な振る舞いはサブクラス(子クラス)で提供されることを意味します。

 弾を生成する、という実行処理自体は共通していますが、内部的な弾の生成処理については、個々のサブクラスに任せる形です。

 この設計により、共通の振る舞いを抽象クラスで定義し、個別の振る舞いをサブクラスでカスタマイズすることができます。


2.IGeneratable インターフェースの実装


 IGeneratableインターフェースは弾を生成する機能を規定するものです。
GeneratorBase クラスはこのインターフェースを実装します。
これにより、弾を生成する機能を共通のインターフェースで提供し、異なる種類の弾に対しても統一的な操作が可能となります。


3.GenerateBullet メソッドの抽象定義


 GeneratorBase クラスには、IGeneratableインターフェースの一部としてGenerateBulletメソッドがあります。
このメソッドは抽象メソッドとして定義されます。
抽象メソッドはメソッドの実装を持たず、サブクラスにおいて必ずオーバーライドされることを強制します。

 これにより、サブクラスでの弾の生成機能のカスタマイズを保証し、実装忘れを防ぐことが出来ます。


4.MonoBehaviour クラスの継承


 インターフェース自体は実際にはシリアライズできませんが、インターフェースを実装するクラスはシリアライズ可能です。
したがって、シリアライズが必要な場合、インターフェースを実装したクラスをシリアライズすることで表現できます。

 Unity の場合、MonoBehaviour クラスを継承しているクラスや、System.Serializable 属性を付与したクラスはシリアライズが可能となり、
それらのクラスはインスペクターに表示する恩恵を受けることが出来ます。

 インターフェース単独の場合には、このシリアライズに対応ができないため、例えば、インスペクターでの確認や、アサインなどが行えません。

 今回、GeneratorBase クラスにインターフェースを実装することにより、このクラスを親クラスとした子クラスの情報はインスペクターに表示されるようになります。

 子クラスに直接インターフェースを実装せずに、親クラスにインターフェースを実装していることで、このインターフェースのシリアライズに関する問題も回避しています。
 


 GeneratorBase を抽象クラスとして設計し、IGeneratableインターフェースを実装することができます。
これにより、GeneratorBase のサブクラスはIGeneratableインターフェースの実装を強制されるだけでなく、シリアライズに関する問題も回避できます。
各サブクラスはIGeneratableを実装する必要があるため、再利用性や保守性が高まります。

 この設計により、GeneratorBase クラスを継承する具体的な弾のクラスは、必ずGenerateBulletメソッドを実装する必要があります。
これによって、弾の生成の流れを統一的に管理しつつ、個別の弾生成クラスごとに異なる生成を提供することができます。


4.サンプルクラスによるシリアライズ情報の比較例


 具体的なサブクラスの作成に入る前に、サンプルクラスを作成して、インスペクターでのシリアライズについて確認しておきましょう。

 クラス内に IGeneratable インターフェースの List と GeneratorBase クラスの List をそれぞれ作成し、SerializeField属性を付与します。
このサンプルクラスを任意のゲームオブジェクトにアタッチしてみてください。それぞれの挙動が異なることが分かります。



TestInspector.cs

 <= クリックすると開きます



1.IGeneratable インターフェースの List


 インターフェースは SerializeField属性を付与しても、public 修飾子で宣言していてもインスペクターには表示されません。






 デバッグモードにしても表示されません。



 


2.GeneratorBase クラスの List


 こちらはクラスであるため、インスペクターに表示されます。





 今回のように MonoBehaviour クラスを継承していない場合には自動的にシリアライズされますが、
MonoBehaviour クラスを継承していない場合には自動的にはシリアライズされないため、インターフェースと同じようにインスペクターに表示されません。
その場合、クラスに System.Serializable 属性を付与する必要があります。


 以上で完成です。

 複数回の手順に分けて学習を行います。

 Unity の用意しているスクリプタブル・オブジェクトとオブジェクトプールの機能を利用し、
複数種類のエフェクトをスクリプタブル・オブジェクトとオブジェクトプールを活用して効率的に管理する手法を実装していきます。



 新しい学習内容は、以下の通りです。

 ・抽象化(インターフェース、抽象クラス、クラスの継承)

 なお、処理内では非同期処理として UniTask を採用しています。


1.エフェクト用のアセットをダウンロードしてUnityにインポートする

1.設計


 まずはゲーム内のどの場面でエフェクトを再生するかを想定し、どの位の種類のエフェクトが必要になることを把握します
例えば、以下のような種類です。左側が想定するファイル名、右側がエフェクトを再生する場面です。

<エフェクト・リスト>
 1.Hit       --  攻撃命中時
 2.Damage   --  敵からの攻撃の被弾時
 3.ItemGet  --  アイテム取得時
 4.LevelUp  -- レベルアップ時

 このようにリスト化して書き出しておくと管理しやすくなります。

 今後、エフェクトを利用する場面が増えたり、変更したい、といった場合には、上記に追加をして検討してください。


2.アセットストアより、エフェクトを再生する場面をイメージして、任意のアセットを探して Unity にインポートする


 エフェクトにはパーティクルシステム、あるいは Animator を利用して作成されたアセットを利用します。
アセットストアより任意のアセット Unity インポートして、ゲーム内でエフェクトを再生するための準備をします。

 頭の中で場面を思い浮かべながら、どんなエフェクトがよいか、確認しながら決めていきましょう。
先ほど提示した場面に合わせて、合計で3種類のエフェクト用のゲームオブジェクトを用意をしてください。
パーティクルでも Animator でもどちらでも問題ありません。

 また分割可能な画像を利用すればパーティクルの Texture Sheet Animation や、Animator 用の AnimationClip を作成可能です。

Unity公式マニュアル
パーティクルシステム
https://docs.unity3d.com/ja/current/Manual/class-P...
Unity公式マニュアル
Texture Sheet Animation モジュール
https://docs.unity3d.com/ja/current/Manual/PartSys...



 出来れば複数のアセットをインポートして、Unity 内で再生してみるとよいでしょう。


2.エフェクト用のクラスの作成を行う

1.設計


 エフェクトのプレハブの管理には、スクリプタブル・オブジェクトの機能と、オブジェクトプールの機能を利用します。

 複数の種類のエフェクトのプレハブをスクリプタブル・オブジェクトを利用することでデータベースとして管理し、必要な情報にアクセスできるようにします。

 生成されたエフェクトのプレハブはオブジェクトプールの機能を持たせ、繰り返し再利用できるようにします。

 そのために、まずはオブジェクトプールの機能を実装するための、エフェクトのプレハブ用のクラスを作成します。

 1.IPoolable インターフェース
 2.PoolBase 抽象クラス
 3.EffectPlayerBase クラス
 4.EffectPlayerBase クラスを継承したサブクラス

 1はインターフェース、2は抽象クラスですので、これらはエフェクトのプレハブにはアタッチできません。
そのため、3を作成し、これをエフェクトのプレハブにアタッチします。
 
 また各エフェクトのプレハブごとに処理のふるまいを変えたい場合には、4をそれぞれのエフェクトのプレハブごとに作成し、
3の代わりに4のみをアタッチして利用します。


2.IPoolable インターフェースの作成


 インターフェースです。実装したクラスにオブジェクトプール用の共通メソッドを提供します。


IPoolable.cs

 <= クリックすると開きます




3.PoolBase クラスの作成


 様々なオブジェクトプールに対応できる、汎用型の抽象クラスです。
抽象クラスですので、必ずほかのクラスに継承して利用します。


PoolBase.cs

 <= クリックすると開きます



4.EffectPlayerBase クラスの作成


 抽象クラスである PoolBase を継承して、エフェクト用のベース(親)クラスを作成します。
PlayEffect メソッドを仮想メソッドとして用意し、具体的な処理のふるまいは各エフェクトに依存できるようにしています。
また IPoolable インターフェースによる Release メソッドも同様に仮想メソッドとして実装して、
オブジェクトプールへ戻る処理を共通化したうえで、ふるまいを変えられるように用意しています。

 メンバ変数には Index か enum でエフェクトのプレハブを特定するための情報を設定します。
これはどちらを利用しても問題ありませんので、自分の実装に合わせて作成してください。

 enum を利用する場合には、次の手順で実装しますので、実装の後にメンバ変数を追加してください。


EffectPlayerBase.cs

 <= クリックすると開きます



5.EffectPlayerBase クラスを継承したサブクラス


 サンプルとして、サブクラスを1つ提示しておきます。
PlayEffect メソッドのみ上書きしてふるまいを変え、Release メソッドは親クラスのメソッドをそのまま利用しています。

 この時点でこのサブクラスは、MonoBehaviour、PoolBase、そして EffectPlayerBase の機能を有していることになります。


LevelUpEffect.cs

 <= クリックすると開きます



3.エフェクト用のプレハブに EffectPlayerBase クラスをアタッチする


 いったん、すべてのエフェクト用のプレハブに EffectPlayerBase クラスをアタッチします。
Index については、一意の値を設定し、ほかのエフェクトのプレハブとは同じ数値にならないようにしておきます。






 各エフェクトごとにふるまいを変えたい場合には、EffectPlayerBase クラスをリムーブし、
EffectPlayerBase クラスを親クラスにした子クラスを作成して、それを再度プレハブにアタッチし直します。






 以上でこの手順は終了です。