マニュアル: 非同期シーンロードとオブジェクトプール(ローディング画面/オブジェクトプール)

このマニュアルについて

このマニュアルでは、ローディング画面オブジェクトプールの設定方法を解説します。「シーンを切り替えたときに一瞬固まる」「暗転が明けてからオブジェクトがパラパラ出てくる」「弾をまとめて撃つとそこだけカクつく」といった、読み込みと生成が原因のつまずきを減らすための機能です。

どちらも読み込みのタイミングをずらすという同じ狙いの機能で、ローディング画面の進捗にはオブジェクトプールの事前生成も含まれます。あわせて設定すると効果がわかりやすくなります。

ACTION GAME MAKER での読み込みの概要

読み込みに関わる仕組みは 3 つあり、必要な設定量が違います。

仕組み 何をするか 必要な設定
シーンの非同期ロード シーンの読み込みを裏側で進め、本体の処理を止めない なし(常に有効。オン/オフの設定はありません)
ローディング画面 ロード中に自分で作った画面を表示する シーンを作り、設定で 選ぶだけ
オブジェクトプール オブジェクトの実体を先に作っておき、消えたら捨てずに使い回す ノードを 1 つ置くだけ

非同期ロード自体は何も設定しなくても働いています。 ロードが裏で進むおかげで、ローディング画面はロード中もアニメーションし続けられますし、遷移先のシーンが実際に描画できる状態になってから画面が切り替わります。

オブジェクトプールが狙っているのはインスタンス生成にかかる時間の削減です。メモリの節約や断片化対策ではありません。


ローディング画面

設定のしかた

  1. マップ一覧の「+ 新規作成」を押し、種類で「ローディングシーン」を選んで名前を付けます。res://loadingscenes に作られ、そのまま編集できます。
  2. 設定」メニュー →「基本的な設定」を開きます。
  3. ローディングシーン」の「有効」をオンにします。
  4. シーンのパス」のプルダウンから 1 で作ったシーンを選びます。

以上です。撤去のタイミングや、遷移の種類ごとの設定は必要ありません。

プルダウンにはプロジェクト内のローディングシーンが並びます(ルートが LoadingScene のシーンを自動で拾います)。「指定なし」に戻せば未設定になります。

有効」をオフに戻せば、ローディング画面を使わない従来どおりの挙動になります。

置き場所を変えたい場合は、同じ「基本的な設定」の素材フォルダの欄で「ローディングシーン」のフォルダを変更できます。

編集のしかた

作ったローディングシーンは、マップやメニューと同じマップ制作ビューで編集します。マップ一覧の種別プルダウンを「ローディングシーン」にすると一覧に出ます。

レイヤードックのルート行は「ローディング設定」で、その「」から置けるのは次の6種です。ローディング中はゲームが動いていないため、表示物とアニメーションだけに絞っています。

スプライト / 単色矩形 / アニメーション / ラベル / リッチテキスト / ナインパッチレクト

マップと違いレイヤーはありません。要素はルート直下に並びます。

ルートノードの型

何でも構いません。 CanvasLayer / Control / Node2D などをそのまま使えます。一番手前に表示する処理はエンジン側で行うので、layer の調整も不要です。マップ一覧から作った場合は LoadingScene になります。

GameScene / MenuScene / CoreScene をルートに持つシーンだけは指定できません。これらはゲーム側が管理するシーンで、同時に 2 つ存在させられないためです。指定するとログにエラーが出て、ローディング画面は表示されません。

ロードの開始・完了を受け取りたい場合と、ビジュアルスクリプトで動くキャラクターを置きたい場合は、専用の LoadingScene を使います(後述)。

置くだけで動くもの

ローディング画面のシーンに置くだけで、エンジンが面倒を見ます。つなぐ設定は要りません。

置くもの どうなるか
シンプルゲージ / イメージゲージ / ProgressBar エンジンが値を更新します。ノードの最小値/最大値をそのまま使うので、既定の 0〜100 のままで動きます
AnimationPlayerhide という名前のアニメーション 撤去時に再生され、再生が終わってから画面が消えます(loading_out という名前でも同じです)
AnimationPlayer自動再生 ロード中ずっと動き続けます。ゲームがポーズ状態でも止まりません

進捗バーは Range を継承したノードなら何でも使えます。ACTG のシンプルゲージ/イメージゲージもこれに含まれるので、そのまま置けば進捗が出ます(表示する変数を指定していないゲージは、外から書かれた値をそのまま表示します)。

ただしツリー順で最初に見つかった 1 つだけが更新されます。複数のバーへ配りたい場合は、ルートのスクリプトで set_progress を受け取ってください(引数は 0.0〜1.0 です)。set_progress を実装すると、ゲージの自動更新は止まります(二重に触らないようにするためです)。

hide アニメーションを置かなかった場合は、エンジンが用意した暗幕で自動的にフェードします。

hide のループはオフにしてください。 ループしているとアニメーションが終わらず、完了の通知が来ません。エンジンはループ設定を検出するとログで知らせ、暗幕での撤去に切り替えます。

ビジュアルスクリプトで動くキャラクターを置きたい場合だけは、ルートの型を LoadingScene にする必要があります(次の項を参照)。

ルートを LoadingScene にすると増えること

ローディング画面のルートには LoadingScene を指定できます。指定は任意で、設定項目は持っていません。指定すると次の 2 つができるようになります。

できること 内容
ロードの通知を受け取る 進捗の変化・ロードの開始・完了を signal で受け取れます
ビジュアルスクリプトのキャラを動かす 子に置いたビジュアルスクリプト付きのオブジェクトが、通常のシーンと同じように動きます

2 つめは LoadingScene が子孫を走査して登録するため、ルートが CanvasLayer などのままでは動きません。 ローディング中に歩き回るキャラを置きたい場合は LoadingScene にしてください。

逆に、前の項に挙げたもの(進捗バーの自動更新、hide による撤去演出、自動再生、暗幕、消えるタイミング、set_progress)はルートの型に関係なく動きます。「進捗の表示を自分で加工したいだけ」なら set_progress を実装すれば済むので、LoadingScene は必要ありません。

消えるタイミング

次がすべて揃うと消えます。設定項目はありません。

  1. 遷移先シーンのロードが終わった
  2. 遷移先シーンが画面に配置され、動き始めた
  3. 配置後に 2 フレーム描画された
  4. オブジェクトプールの事前生成が終わった
  5. 最低表示時間(0.3 秒)が経過した

3 番目が「暗転が明けてからオブジェクトが出てくる」現象への対策です。ローディング画面の裏でシーンを温めてから消すので、消えた直後からすぐ遊べます。

遷移先シーンは、撤去が決まるまで動き出しません。 事前生成の待ち時間ぶんゲームが見えないところで進まないようにするためです。動き出すのは撤去演出(暗幕の閉じ、または hide)が始まる時点なので、画面が隠れている間に初回フレームが走り、見え始めるときには表示が確定しています。

ゲームシーンに設定した BGM も撤去まで待ちます。 画面がまだ出ているうちに BGM だけ先に始まることはありません。

5 番目は、ロードが一瞬で終わったときにローディング画面がチラつくのを防ぐためのものです。この 0.3 秒は暗幕が開ききってから測るので、「一瞬出てすぐ消える」ことはありません。

出現と消滅のフェード(暗幕)

ローディング画面がいきなり出ていきなり消えるとチカチカして見えるため、エンジンが暗幕を開閉して、切り替わる瞬間は必ず画面が単色になるようにしています。

動き 長さ
ローディング画面が現れる 0.2 秒 遷移前演出の色に合わせます
ローディング画面が消える 0.2 秒 遷移後演出の色に合わせます
遷移先シーンが現れる 遷移後演出の長さ(最低 0.2 秒) 遷移後演出の色に合わせます

最後の「遷移先シーンが現れる」は、シーン遷移エディタで設定した遷移後演出(明転)の長さをそのまま使います。遷移後演出を「なし」にしていても、カットにならないよう最低 0.2 秒はフェードします。

遷移後演出をスライド系(SLIDE_UP など)にしている場合は、暗幕が指定した方向へ退くワイプになります。「新しいシーンの画像が動いてくる」本来のスライドは再現できません(シーンの中にカメラがあるため、シーンをずらしてもカメラごと動いて画面上は動かないためです)。方向は設定どおりに尊重されます。

hide アニメーションを自分で置いた場合は、消える以降の演出は作者側に任せます(暗幕は使いません)。二重にフェードするのを避けるためです。

進捗バーの動きについて

進捗バーは完了までの見込みを表すもので、厳密なパーセンテージではありません。

区間 内容
0〜60% リソースのロード中
60〜85% ロード完了からシーン配置まで
100% 撤去が確定

各区間の上限へ向かって滑らかに伸びていき、段階が進むと加速します。巻き戻ることはありません。 また 100% で止まったまま待たされることもありません。

オブジェクトプールの事前生成がある場合は、そこから 99% までを事前生成の進捗が埋めます。 こちらは実際に作った数の割合なので、時間による滑らかな伸びではなく生成の進み方そのままに動きます。事前生成が始まる位置は最低 30% からで、それより手前で始まることはありません(ロード中にバーが動かず止まって見えるのを避けるためです)。

厳密な値にできないのはエンジン側の制約です。ロードの途中経過を取得できず、さらに遷移先のシーンは近傍シーンとして先読みされていることが多く、ロード自体が一瞬で終わってしまうためです。

起動時の流れ

起動すると、まず ACTION GAME MAKER のスプラッシュが 1 秒表示され、そのあとローディング画面へ進みます。

スプラッシュを出している間もロードは裏で進むので、スプラッシュを見せるぶん起動が遅くなることはありません。 最初のシーンが軽く、スプラッシュ表示中にロードが終わった場合は、ローディング画面を出さずにそのままゲームへ移ります(チラつき防止)。

スプラッシュは「アプリケーション」→「Boot Splash」の設定(画像/背景色/拡大/フィルタ)をそのまま反映します。指定が無ければ ACTION GAME MAKER 内蔵のスプラッシュになります。

ただし、起動時にオブジェクトプールの事前生成が残っている場合は、チラつき防止よりローディング画面の表示を優先します(事前生成の完了を待つ相手が必要なためです)。

連結スライド遷移では表示しません

シーン遷移エディタで連結スライドSLIDE_UP_CONNECT など)を指定した遷移では、ローディング画面を表示しません。連結スライドは「隣の部屋へシームレスに移動する」ための演出で、ローディング画面を挟むと連結スライドとプレイヤーの移動演出がすべて隠れてしまうためです。

遷移前演出・遷移後演出のどちらかに連結スライドを指定していれば、表示しません。


オブジェクトプール

どういうときに効くか

オブジェクトを新しく作る処理には時間がかかります。1 体ずつなら気になりませんが、弾を 20 発同時に発射するようなまとめて作る場面では、そこだけフレームが落ちます。

オブジェクトプールは、その実体をローディング中に先に作っておき、消滅時も捨てずに休ませておいて次の生成で使い回す仕組みです。生成の待ち時間がなくなるので、まとめて出しても引っかかりません。

設定のしかた

  1. マップ制作ビューのレイヤードックで「マップ設定」の「」を押し、「ゲームオブジェクトプール」を選びます。
  2. プールの一覧」に要素を追加します。
  3. オブジェクトパス」に、プール対象にしたいオブジェクトのシーンを指定します。
  4. 必要に応じて「事前に生成する数」などを調整します。

以上です。設定はこのノードだけで完結します。 オブジェクト側・弾データ側・ビジュアルスクリプト側に設定する項目はありません。一覧へ登録したオブジェクトは、どの生成経路からでもプール経由になります。

追加先はマップ設定の直下です(事前生成の計画がシーンの読み込み時点で行われるため、レイヤーの中には置きません)。

ゲームオブジェクトプールの設定項目

プールの一覧」の 1 要素につき、次の項目があります。

項目 意味 既定値
プールの表示名 一覧で見分けを付けるための名前。参照には使いません
オブジェクトパス 生成元となるオブジェクトのシーン。このシーンがプールの識別子になります
事前読み込みのみ行う ロードだけ先に済ませ、プールは持たない オフ
事前に生成する数 あらかじめ作っておく数 8
不足時に自動で追加生成する 在庫が尽きたときに追加で作るか オン

不足時に自動で追加生成する」をオフにすると、在庫が尽きた分は通常どおり新しく作られ、その個体はプールへ返却されません。エラーにはならず、プールを使う前と同じ挙動になるだけです。

事前生成はフレームをまたいで少しずつ行われます(1 フレームあたりの時間に上限を設けています)。まとめて作り切ってフレームが止まることはありません。進捗はローディング画面へ渡され、事前生成が終わるまでローディング画面は消えません。

ノードの置き場所

休眠中の在庫はこのノードが持っています。 そのため、ノードが破棄されると在庫も失われます。置き場所で挙動が変わります。

置き場所 挙動
シーン遷移で破棄されない場所 在庫がシーンをまたいで残ります。遷移時は貸し出し中の分も返却されるので、遷移後もそのまま使えます
ゲームシーンの中 遷移でノードごと破棄され、在庫も失われます。遷移先のノードが改めて事前生成します

在庫をシーンまたぎで使い回したい場合は、常駐させてください。 手軽な方法は自動読み込みです。ゲームオブジェクトプールを 1 つ置いただけのシーンを作り、プロジェクト設定の「グローバル」→「自動読み込み」に登録します。登録したノードはゲーム開始時に一度だけ作られ、シーン遷移では破棄されません。

別のマップからコピーして持ってくることもできます。 貼り付け先はマップ設定の直下になります。

実行時に生成されるゲームオブジェクトの子に置いてはいけません。 事前生成の計画はシーンの読み込み時点で済んでいる必要があるため、専用のノードとしてシーンへ直接置いてください。

事前読み込みのみ行う

事前読み込みのみ行う」をオンにすると、ロードだけを先に済ませて、在庫は持ちません。

  • 生成元シーンのロード後に 1 体だけ作って即座に捨てます。シーンとその素材はロード済みのまま残るので、実際に生成するときの引っかかりが減ります
  • そのシーンのオブジェクトは通常どおり生成されます(プールから配られず、消滅時もそのまま消えます)
  • ルートノードの型を問いません。 ゲームオブジェクト以外のシーンや、UI のシーンも登録できます
  • 「事前に生成する数」「不足時に自動で追加生成する」は使わないため、編集できなくなります

「使い回しはしたくないが、出た瞬間の引っかかりは消したい」オブジェクトに向いています。

エフェクトとパーティクルはこの設定が効きます。 EffectObject / ParticleObject は使い回しの対象外(後述)ですが、「事前読み込みのみ行う」なら型を問わないので登録できます。初めて表示するときのロードを先に済ませられるため、初回の引っかかりが軽くなります。 使う予定のエフェクトをまとめて登録しておくとよいでしょう。

対象になるオブジェクト

使い回しの対象になるのは、ゲームオブジェクトと軽量オブジェクトだけです。 返却をこの 2 種類の消滅処理から行っているためです。

物理演算で動くオブジェクト(PhysicsGameObject)、エフェクト(EffectObject)、パーティクル(ParticleObject、その他のノードは対象外です。これらは消滅時にプールへ返却する経路を持たないためです。「オブジェクトパス」には任意の .tscn を指定できてしまいますが、ルートが対象の種類でないシーンはプールが作られません(実行時にログで知らせます)。

「事前読み込みのみ行う」の場合はこの制限が掛かりません。 エフェクトやパーティクルは、こちらで登録して初回のロードだけ先に済ませてください。

効く生成経路

一覧へ登録すれば、下記のすべてが自動的にプール経由になります。 経路ごとの設定はありません。

経路
弾の発射 弾を撃つアクション
オブジェクトの生成 「オブジェクトを生成」アクション
接続オブジェクト 子として生成されるオブジェクト
オブジェクトの変更 「オブジェクトを変更」アクション
復活 消滅したオブジェクトの復活、カメラ外からの復帰、シーン切り替え後の復元

返却されるタイミング

オブジェクトが消えるときに、削除の代わりにプールへ戻ります。 消滅アクション、オブジェクトの変更、シーン遷移のいずれでも返却されます。

戻ってきた個体は、返却の時点でシーンに保存されている状態へ戻されます。実行時に変わったプロパティは保存値へ、実行時に追加されたノードは削除、ビジュアルスクリプトは初期アクションから走り直す状態へ戻ります。取り出すときではなく戻すときに片付けるので、まとめて出す場面でコストが増えません。

シーンに最初から配置してあるオブジェクトは返却の対象外です(プールから配られたものではないため、そのまま削除されます)。

エディタの警告とログ

設定の取りこぼしは、シーンツリー上の警告で気付けます。

  • オブジェクトパス」が未設定
  • オブジェクトパス」が重複(同じシーンを複数登録すると、後ろ側は使われません)

実行時には、事前生成の失敗をログで知らせます(パス未設定/パスが解決できない/重複/ロード失敗/ルートが対象の種類でない)。

プールが無いこと自体はログに出ません。 一覧へ登録していないオブジェクトは通常生成されるだけで、設定ミスではないためです。

在庫が尽きただけの場合(「不足時に自動で追加生成する」がオフ)は、通常の生成へ回すだけなので何も出ません。


実用例

弾を 20 発同時に撃つとそこだけカクつく

  1. ゲームオブジェクトプールを置き、「オブジェクトパス」に弾のシーンを指定します。
  2. 事前に生成する数」を、同時に画面に出る最大数より少し多めにします(20 発なら 24 など)。
  3. 弾データ側は何も変えずに実行します。

在庫が足りているかどうかは、「不足時に自動で追加生成する」をオフにして試すと分かります。足りていなければ途中の弾だけ引っかかるので、その分「事前に生成する数」を増やしてください。確認が済んだらオンへ戻しておくと、想定外の場面でも弾が出なくならずに済みます。

ローディング画面に「Now Loading」を出す

  1. マップ一覧の「+ 新規作成」→ 種類「ローディングシーン」で作ります。

この時点で、黒い背景・ゲージ・「NOW LOADING …」のラベル・点を増減させるアニメーションが入った状態になっています。あとは「設定」→「基本的な設定」で有効にし、プルダウンからこのシーンを選べば動きます。

見た目を変えたい場合は、レイヤードックの「」から要素を足し、ラベルの文言やゲージの色を調整してください。消えるときの演出を自分で作る場合は、AnimationPlayerhide という名前のアニメーションを追加します(ループはオフにしてください)。

ボスのシーンだけ先に読み込んでおく

使い回しはしたくないが、出現時の引っかかりを消したい場合です。

  1. ゲームオブジェクトプールの「プールの一覧」に要素を追加します。
  2. オブジェクトパス」にボスのシーンを指定します。
  3. 事前読み込みのみ行う」をオンにします。

ボスは通常どおり生成されますが、シーンと素材はロード済みなので出現が軽くなります。

在庫をステージをまたいで使い回す

  1. ゲームオブジェクトプールを 1 つ置いただけのシーンを作り、res:// に保存します。
  2. プロジェクト設定の「グローバル」→「自動読み込み」でそのシーンを登録します。
  3. プールの設定はそのシーンのノードに並べます。

ゲーム開始時に一度だけ事前生成が走り、以降のシーン遷移では作り直されません。ステージごとに置く場合との違いは在庫が残るかどうかだけで、設定項目は同じです。


注意点

  • 非同期ロードにオン/オフの設定はありません。 シーンの読み込みは常に裏側で進みます。設定が要るのはローディング画面とオブジェクトプールだけです。
  • ローディングシーンの設定は「設定」→「基本的な設定」にあります。 プロジェクト設定から直接触る場合は「高度な設定」側(application/loading_scene/*)なので、トグルをオンにするか検索欄に loading と入れてください。
  • ローディング画面のルートに GameScene / MenuScene / CoreScene は指定できません。 指定するとログにエラーが出て表示されません。
  • ローディング画面にビジュアルスクリプトのキャラを置くなら、ルートを LoadingScene にしてください。 他の型では登録されず、置いても動きません。エラーも警告も出ないので気付きにくい箇所です。
  • hide アニメーションのループはオフにしてください。 ループしていると完了の通知が来ません。検出するとログで知らせ、暗幕での撤去に切り替えます。
  • 撤去が成立しないまま時間が経つと、強制的に撤去します。 何を待っていたかはログに出ます。事前生成の数が極端に多い場合もここで打ち切られますが、残りは取り出し時に作られるので動作に影響はありません。
  • 進捗バーは 1 つだけ自動更新されます。 複数出したい場合はルートのスクリプトで set_progress を受け取ってください。実装すると自動更新は止まります。
  • 連結スライド遷移ではローディング画面が出ません。 遷移前・遷移後のどちらかに連結スライドを指定していれば表示しません(仕様です)。
  • ゲームオブジェクトプールのノードが破棄されると在庫も失われます。 シーンまたぎで使い回したい場合は、自動読み込みなどで常駐させてください。
  • 同じシーンを複数のプールに登録しないでください。 生成元シーンがプールの識別子なので、後ろ側は使われません。シーンツリーの警告で気付けます。
  • プールの対象はゲームオブジェクトと軽量オブジェクトだけです。 それ以外を「オブジェクトパス」に指定してもプールは作られません(「事前読み込みのみ行う」を使う場合は型を問いません)。
  • 在庫が尽きたときの挙動は静かです。 「不足時に自動で追加生成する」がオフだと通常生成へ回るだけで、ログも出ません。在庫が足りているかを確かめたいときは、意図的にオフにして試してください。
  • 使い回しは「消えて戻った個体を作り直さない」だけです。 オブジェクトの状態は返却時にシーンの保存値へ戻されるので、前回の続きから始まることはありません。逆に、実行時に足した子ノードを次回も残したい、といった使い方はできません。
  • 実行時に生成されるオブジェクトの子にゲームオブジェクトプールを置かないでください。 事前生成の計画がシーン読み込み時に済んでいる必要があります。
  • オブジェクトプールが狙っているのは生成時間の削減です。メモリ使用量は事前生成した分だけ増えます。
  • 事前生成の数はそのままローディング時間になります。 1 体あたりの生成時間はオブジェクトの規模によりますが、ビジュアルスクリプト付きの弾では数ミリ秒かかります。数百体にすると数秒単位で伸びるので、実際に同時に出る数を見て決めてください。

GameObjectPools を GDScript から設定する

オブジェクトプール(GameObjectPools)は通常シーンへノードを置いて使いますが、
GDScript から実行時に組み立てることもできます。

プールの利用(取り出し・返却)はエンジン側が自動で行うため、スクリプトから呼ぶ API はありません。
弾・「オブジェクトを生成」アクション・接続オブジェクトなど、どの生成経路でも透過的にプールが使われます。
GDScript で行うのはプールの設定と状態確認だけです。


1. パラメータ(GameObjectPoolData と同じ)

プロパティ 既定値 内容
pool_name String "" 一覧表示用の名前。参照には使われません(付けなくても動きます)
object_path String "" 生成元オブジェクトのシーンファイルパス。res://...tscn / uid://... のどちらでも可。ルートが GameObject / Area2DGameObject のシーンのみis_prewarm_only の場合は制限なし・5 章参照)
is_prewarm_only bool false 事前読み込みのみ行い、プールは持たない(下記参照)
create_count int 8 事前に生成しておく数。0 も指定可(その場合は返却用の準備だけ行われます)。is_prewarm_only がオンのときは無視されます
auto_expand bool true 休眠中のオブジェクトが尽きたときに追加生成するか

is_prewarm_only(事前読み込みのみ)

オンにするとロードだけを先に済ませ、プールは持ちません

  • 生成元シーンのロード後に 1 体だけインスタンス化して即破棄します。シーンとその依存リソースはロード済みのまま残るので、実際に生成するときの引っかかりが減ります
  • そのシーンのオブジェクトは通常どおり生成されます(プールから配られず、消滅時もそのまま解放されます)
  • 使い回さないため、ルートノードの型を問いません
  • プール対象クラスの制限も掛かりません。 ルートノードの型を問わず、どのシーンでも指定できます
  • create_count は無視され、インスペクタでも編集できなくなります

プールは object_path のシーンで識別されます。pool_name は見分けのための表示名です。


2. 最小のサンプル

extends Node

func _ready() -> void:
	var data := GameObjectPoolData.new()
	data.pool_name = "敵の弾"
	data.object_path = "res://objects/enemy_bullet.tscn"
	data.create_count = 64
	data.auto_expand = true

	var data_list: Array[GameObjectPoolData] = [data]

	var pools := GameObjectPools.new()
	# ツリーへ入れる前に設定すること(ツリーへ入った時点で事前生成が始まる)
	pools.set_pool_data_list(data_list)

	add_child(pools)

3. 複数プール + 完了待ち + 在庫確認

extends Node

# プール設定(GameObjectPoolData と同じパラメータ)
const POOL_SETTINGS := [
	{
		"pool_name": "敵の弾",
		"object_path": "res://objects/enemy_bullet.tscn",
		"create_count": 64,
		"auto_expand": true,
	},
	{
		"pool_name": "爆発エフェクト",
		"object_path": "res://objects/explosion.tscn",
		"create_count": 8,
		"auto_expand": false,
	},
]

var _pools: GameObjectPools = null


func _ready() -> void:
	_pools = _create_pools(POOL_SETTINGS)


## 設定リストから GameObjectPools を組み立ててツリーへ追加する
func _create_pools(p_settings: Array) -> GameObjectPools:
	var data_list: Array[GameObjectPoolData] = []

	for setting in p_settings:
		var data := GameObjectPoolData.new()
		data.pool_name = setting.get("pool_name", "")
		data.object_path = setting.get("object_path", "")
		data.create_count = setting.get("create_count", 8)
		data.auto_expand = setting.get("auto_expand", true)
		data_list.append(data)

	var pools := GameObjectPools.new()
	pools.name = "GameObjectPools"

	# 事前生成はツリーへ入った時点で始まるので、設定は add_child より前に行う
	pools.set_pool_data_list(data_list)
	pools.prewarm_finished.connect(_on_prewarm_finished)

	add_child(pools)

	return pools


## 事前生成の完了通知(このノードが受け持つ分がすべて用意できたときに1度だけ呼ばれる)
func _on_prewarm_finished() -> void:
	for setting in POOL_SETTINGS:
		var object_path: String = setting["object_path"]
		print("%s : 休眠 %d 体" % [object_path, _pools.get_dormant_count(object_path)])

4. インスペクタから設定する

@export_custom を使うと、プール設定をインスペクタ上で編集できます。設定値をスクリプトへ書かずに済むため、
プロジェクト側で数を調整したい場合はこちらが扱いやすいです。

extends Node

@export_custom(PROPERTY_HINT_ARRAY_TYPE, "GameObjectPoolData") var list : Array

var _pools: GameObjectPools = null


func _ready() -> void:
	_pools = _create_pools(list)


## 設定リストから GameObjectPools を組み立ててツリーへ追加する
func _create_pools(p_settings: Array) -> GameObjectPools:
	var pools := GameObjectPools.new()
	pools.name = "GameObjectPools"

	# 事前生成はツリーへ入った時点で始まるので、設定は add_child より前に行う
	pools.set_pool_data_list(p_settings)
	pools.prewarm_finished.connect(_on_prewarm_finished)

	add_child(pools)

	return pools


## 事前生成の完了通知(このノードが受け持つ分がすべて用意できたときに1度だけ呼ばれる)
func _on_prewarm_finished() -> void:
	for setting in list:
		var object_path: String = setting.object_path
		print("%s : 休眠 %d 体" % [object_path, _pools.get_dormant_count(object_path)])

このスクリプトを常駐ノード(シーン遷移で破棄されないノード)へ付けると、インスペクタに配列が出て
GameObjectPoolData を要素として追加・編集できます。object_path はファイル選択で指定できます。

補足

  • @export_custom の第 2 引数(hint_string)に要素のクラス名を渡すことで、要素の型がインスペクタへ伝わります
  • 得られるのは型なしの Array ですが、要素が GameObjectPoolData であれば set_pool_data_list() はそのまま受け取ります
  • 配列の中身はスクリプトを付けたシーン(.tscn)へ保存されます。設定を変えたらシーンの保存が必要です
  • 同じスクリプトを複数のノードへ付ける場合は、同じシーンを複数のプールへ登録しないよう注意してください(先に登録された方だけが使われます)

5. 注意点

設定は「ツリーへ入れる前」に行う

事前生成はノードがツリーへ入った時点で始まります。add_child()set_pool_data_list()
呼んでも反映されません。

配列は型付きで渡す

set_pool_data_list()Array[GameObjectPoolData] を受け取ります。スクリプトで組み立てる場合は
型を付けてください(GDScript の型チェックで弾かれることがあります)。

var data_list: Array[GameObjectPoolData] = []   # ← 型を付ける

4 章の @export_custom は型なしの Array になりますが、要素が GameObjectPoolData であれば
そのまま渡せます。

対象は GameObject / Area2DGameObject のみ

プールの対象になるのは GameObjectArea2DGameObject です。返却経路を持つのが
この 2 クラスだけで、返却も両クラスの破棄経路から行っているためです。

PhysicsGameObject や通常の Node2D 派生は対象外です
object_path に指定してもプールは作られません)。

is_prewarm_only を有効にした場合はこの制限が掛かりません。 プールとして使わないため、
ルートノードの型を問わずどのシーンでも指定できます(UI シーンなども可)。ロードだけ先に
済ませたい場合に使えます。

対象オブジェクト側の設定が必要

プールを使うかどうかは一覧への登録だけで決まります。 対象シーンのルートが対象クラスで
オンにしてください。オフのままだとプールは作られず、以下のエラーが出ます。

[GameObjectPools] The root node is not a GameObject or an Area2DGameObject, so the pool was not created.

ノードの置き場所

  • 休眠中のオブジェクトは GameObjectPools ノードが所有します。ノードが破棄されると休眠中の在庫も破棄されます
  • シーンをまたいで使い回したい場合は、シーン遷移で破棄されない場所(常駐するノードの下)へ置いてください。貸出中のオブジェクトは遷移時にプールへ返却されるため、遷移後もそのまま使えます
  • ゲームシーンの中に置いた場合は、遷移でノードごと破棄され在庫も失われます(遷移先のノードが改めて事前生成します)
  • 実行時に生成されるゲームオブジェクトの配下には付けないでください(そのオブジェクトの生死に引きずられます)

常駐させる方法(自動読み込み)

プロジェクト設定 → グローバル → 自動読み込み → Select Script/Scene で登録します。指定できるものは
2 通りあります。

登録するもの 用途
GameObjectPools ノードを置いたシーン(.tscn GDScript を書かずに済みます。ノードを 1 つ置いたシーンを作り、インスペクタでプール設定を並べるだけです
GameObjectPools を組み立てる GDScript をアタッチしたノード(4 章のスクリプトなど) 設定を実行時に決めたい場合や、事前生成の完了を受けて処理したい場合

自動読み込みに登録したノードはゲーム開始時に一度だけ作られ、シーン遷移では破棄されません。
そのためそのまま常駐 GameObjectPools として使えます。在庫はシーンをまたいで残り、遷移時には
貸出中のオブジェクトも返却されます。

GDScript を使う場合、スクリプト単体(.gd)を指定すると Node が自動で作られてアタッチされます。
インスペクタからプール設定を編集したい場合は、そのスクリプトを付けたシーン(.tscn)を作って
登録してください。

事前生成はフレームをまたいで進む

ローディング画面を止めないよう、事前生成は 1 フレームあたりの時間予算内で少しずつ進みます。
add_child() の直後は在庫がまだ 0 です。数を確認したい場合は prewarm_finished を待ってください。

事前生成が途中でも取り出しは可能です(残りをその場で生成して渡します)。

同じシーンを複数のノードに登録した場合

先に登録された GameObjectPools が有効になり、後から登録した側のプールは使われません
(事前生成した分がそのまま無駄になります)。開発ビルドでは以下のログが出ます。

[ACTG][GameObjectPools] ... pool is already registered by another node. (scene:res://...)

auto_expand = false はオブジェクト総数の上限ではない

false にするとプールは増えませんが、在庫が尽きた場合は通常の生成にフォールバックします
(=オブジェクトが作られないわけではありません)。フォールバックで作られた分はプール管理外になり、
消滅時にそのまま解放されます。


6. プールの状態を Label に表示する

GameObjectPools.get_all_pool_status_list()静的メソッドなので、ノードの参照は不要です。
ツリーに入っている全 GameObjectPools のプールが 1 回の呼び出しでまとめて返ります。

extends Label

## 更新間隔(秒)
@export var update_interval: float = 0.2

var _elapsed: float = 0.0


func _process(delta: float) -> void:
	# 取り出し/返却の通知は無いのでポーリングで更新する
	_elapsed += delta
	if _elapsed < update_interval:
		return
	_elapsed = 0.0
	_update_text()


func _update_text() -> void:
	var lines: PackedStringArray = PackedStringArray()

	# 事前生成の進捗(x:生成済み / y:予定)
	var prewarm: Vector2i = GameObjectPools.get_all_prewarm_count()
	lines.append("事前生成 %d/%d %s" % [
		prewarm.x, prewarm.y,
		"完了" if GameObjectPools.is_all_prewarm_finished() else "生成中",
	])

	for status in GameObjectPools.get_all_pool_status_list():
		var pool_name: String = status["pool_name"]
		if pool_name.is_empty():
			pool_name = String(status["scene_path"]).get_file()

		lines.append("%s  使用 %d / 休眠 %d / 予定 %d(残 %d)%s%s" % [
			pool_name,
			status["lending"],
			status["dormant"],
			status["create_count"],
			status["pending_create"],
			"" if status["auto_expand"] else " [自動追加なし]",
			"" if status["is_loaded"] else " [ロード中]",
		])

	text = "\n".join(lines)

返る Dictionary のキーは以下です。

キー 内容
owner_path String そのプールを保持している GameObjectPools のパス(全ノード分をまとめて取った際の見分け用)
pool_name String 設定した表示名
scene_path String 生成元シーンのパス(プールの識別子)
dormant int 休眠中(在庫)の数
lending int 貸出中(使用中)の数。返却処理待ちも含みます
create_count int 事前生成の予定数
pending_create int 事前生成の残り数
auto_expand bool 枯渇時に追加生成するか
is_loaded bool 生成元シーンのロードが済んで使える状態か

設定リストではなく、実際に保持しているプールだけが返ります。ロード失敗や対象クラス外で
作られなかったプールは(設定には残っていても)含まれません。

対象はツリーに入っている GameObjectPools です。同じシーンを複数のノードが登録している場合は
その分だけ並びます(取り出しに使われるのは先に登録された方だけです。owner_path で見分けられます)。

取り出し/返却の通知シグナルは無いため、更新はポーリングで行ってください。弾のように出入りが激しい
プールでも、更新間隔を 0.1〜0.5 秒に取れば表示コストは問題になりません。


7. GDScript から使える API

API 内容
GameObjectPoolData.new() 設定データの生成。プロパティは 1 章のとおり
GameObjectPools.new() プールノードの生成
set_pool_data_list(list) / get_pool_data_list() プール設定リストの設定・取得
get_dormant_count(packed_scene_path) 指定シーンの休眠オブジェクト数
prewarm_finished(シグナル) そのノードが受け持つ事前生成の完了通知(1 度だけ)
GameObjectPools.get_all_pool_status_list() 静的。ツリー内の全プールの状態(ノード参照不要・6 章)
GameObjectPools.get_all_prewarm_count() 静的。事前生成の進捗(x:生成済み / y:予定)
GameObjectPools.is_all_prewarm_finished() 静的。事前生成が完了したか

状態の取得はノードの指定が要らない静的メソッドのみを用意しています(表示のためにノードを探したり
@export で指し示す必要がありません)。

取り出し・返却・事前生成の駆動はエンジン内部で行われるため、GDScript 向けには公開されていません。