Unityに関連する記事です

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

 エフェクトを生成し、破棄を行うサイクルは、同じエフェクトを繰り返し利用することを考えると大変勿体ない処理です。
そこでこの手順ではオブジェクトプールの機能を作成し、エフェクトを破棄して使い捨てるのではなくて、非表示にしておいて、再利用する仕組みを作成します。

 こちらの処理を実装するにあたり、事前準備が必要になります。

   => スクリプタブル・オブジェクトとオブジェクトプールを活用したエフェクト複数種管理
   => スクリプタブル・オブジェクトとオブジェクトプールを活用したエフェクト複数種管理

 先にこちらの処理を実装し、理解を深めておいてください。



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

 ・オブジェクトプールの活用



1.EffectManager ゲームオブジェクトと、EffectManager スクリプトを作成する

1.設計


 エフェクトを再生する処理を考えた場合、エフェクトを再生したいゲームオブジェクトのクラス内に専用の変数を作成し、
エフェクトのプレハブをアサインしておく方法が考えられます。
例えば PlayerController 内にプレイヤーへの被弾時の処理用のエフェクトを登録するための変数を用意する形です。

 エフェクトは色々な場所で用いられるため、この方法の場合、色々なゲームオブジェクトに対して異なるエフェクトをアサインする手間が発生します。
色々なオブジェクトを見ていく都合上、管理が煩雑になるため、結果としてアサイン忘れが発生したり、それに気づくのが遅れてしまったりします。



 そこで今回は、エフェクト関連の処理をすべて管理するためのマネージャークラスとして EffectManager スクリプトを作成して、
ゲームオブジェクト単位で設定していたエフェクト情報を、1つのクラスにまとめて管理するように設計を考えます。
ここの中に、パーティクルのプレハブをエフェクトとして再生したり、停止したりする制御の実装を行います。



 EffectManager スクリプトには、エフェクトに関連する機能をすべて管理する役割を持たせます。
そのためこれらのオーディオ関係の処理は、いつどのスクリプトでもよいように、シングルトンデザインパターンを採用して設計を行うことを考えます。

 エフェクトに限らず、SE なども、いずれのスクリプトからでも実行命令が発生する可能性があります。
そういった場合、各スクリプトごとに EffectManager スクリプトの変数を用意して代入処理を行うのでは、非常に労力がかかります。
恐らくすべてのスクリプトに追加をするようになってしまう可能性もあります。

 こういったケースの場合は、各スクリプトが変数を用意して利用できる状態にするのではなく、
EffectManager 側が、いずれのスクリプトから、いつでも利用しても問題ない状態にしておく設計の方が管理もアクセスも容易になります。

 一方からの処理を考えるのではなく、双方の処理を見直して、どのように設計を行っておいたほうが
先々の利便性や修正作業、管理が簡単かつ、わかりやすくなるのかを、念頭に置いて設計を行うようにしましょう。


2.EffectManager スクリプトを作成する


 シングルトンデザインパターンを活用し、EffectManager を作成します。
このクラス内に Unity で用意されているオブジェクトプールの機能を組みこみます。
オブジェクトプールを活用することにより、一度生成したエフェクト用のゲームオブジェクトを使い捨てせずに再利用することで、パフォーマンスの向上が期待できます。

 変数やメソッドも多く、処理も複雑ですので、処理を記述して満足するのではなく、
どの処理がどのように動いているか、どの処理と処理がつながっているかをしっかりと副種して、処理を把握出来るようにしましょう。
処理の内容がわからないままだと、自分で修正作業が行えなくなってしまうためです。


 なお,鉢△僚萢はエフェクトの再生時間を設定する機能です。
再生時間から自動で設定する場合には ⊆蠧阿妊好リプタブル・オブジェクトに入力している場合には◆△い困譴を実装してください。


EffectManager.cs




 スクリプトを作成したらセーブします。

 ここで、オブジェクトプール用にオブジェクトプールの初期化、
オブジェクトプールとして作成したエフェクト用のゲームオブジェクトの取得・返却を行うようにしました。



 Dictionary の Key は EffectName でも実装可能です。
その場合の EffectManager は、下記の通りになります。


EffectManager.cs




3.<シングルトンデザインパターン>


 こちらのクラスはシングルトン・デザインパターンになっています。
シングルトンにすることによって、いずれのクラスからでもエフェクトを見つける命令しやすい設計にしています。



 シングルトンとは、数多くあるデザインパターンの1つです。そのクラスのインスタンスが必ず1つであることを保証するデザインパターンのことを言います。

 EffectManagerクラスでは、このシングルトンを採用しています。つまり、ゲーム中を通じて、このEffectManagerクラスが1つしか存在できないようになります。
実装例は複数ありますが、一番読みやすい方式で記述しています。

 このシングルトンによってインスタンスが1つか生成されないことが保証されますので、このEffectManagerクラスへの参照は、いずれのクラスからであっても変数を介さずに参照を行えるようになります。

 例えば、Enemyというクラスがあり、そのEmenyクラスを持つゲームオブジェクトが5つあった場合、「どの」Enemyクラスであるかを確定できないと、対象となるEnemyクラスへは参照できません。
そのため、Enemy型の変数を用意して、その変数へ参照したいEnemyクラスを代入することによって、はじめてEnemyクラスの情報を扱うことができるようになります。

 ですがシングルトンであるEffectManagerクラスの場合には、このインスタンスは常に1つしかないことが保証されていますので、「どの」という指定の部分が不要になります。
つまり変数への代入が不要になります。
EffectManagerという指定はすなわち、自動的にただ1つのEffectManagerへの参照が行われます。


2.オブジェクトプール


 オブジェクトプールとは、デザインパターンの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 だが、インスタンスを作る訳ではない
        PoolBase pool = new ObjectPool<PoolBase>(
            createFunc: () => CreateEffect(effectPrefab),
            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 に指定しています。
プールで利用するコレクションの初期許容量を示しています。



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


5.EffectManager ゲームオブジェクトを作成して、EffectManager スクリプトをアタッチし、設定を行う


 ヒエラルキーの空いている場所を選択して右クリックをしてメニューを開き、Create Empty を選択して空のゲームオブジェクトを1つ作成します。
名前を EffectManager に変更します。

 先ほど作成した EffectManager スクリプトをドラッグアンドドロップしてアタッチしてください。



 続けて EffectManager ゲームオブジェクトを選択して、インスペクターにて EffectManager スクリプトのアタッチを確認し、設定を行います。

 EffectDataSO 変数に、Datas フォルダにある EffectDataSO スクリプタブル・オブジェクトをドラッグアンドドロップしてアサインします。
他の部分はそのままで問題ありません。


EffectManager ゲームオブジェクト インスペクター画像



 以上で設定は完了です。


3.利用例

1.設計


 EffectManager クラスは、シングルトンデザインパターンによって作成されています。
そのため、アサインやクラスの参照は不要(GetComponent 不要)で、いずれのスクリプトからでも自由に参照して、メソッドを実行することが可能です。

 エフェクト再生のたびに、各スクリプト内に EffectManager スクリプトの変数を用意して取得をしていると、
例えば、エネミーの攻撃に合わせてエフェクトを再生したいとなった場合、生成されるたびに EffectManager スクリプトの参照を取得する必要が生まれます。

 こういった【複数のスクリプトから参照される用途があり、汎用的に利用される情報】については、
シングルトンデザインパターンを採用することにより、各スクリプト内に新しい変数を追加する必要もなくなり、
エフェクトの再生処理についてもメソッドを実行する命令文を1行記述すれば済みます。



 EffectManager クラスに記述したメソッドは、【クラス名.クラスの代入されている変数名.実行したい変数名かメソッド名(引数の指定)】の書式で記述します。

 今回の場合であれば、エフェクトを使用したい時は GetEffect メソッドを呼び出し、不要になったパーティクルは Release メソッドでオブジェクトプールに戻します。

 この手法を利用することにより、必要になったエフェクトがない場合だけ、新しいゲームオブジェクト(パーティクルやAnimator)を作成し、
すでに不要となってオブジェクトプール内に待機しているエフェクトがある場合には、それを再利用することで、毎回のエフェクトの生成処理を止めて、効率化しています。

<シングルトンクラスのメソッドの実行命令>
  EffectManager.instance.GetEffect(EffectName.Hit);

 引数に EffectName を指定することにより、指定した EffectName の設定されている EffectData が参照されて、
そのクラス内にアサインされているエフェクトのプレハブを利用する設計になっています。



 それではレベルアップの際の、実際の実装例です。

 GetEffect メソッドは戻り値として ValueTuple 型の情報を戻しますので、
受け取る側も ValueTuple 型で変数を用意します。

 ,鉢△僚萢は、自分の EffectData の構造に合わせて利用方法を変えますので、いずれかの命令を採用します。


// EffectDataSO スクリプタブル・オブジェクト内の EffectData の enum と照合するための値
public EffectName effectName = EffectName.LevelUp;

// EffectDataSO スクリプタブル・オブジェクト内の EffectData の Index と照合するための値
public const int LEVEL_UP_EFFECT_INDEX = 501;

// エフェクトの再生先。ここではプレイヤーの想定
public Transform targetTran;


// レベルアップ演出
{
    // .ブジェクトプールからレベルアップ用のエフェクトを EffectName を指定して取り出し
    (EffectPlayerBase levelUpEffect, float duraion) = EffectManager.instance.GetEffect(effectName);

    // ▲ブジェクトプールからレベルアップ用のエフェクトを Index を指定して取り出し
    (EffectPlayerBase levelUpEffect, float duraion) = EffectManager.instance.GetEffect(LEVEL_UP_EFFECT_INDEX);

    // エフェクトを表示する位置を調整
    Vector3 effectPos = new(targetTran.position. x, targetTran.position. y + offset, targetTran.position. z);

    var token = PlayerManager.instance.GetCancellationTokenOnDestroy();

    // プレイヤーの足元で再生
    levelUpEffect.PlayEffect(duraion, effectPos, token).Forget();
}


2.ゲームを実行して動作を確認する


 実際に処理を書いて、動かしてみましょう!

 いままで個別のゲームオブジェクトに登録していたエフェクトの変数があれば、一旦そちらをコメントアウトします。
そして、エフェクトの再生処理の部分を、先程使い方を紹介した EffectManager からエフェクトを再生する処理に置き換えてください。


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

コメントをかく


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

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

Menu



技術/知識(実装例)

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

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

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

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

レースゲーム(抜粋)

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

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

3D脱出ゲーム(抜粋)

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

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

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

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

VideoPlayer イベント連動の実装例

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

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

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

private



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

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