タレットやプレイヤーなどが砲撃の弾を大量に生成するときの最適化の処理方法について解説します。
今作っているゲームが500万円売り上げたら紹介しようと思っていましたが、解説して欲しいというコメントがあったので、小規模プールシステムの作り方を大公開(ちょろい)。
この記事では、アンリアルエンジンの基本操作ができて、なおかつブループリントノードの作成を、ノードが見ただけで作ることができる、中・上級者向けの解説を行います。
初心者向けにすると解説のカロリーが高いので今回は行いません。
プールシステムを使用すると何がどう嬉しいのかはCreationチャンネルにアップロードしたプールシステムでFPSが2倍に向上という動画を見てください。見なくてもまあ最適化でFPSを高く維持できるような仕組みを作ることが目的です。
解説動画・もし作ってできたらここに貼り付ける予定 (作らないかもしれない)
解説
プールシステムの種類
まずプールシステムと一口に言っても大きく2種類の作り方があります。弾の数が数千〜数万発生成するかなり大規模なプールシステム。一方、弾を生成する数が数百〜数千と数が少ない小規模なプールシステム。
今回解説するのは小規模なプールシステムです。
余談・以前大規模プールシステムの構造でプールシステムを作りましたがFPSがむしろ下がってしまいました。PoolManagerと通信して弾を受け渡す処理が重たいのかよくわかりませんが、処理も複雑になり作るのが大変でした。
プールシステムの概要
絵のように、「 生成→消去 」ではなく「 生成→ 変数に格納 →再利用 」することでPCのメモリの割当と破棄を大量に行うことなく、大量のアクターを利用する仕組みです。これによりガーベージコレクションが発生することがなくなりメモリを有効活用することで処理負荷を減らすことができます。
プールするのに必要な弾の数、弾の寿命は A B C のパラメータから適宜計算してください。
登場人物
- 生成される弾(BP_Bullet_Enemy_Pool)
- 生成するタレットなど(BPA_Turret_Pool)
- 弾を格納する構造体(ST_BulletPool)(Get a ref ノードを使用するため[後述])
プールシステムを作るに当たり上記の3つを作成してください。
言うまでもなく、タレットなどから弾を生成して、一度生成した弾を消去することなく、再利用するという処理をこれから作っていきます。
構造体の中身
単純に弾を格納するだけです。アクティブ状態か?非アクティブ状態か?はActor Hidden In Gameで取得できるので、わざわざここにアクティブか?どうかのBool変数を作る必要はありません。
注意
すでに既存のプールシステムを使わないタレットブループリントや弾のブループリントがある場合は、それにプールシステムを追加するのではなく、ブループリントを複製して新しいバージョンのブループリントととして作成しましょう。そうすると既存のシステムが破壊されたときに元に戻ることもできるし、プールありなしでの比較も容易にできます。
プールシステムがうまくいかなかった場合は既存のブループリントに戻すだけで問題なく制作を進められます。
タレット
コンポーネント
全体像
字が小さいですが弾を生成するアクターであるタレットのイベントグラフ全体は図のようになっています。処理の流れとしては
- 必要な弾の数を計算
- 弾を生成してプールシステムに格納
- 弾を発生するタイミング(今回はタレットの一定範囲に入ったとき)でプールシステムから弾を利用する
- 弾の寿命やなにかに衝突したときに弾を非アクティブに
- 非アクティブな弾から弾を利用する
です。
今回紹介する処理では非アクティブの弾がない場合は弾は利用されずに非アクティブな弾が発生するまで処理が止まるという欠点がありますが、必要より多めの弾を生成することで弾切れを発生させなくすることができます。このあたりは自分の必要な弾の数を前もって設計しておきましょう。
スタート処理
タレットが攻撃する対象であるプレイヤーを取得しています。ついでに弾を放出する速さを計算しています。UEではcmがデフォルトの単位で扱いにくいので、メートルで指定できるようにしておき、あとからセンチメートルに計算し直すという方法がメートルに慣れた人には分かりやすいです。
必要な弾の数を計算しています。弾を消す距離から弾の弾速をつかって弾の寿命を計算して、発射する間隔から、何個弾を用意すれば連続で砲撃できるのかを計算しています。
プールの作成
EventBeginePlayで弾を必要な数生成します。ゲーム開始時にややゲームが重たくなる弊害がありますが、長くても数秒なので、ロード時間として数えてゲームを遊んでいる人に待ってもらいましょう。
コリジョンをなくしてHiddenInGameでゲーム上に描画されない弾を指定したトランスフォーム(今回は原点)に必要な数生成します。ポイントは構造体で作成した配列変数(ST_PoolSystem)に弾を格納することです。[Get a refノードを使用するため]。弾の寿命もタレット側で計算してその結果を弾(BP_Bullet_Enemy_Pool)に渡しておきましょう。(非アクティブ処理を弾側に実装するため)
プールの利用タイミング
タレットにプレイヤーが一定距離以内に入ったらスフィアコリジョンを使って検出して、タレットの向きをプレイヤーに向ける処理と、砲撃をプールシステムから利用する処理を開始します。SetTimerByEventが何度も起動しないようにBool変数できっかり1回のみ動作するようにブランチで条件を切っています。
プールの利用方法
SetTimerByEventを使って一定間隔で弾を利用します。スタートの時点で生成し、配列変数に格納したプールシステムから順番に弾を取得していきます。1発取得するごとに変数 NowBulletPoolIndex を1づつ増やして配列変数の0から生成した数までアクセスします。ここで重要なのが「 Get a Copy 」ノードではなく「 Get a ref 」ノードを使用することです。Get a refノードを使用することで配列変数に格納したアクターを直接編集利用することができます。構造体を作ったのはここで「 Get a ref 」ノードを利用できるようにするためでした。普通に作った配列変数では「 Get a ref 」ノードは利用できません。
もし配列変数の弾が非アクティブされていない場合はプリントストリングを実行するようにしていますが、必ず弾を生成したい人はここに、弾を追加で生成してプールシステムである配列変数に追加する処理を書き足しておきましょう。(もしくは弾を必要よりすこし余分にプールする)今回の場合は、非アクティブな弾がない場合は何もせず次の非アクティブの弾が発生するまで待つことになります。
コリジョンとゲーム上の見た目がない状態で、生成したいポイントに弾を移動させましょう。今回はタレットバルカンの発射先であるArrowコンポーネントのトランスフォームを利用しています。
このとき弾の物理的な力と回転がリセットされている必要がありますが、それは弾を非アクティブ化するときに行います。弾を発射位置に移動したらコリジョンとゲームの見た目を有効にしましょう。
お好みで弾の発射音を生成しましょう。これもプールすると良いかもしれませんが、そこまでPCに大きな負荷をかけないという理由で音の生成はプールしていません。ちなみにですが、バルカンの発射音は微妙に音が異なる発射音をランダムで鳴らすとよりリアリティのあるバルカンの発射音を再現することができます。ちょっとしたライフハックですね。
続いては弾の前方向(+X)に力をくわえて発射させます。好みの発射速度で発射しましょう。
その後ろの 「 Inactive Actor By Life Time S 」は弾側のカスタムイベントで、弾の寿命を実行する処理になっています。(後述)
あとはプールした配列変数のインデックスを増やして、インデックスが生成した弾の数を超えた場合、再度0番の弾から再利用するようにインデックスを調整しましょう。
これでSetTimerByEventが止まらない限り永遠に弾をプールから取得して弾を砲撃しまくります。
タレットの向きをプレイヤーに向ける
タレットの向きをターゲットの方向に向けることで、敵に攻撃することができます。偏差射撃といって相手の動きを予想して予想座標にタレットを向けることで的中率が100-90%くらいの高精度な射撃をすることができますが、今回は趣旨ではないのと企業秘密で公開はしません。
弾(BP_Bullet_Enemy_Pool)側の処理
弾の寿命
生成された弾は指定したAddImpulseの力によって勢いよく飛んでいきます。そして弾を利用するときに出てきたカスタムイベント「 Inactive Actor By Life Time S 」がここで登場です。
DelayではなくSet Timer By Eventを使用しているのはより正確に時間をカウントできるからだそうです(ChatGPTいわく)。Looping=Falseに注意しましょう。1回動かすだけでOKです。
弾を生成するときに指定した寿命の時間がすぎるとカスタムイベント「 Bullet Deactive 」を読んで弾を非アクティブにします。Destroyではなく非アクティブにすることによって新規にメモリ領域を割り当てたり開放することがなくなり処理が素早くなるという仕組みらしいです。
非アクティブ化
至ってシンプルで、動く力と回転をゼロにリセットして、コリジョンとゲーム上の描画をOFFにしているだけです。今回の肝になる非アクティブ化のブループリントですが、小さくて申し訳ないです。
弾が寿命で尽きる前になにかにぶつかったときにも非アクティブ処理を実行しましょう。
またお好みで弾がぶつかった処理などをここに追加するとリアリティが増すでしょう。
まとめ
これでプールシステムの解説は終わりです。あとはプレイヤーがタレットの攻撃範囲内から出たときに砲撃を止めるとかの処理がありますが、まあその当たりは適宜プロジェクトに応じてカスタマイズしてください。
後処理など
プレイヤーがタレットの攻撃範囲から出たら砲撃のSetTimerByEventを停止させています。
弾が光るようにしているのはマテリアルで設定しています。より弾の動きを強調したい場合は弾の弾道のようなモデル(今回は簡易的にCubeを使用)を作って光らせましょう。もちろんコリジョンはOFFにしてすり抜けるように設定しておきましょう。
コメント
分かりやすいプールシステムの解説記事ありがとうございました!!プールシステムが理解出来ました。助かりました!!!
細かいところかなり端折ったので伝わるか不安でしたが
理解できてよかったです。
あとは実装してできた成果物を見てうっとりするだけですね。