Unityに関連する記事です

 2Dゲームにおいて、弾の生成機能に関しての発展例です。
実装する処理が多いため、2回の手順に分けます。



 オブジェクトプールの機能を使い、同じ弾をリサイクルして繰り返し利用する方法に置き換えます。

 オブジェクトプールは、再利用可能なオブジェクトのセットを管理する仕組みです。
アプリケーション内で頻繁に生成と破棄を行うオブジェクトをプール(非表示化)しておき、
生成と破棄の代わりに再利用(再表示)することでパフォーマンスを向上させることができます。



 この例では、すでに作成している機能の内、公転する弾の生成と削除の処理をリファクタリングしたものを紹介します。
同時にインターフェースやクラスの継承を利用して、処理の抽象化も行います。

 そのため、ゲーム画面上の表現は変わりませんが、内部的な処理はかなり変わります。


<実装動画ーボタンを押すとプレイヤーの進行方向に弾をまとめて投射するー>
動画ファイルへのリンク


 事前に前回の手順の学習と実装が必要になります。

  => 【2D】プレイヤーを軸にして公転する弾の機能
  => 【2D】プレイヤーを軸にして公転する弾の機能

  => 【2D】弾制御用クラスの設計デザインと抽象化
  => 【2D】弾生成用クラスの設計デザインと抽象化



1.設計


 リファクタリングの対象は旋回する弾を投射する際の処理になります。

 いままでは投射された旋回する弾は、投射後に破壊していましたが、この部分をオブジェクトプールで管理します。
この手法を用いた場合、旋回する弾は破壊されるのではなく、非表示の状態になり、プールされます。

 投射する処理に利用している ThrowingBullet は、旋回する弾を管理するための入れ物であるため、
こちらはその都度破壊して、生成するようにしています。


◇ 1.OrbitingBullet(旋回する弾) をオブジェクトプールから取り出して画面に表示する
    オブジェクトプール内にない場合のみ、新しく生成する

  2.ThrowingBullet を生成し、その子オブジェクトとしてr OrbitingBullet を入れて投射する
 
◇ 3.ThrowingBullet を破壊する前に、OrbitingBullet をオブジェクトプールにすべて戻す

  4.ThrowingBullet を破壊する

 このような処理の流れになります。
◇のついている部分が新しく採用するオブジェクトプールの機能です。


2.BulletBase クラスを修正して、オブジェクトプールに対応するための機能を追加する


 以前の手順で作成している BulletBase クラスを修正し、オブジェクトプールに対応するための機能を追加します。

 using UnityEngine.Pool の宣言が必要になります。



BulletBase.cs

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




 処理のポイントはプロパティの活用方法です。
オブジェクトプールを参照先としておくことにより、このクラスからいつでもオブジェクトプール
(今回は次に修正する GeneratorBase クラス)へのアクセスを行えるようにしています。

 そのため、新しく追加した ReleaseBullet メソッドを任意のタイミングで実行できる状態を作り、
弾側からオブジェクトプールに戻す処理を実装出来ます。


3.GeneratorBase クラスを修正してオブジェクトプールの機能を追加する


 以前の手順で作成している GeneratorBase クラスを修正し、オブジェクトプールの機能を追加します。

 using UnityEngine.Pool の宣言が必要になります。



GeneratorBase.cs

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



4.オブジェクトプール


 オブジェクトプールとは、デザインパターンの1つです。

 その概念をゲーム内でも利用できるように Unity の用意している機能の1つでもあります。
以前は自作して利用していましたが、Unity 2021 より、Unity の標準機能として実装されました。


参考サイト
Unity 公式マニュアル
オブジェクトプール
【Unity】オブジェクトプール(Object Pool)の使い方!Unity2021から標準で使えるぞ
Zenn twugo様
(非公式和訳)Unity - level up your code with game programming patterns Chapter 05 オブジェクトプール(Object Pool)
【Unity】Unity公式のObjectPoolを使ってみる(内部実装も一部紹介)


1.オブジェクトプールの初期化


 コンストラクタメソッドを定義します。
この部分における初期化とは、オブジェクトプールの機能を設定する、という意味合いになっています。
そのため、この部分でオブジェクトを生成している訳ではなく、どのような挙動をするのかを決める、という内容の初期化処理です。


参考サイト
Unity 公式ドキュメント
ObjectPool T0 Constructor



    // オブジェクトプールの初期設定。new だが、インスタンスを作る訳ではない
        bulletPool = new ObjectPool<BulletBase>(
            createFunc: () => Create(),
            actionOnGet: OnGetFromPool,  // メソッド作成して登録できる
            actionOnRelease: target => target.gameObject.SetActive(false),
            actionOnDestroy: target => Destroy(target.gameObject),
            collectionCheck: true,
            defaultCapacity: 10,
            maxSize: 1000);

 ObjectPool はジェネリック<T>ですので、利用するときに型を指定します。
ここでは BulletBase 型にしています。抽象化しておくことにより、特定の弾ではなく、弾であればどれでも、という利用方法が出来ます。

 引数は7つあり、それぞれに指定された値を設定していくことで、オブジェクトプールの挙動を作ります。

 この例では各引数について名前付き引数の機能を利用して書き込んでいますが、値だけを記述しても問題ありません。


参考サイト
MicroSoft
名前付き引数と省略可能な引数 (C# プログラミング ガイド)


2.第1〜4引数


 第1〜4引数までは、特定のイベント発生時の実行処理を設定します。OnTriggerEnter メソッドなどのコールバック処理のイメージです。
「こういう状態になったら(特定のイベント発生したら)」⇒「設定した(紐付けした)処理を実行する」というパターンです。
このうち、「設定した(紐付けした)処理を実行する」の部分はデリゲートになっており、ラムダ式による匿名メソッドが利用できます。



 第1引数は createFunc です。これは、IObjectPool.Get() メソッドにより実行されます。
この Get メソッドは内部で自動分岐し、オブジェクトプール内にオブジェクトがある場合には、次の第2引数に登録されているメソッドが実行されます。
そしてオブジェクトプール内にオブジェクトがない場合に、この第1引数の処理が実行されます。

 これは Func<T> 型です。ここにはオブジェクトプールに対して生成の命令がきた場合の挙動を設定します。
主にプールが空のときに新しいオブジェクトのインスタンスを生成する機能を設定します。
() => Create() を指定していますので、生成命令がきた場合には Create メソッドを実行します。

 第2引数以降も同様です。特定のイベントに紐づける形でデリゲートを設定していきます。



 第2引数は actionOnGet です。これは、IObjectPool.Get() メソッドにより実行されます。
 こちらは Action<T> 型です(戻り値がありません)。
ここにはオブジェクトプール内にプールされて非表示の状態になっているオブジェクトを取り出す際の処理を設定します。
OnGetFromPool メソッドを指定していますので、オブジェクトの取り出し命令が来たら、OnGetFromPool メソッドが実行されます。



 第3引数は actionOnRelease です。これは IObjectPool.Release() メソッドにより実行されます。
 こちらも Action<T> 型です。
このメソッドはオブジェクトプールと同じ型を第1引数に取ります。今回であれば BulletBase 型の情報を指定します。

 ここにはオブジェクトをオブジェクトプール内に戻す命令が来たときの処理を設定します。
target => target.gameObject.SetActive(false) と設定されていますので、引数で受けた target (今回は BulletBase 型)のオブジェクトを SetActive メソッドを利用して非表示します。
これにより、オブジェクトが再度、オブジェクトプール内に戻り、プールされます。

 このようにデリゲート部分には、メソッドだけではなく、処理が1行である場合にはラムダ式を利用して、直接処理を記述することも出来ます。



 第4引数は actionOnDestroy です。こちらも Action<T> 型です。
これには直接の実行命令はなく、オブジェクトプールが第7引数で設定する maxSizeに達した際(オブジェクトをプールに戻せなかったとき)に自動的に呼び出される処理を設定します。
プールからあふれてしまったオブジェクトに対しての処理になりますので、主にオブジェクトの破壊処理を設定します。
target => Destroy(target.gameObject) と設定されていますので、引数で受けた target のオブジェクトを破壊します。
この場合、このオブジェクトはプールされずに破棄されます。

 いずれの場合も、オブジェクトプールからの命令により、いずれかのイベントが実行されます。


3.第5〜7引数


 第5〜7引数は、オブジェクトプールの設定値です。



 第5引数は collectionCheck です。bool 型です。チェック機能のオンオフ切り替えです。ここでは true に設定しています。
true に設定しておくことで、オブジェクトのインスタンスがプールに戻されるときに自動的に実行されます。
オブジェクトのインスタンスをプールに戻す際、同一のインスタンスが登録されているか調べ、すでに登録がある場合は、例外がスローされます。
この機能はエディターでのみ実行されます。



 第6引数は defaultCapacity です。int 型です。ここでは 10 に指定しています。
最初にオブジェクトプール内にオブジェクトを生成する命令がきたとき、
この値の分だけ、オブジェクトプール内にオブジェクトのインスタンスを生成します。

 今回の指定値の場合、最初の生成命令により10個のオブジェクトを生成することになり、再利用されることが保証されます。
ゲーム開始時など、予めオブジェクトを用意しておきたい場合などに役立ちます。



 第7引数は maxSize です。int 型です。ここでは 1000 に指定しています。
オブジェクトプールの最大サイズです。この値がオブジェクトプール内に保持できるオブジェクトの総数になります。
オブジェクトプールが最大サイズに達すると、プールに返されたそれ以上のインスタンスは無視され、ガベージコレクションされる可能性があります。
そのため、この値を適切に使用すると、プールのサイズが非常に大きくなるのを防ぐことができます。


5.BulletBase クラスを継承した子クラスの作成   OrbitingBullet_Pool クラスー


 OrbitingBullet クラスを元に修正し、BulletBase を継承し、ObjectPool の機能を備えた新しいクラスを作成します。

 前のクラスは残しておいて、こちらの処理と比較しながら、どの部分がリファクタリングされているかを確認してみましょう。



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




 SetUp メソッドと Shoot メソッドを override して実装しています。
Shoot メソッドは、このクラスでは利用しませんが、BulletBase での Shoot メソッドが abstract によって定義されているため、
実行したい処理がない場合であっても必ず定義しなければなりません。抽象メソッドによる制約です。

 この制約により、BulletBase クラスを継承しているサブクラスには、必ず Shoot メソッドを定義する必要があります。
このクラスでは利用しませんが、他の弾用のクラスにも実装の強制が生じますので、弾を発射する機能を書くことが保証されるという意味合いがあります。



 なお、もしも敵側に旋回する弾があたったとき、弾を破壊している場合には、
Destroy させている部分を、ReleaseBullet() メソッドを実行するように変更してください。
 
 敵に接触しても旋回する弾自体が破壊されていないのであれば問題ありません。


6.BulletBase クラスを継承した子クラスの作成◆  ThrowingBullet_Poolクラスー


 こちらも以前に制作した ThrowingBullet を、BulletBase を継承し、かつ、オブジェクトプールに対応した形にリファクタリングします。

 新しくクラスを制作して比較する学習方法がよいでしょう。



ThrowingBullet_Pool.cs

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




 OrbitingBullet_Pool と同じように SetUp メソッドと Shoot メソッドを override して実装しています。
SetUp メソッド内の処理も OrbitingBullet とは異なっていますし、Shoot メソッドでは投射の処理を行い、
そのままオブジェクトプールに戻すためのコルーチンメソッドの呼び出しも行っています。



 BulletBase クラスの ReleaseBullet メソッドは、戻り値の型は void で、引数はありませんでしたが
ここでは新しくコルーチンメソッドとして ReleaseBullet メソッドを作成するとともに、引数も設けています。

 このように同じ名前のメソッドであっても引数の設定が異なる場合には定義することが出来ます。
この機能をオーバーロード機能と呼びます。



 引き続き、オブジェクトプールのリファクタリング手法を学習しましょう。

   => 【2D】オブジェクトプールを活用した弾生成の最適化

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3Dレールガンシューティング(応用編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます