Manual: Asynchronous Scene Loading and Object Pools (Loading Screen / Game Object Pools)

About this manual

This manual explains how to set up the loading screen and object pools. They exist to reduce the stumbles caused by loading and creating things: “the game freezes for a moment when the scene changes”, “objects trickle in after the screen fades back up”, “firing a burst of bullets stutters right there”.

Both features have the same aim — shifting when the work happens — and the loading screen’s progress includes the object pool’s advance creation. Setting up both together makes the effect easiest to see.

How loading works in ACTION GAME MAKER

Three things are involved, and they need very different amounts of setup.

Feature What it does Setup needed
Asynchronous scene loading Loads scenes in the background without stopping the game None (always on; there is no switch)
Loading screen Shows a screen you made while loading Make a scene and pick it in the settings
Object pools Creates the objects in advance and reuses them instead of throwing them away Place one node

Asynchronous loading is already working without any setup. Because loading runs in the background, the loading screen can keep animating while it loads, and the screen only changes once the destination scene can actually be drawn.

What object pools aim at is cutting the time it takes to instantiate an object. They are not about saving memory or avoiding fragmentation.


Loading screen

Setting it up

  1. Press “+ New” in the map list, choose “Loading Scene” as the kind and give it a name. It is created in res://loadingscenes and opens ready to edit.
  2. Open the “Settings” menu → “Basic Settings”.
  3. Turn on “Enabled” under “Loading Scene”.
  4. Pick the scene you made in step 1 from the “Scene Path” dropdown.

That is all. There is nothing to set for when it is removed, or per transition type.

The dropdown lists the loading scenes in your project (it finds scenes whose root is a LoadingScene automatically). Setting it back to “Not Set” clears it.

Turning “Enabled” back off returns to the old behaviour with no loading screen.

To change where they are stored, use the asset folder fields in the same “Basic Settings” and change the folder for “Loading Scenes”.

Editing it

A loading scene is edited in the same map-making view as maps and menus. Set the kind dropdown in the map list to “Loading Scene” to see them listed.

The root row in the layer dock is “Loading Settings”, and its “+” offers these six. The list is limited to visuals and animation because the game is not running while loading.

Sprite / Color Rect / Animation / Label / Rich Text / Nine Patch Rect

Unlike a map there are no layers. Elements sit directly under the root.

The root node type

Anything works. CanvasLayer, Control, Node2D and so on can be used as they are. The engine handles drawing it in front of everything, so you do not need to adjust layer. Creating one from the map list gives you a LoadingScene.

Only scenes whose root is a GameScene, MenuScene or CoreScene cannot be used. Those are managed by the game and two of them cannot exist at once. Choosing one logs an error and no loading screen is shown.

Use the dedicated LoadingScene when you want to receive the start and end of loading, or to place a character driven by a visual script (see below).

Things that work just by placing them

Place these in the loading scene and the engine takes care of them. No wiring needed.

What you place What happens
Simple Gauge / Image Gauge / ProgressBar The engine updates the value. It uses the node’s min / max as they are, so the default 0–100 works
An animation named hide on an AnimationPlayer Played when the loading screen is removed, and the screen disappears after it finishes (the name loading_out works the same way)
Autoplay on an AnimationPlayer Keeps running for the whole load. It does not stop even while the game is paused

Any node deriving from Range can act as the progress bar. ACTG’s Simple Gauge and Image Gauge are included, so placing one is enough (a gauge with no variable assigned simply shows whatever value is written to it from outside).

Note that only the first one found in tree order is updated. To feed several bars, implement set_progress on the root’s script (the argument runs 0.0–1.0). Implementing set_progress stops the automatic gauge update, so that nothing is written twice.

If you do not place a hide animation, the engine’s own curtain fades it out instead.

Turn looping off on hide. A looping animation never ends, so the completion notice never arrives. The engine detects looping, logs why, and falls back to removing the screen with the curtain.

Only if you want a character driven by a visual script do you need the root to be a LoadingScene (see the next section).

What a LoadingScene root adds

You can use LoadingScene as the loading screen’s root. It is optional and has no settings of its own. It adds these two things.

What you get Details
Load notifications Progress changes, load start and load finish arrive as signals
Visual script characters run Objects with a visual script placed as children behave just like they do in a normal scene

The second one is done by LoadingScene walking its descendants and registering them, so it does not work while the root is a plain CanvasLayer. Use LoadingScene when you want a character walking around during the load.

Everything listed in the previous section (automatic gauge updates, the hide animation, autoplay, the curtain, when it disappears, set_progress) works regardless of the root type. If you only want to format the progress yourself, implementing set_progress is enough and LoadingScene is not needed.

When it disappears

It disappears once all of these are true. There is nothing to configure.

  1. The destination scene has finished loading
  2. The destination scene is placed on screen and running
  3. Two frames have been drawn since it was placed
  4. The object pool has finished creating its instances in advance
  5. The minimum display time (0.3 seconds) has passed

The third one is what prevents “objects appearing after the screen fades back up”. The scene is warmed up behind the loading screen before it is removed, so the game is playable the moment it goes away.

The destination scene does not start running until the removal is decided. This keeps the game from progressing out of sight for as long as the advance creation takes. It starts the moment the removal effect (the curtain closing, or hide) begins, so the first frames run while the screen is hidden and everything is settled by the time it becomes visible.

The BGM set on the game scene also waits for the removal. The music will not start while the screen is still up.

The fifth one prevents the loading screen from flickering when loading finishes instantly. The 0.3 seconds is measured from when the curtain has fully opened, so it never “appears for an instant and vanishes”.

The fade in and out (the curtain)

A loading screen that appears and disappears abruptly looks like a flash, so the engine opens and closes a curtain to make sure the screen is always a solid colour at the moment things switch.

Movement Length Colour
The loading screen appears 0.2 seconds Matches the pre-transition effect’s colour
The loading screen disappears 0.2 seconds Matches the post-transition effect’s colour
The destination scene appears The post-transition effect’s length (at least 0.2 seconds) Matches the post-transition effect’s colour

The last one uses the length of the post-transition effect (the fade up) set in the scene transition editor, as it is. Even with the post-transition effect set to “none”, it still fades for at least 0.2 seconds so that it is not a hard cut.

If the post-transition effect is a slide (SLIDE_UP and so on), the curtain wipes away in the direction you specified. The real slide, where the new scene’s image moves in, cannot be reproduced: the camera lives inside the scene, so moving the scene moves the camera with it and nothing moves on screen. The direction itself is respected.

If you place your own hide animation, everything from the disappearance onwards is left to you (the curtain is not used), so that it does not fade twice.

About how the progress bar moves

The progress bar shows an estimate of how much is left, not an exact percentage.

Range What it covers
0–60% Loading the resources
60–85% From the load finishing to the scene being placed
100% Removal is confirmed

It eases smoothly towards the top of each range and speeds up as the stages advance. It never goes backwards, and it never sits at 100% waiting.

When the object pool creates instances in advance, that progress fills from there up to 99%. This part is the ratio of instances actually created, so it moves exactly as the creation progresses rather than easing over time. The advance creation starts at 30% at the earliest and never before that, so the bar does not appear stuck while loading.

The value cannot be exact because of engine constraints: there is no way to read the progress of a load, and the destination scene is usually already prefetched as a neighbouring scene, in which case the load itself finishes instantly.

What happens at startup

On startup the ACTION GAME MAKER splash is shown for one second, and then the loading screen follows.

Loading runs in the background while the splash is up, so showing the splash does not make startup slower. If the first scene is light and loading finishes while the splash is up, the game starts directly without a loading screen, to avoid a flicker.

The splash reflects the “Application” → “Boot Splash” settings (image / background colour / scaling / filter) as they are. With nothing specified, the built-in ACTION GAME MAKER splash is used.

One exception: if the object pool still has instances to create at startup, showing the loading screen takes priority over avoiding the flicker, because something has to wait for the advance creation to finish.

Not shown for connected slide transitions

Transitions set to a connected slide (SLIDE_UP_CONNECT and so on) in the scene transition editor do not show the loading screen. A connected slide exists to move seamlessly into the next room, and putting a loading screen in the middle would hide both the slide and the player’s movement.

It is skipped if either the pre-transition or the post-transition effect is a connected slide.


Object pools

When they help

Creating a new object takes time. One at a time goes unnoticed, but when several are created at once — firing 20 bullets simultaneously — the frame rate drops right there.

An object pool creates those instances in advance during loading, and when an object disappears it is put to sleep instead of being thrown away so the next creation can reuse it. There is no wait to create, so a burst does not stutter.

Setting it up

  1. In the layer dock of the map-making view, press the “+” on “Map Settings” and choose “Game Object Pools”.
  2. Add an entry to “Pool List”.
  3. Set “Object Path” to the scene of the object you want to pool.
  4. Adjust “Create Count” and the rest as needed.

That is all. The setup is complete with this node alone. There is nothing to set on the object, on the bullet data or in the visual script. Once a scene is registered, every creation path uses the pool.

It is added directly under Map Settings — not inside a layer — because the advance creation is planned when the scene is loaded.

The Game Object Pools settings

Each entry in “Pool List” has these fields.

Field Meaning Default
Pool Name A label to tell entries apart in the list. Not used to look anything up empty
Object Path The scene to create instances from. This scene identifies the pool empty
Is Prewarm Only Only load in advance; keep no pool off
Create Count How many to create in advance 8
Auto Expand Whether to create extra instances when the pool runs out on

With “Auto Expand” off, anything beyond the stock is created normally and that instance is not returned to the pool. It is not an error; it simply behaves as it did before the pool existed.

The advance creation is done a little at a time across frames (there is a per-frame time limit). It never creates everything at once and stalls a frame. The progress is handed to the loading screen, and the loading screen does not disappear until the advance creation is done.

Where to place the node

The sleeping stock is held by this node, so destroying the node loses the stock too. Where you place it changes the behaviour.

Where Behaviour
Somewhere not destroyed by scene transitions The stock survives across scenes. Objects on loan are returned during the transition, so it is ready to use afterwards
Inside a game scene The node is destroyed with the scene and the stock is lost. The node in the destination scene creates its own instances again

To reuse the stock across scenes, make it persistent. The easy way is autoload: make a scene containing just one Game Object Pools node and register it under Project Settings → “Globals” → “Autoload”. A node registered there is created once when the game starts and is not destroyed by scene transitions.

You can also copy one over from another map. It is pasted directly under Map Settings.

Do not place it under a game object that is created at runtime. The advance creation must be planned by the time the scene is loaded, so place it in the scene as a node of its own.

Is Prewarm Only

With “Is Prewarm Only” on, only the loading is done in advance and no stock is kept.

  • One instance is created once the source scene has loaded, and discarded immediately. The scene and its assets stay loaded, so there is less of a hitch when it is actually created
  • Objects for that scene are created normally (not handed out from a pool, and simply deleted when they disappear)
  • The root node type does not matter. Scenes that are not game objects, and UI scenes, can be registered
  • “Create Count” and “Auto Expand” are unused, so they cannot be edited

It suits objects you do not want to reuse but whose first appearance you want to smooth out.

This setting is what you want for effects and particles. EffectObject and ParticleObject cannot be reused (see below), but “Is Prewarm Only” does not care about the root type, so they can be registered. Loading them ahead of the first time they are shown makes that first hitch much smaller. Registering the effects you plan to use is worth doing.

Which objects can be pooled

Only game objects and lightweight objects can be reused, because the return happens from those two kinds’ disappearance handling.

Objects driven by physics (PhysicsGameObject), effects (EffectObject), particles (ParticleObject) and other nodes are not supported: they have no path that returns them to the pool when they disappear. Any .tscn can be set as “Object Path”, but a scene whose root is not a supported kind gets no pool (it is reported in the log at runtime).

“Is Prewarm Only” is free of this restriction. Register effects and particles there to get their first load out of the way.

Which creation paths use it

Once a scene is registered, all of these go through the pool automatically. There is nothing to set per path.

Path Example
Firing bullets The bullet-firing action
Creating objects The “Generate Object” action
Connected objects Objects created as children
Changing objects The “Change Object” action
Revival Reviving a vanished object, coming back from off-camera, restoring after a scene change

When objects are returned

When an object disappears, it goes back to the pool instead of being deleted. It is returned by the disappear action, by changing the object, and by scene transitions.

A returned instance is restored to the state saved in the scene at the moment it is returned: properties changed at runtime go back to their saved values, nodes added at runtime are deleted, and the visual script goes back to starting from its initial action. The cleanup happens on the way back, not on the way out, so bursts of creation do not get more expensive.

Objects placed in the scene from the start are not returned (they were not handed out by a pool, so they are simply deleted).

Editor warnings and logs

Mistakes in the settings show up as warnings in the scene tree.

  • “Object Path” is not set
  • “Object Path” is duplicated (registering the same scene twice means the later one is never used)

At runtime, failures of the advance creation are logged (path not set / path cannot be resolved / duplicated / load failed / root is not a supported kind).

The absence of a pool is not logged. An object that is not registered is simply created normally, which is not a mistake.


Worked examples

Firing 20 bullets at once stutters

  1. Place a Game Object Pools node and set “Object Path” to the bullet’s scene.
  2. Set “Create Count” a little above the most that will be on screen at once (24 for 20 bullets, say).
  3. Run it without changing anything on the bullet data.

To check whether the stock is enough, turn “Auto Expand” off and try it. If it is short, only the later bullets stutter, so raise “Create Count” by that much. Turning it back on afterwards means bullets will not stop appearing in situations you did not anticipate.

Showing “Now Loading”

  1. Use “+ New” in the map list → kind “Loading Scene”.

At that point you already have a black background, a gauge, a “NOW LOADING …” label and an animation that cycles the dots. All that is left is to enable it under “Settings” → “Basic Settings” and pick the scene from the dropdown.

To change the look, add elements from the “+” in the layer dock and adjust the label text and the gauge colours. To make your own removal effect, add an animation named hide to the AnimationPlayer (with looping off).

Loading just the boss scene ahead of time

For something you do not want to reuse but whose appearance you want to smooth out.

  1. Add an entry to “Pool List” on a Game Object Pools node.
  2. Set “Object Path” to the boss’s scene.
  3. Turn on “Is Prewarm Only”.

The boss is created normally, but the scene and its assets are already loaded, so it appears without a hitch.

Reusing the stock across stages

  1. Make a scene containing just one Game Object Pools node and save it under res://.
  2. Register that scene under Project Settings → “Globals” → “Autoload”.
  3. Put the pool entries on the node in that scene.

The advance creation runs once when the game starts and is not repeated by later scene transitions. The only difference from placing one per stage is whether the stock survives; the settings are the same.


Things to watch out for

  • There is no switch for asynchronous loading. Scenes always load in the background. Only the loading screen and the object pools need setting up.
  • The loading scene settings live under “Settings” → “Basic Settings”. To reach them from Project Settings directly they are on the advanced side (application/loading_scene/*), so turn the toggle on or type loading into the search box.
  • A loading screen’s root cannot be a GameScene, MenuScene or CoreScene. Choosing one logs an error and nothing is shown.
  • To place a visual script character in the loading screen, make the root a LoadingScene. Other types are not registered, so the character will not move. Nothing is logged, which makes it easy to miss.
  • Turn looping off on the hide animation. A looping animation never reports completion. The engine detects it, logs why, and falls back to removing the screen with the curtain.
  • If the removal never becomes possible, the screen is removed by force after a while. What it was waiting for is written to the log. A very large advance-creation count can also be cut short here, but the rest is created on demand so nothing breaks.
  • Only one progress bar is updated automatically. To drive several, implement set_progress on the root’s script; doing so stops the automatic update.
  • Connected slide transitions do not show the loading screen. It is skipped if either the pre- or the post-transition effect is a connected slide (by design).
  • Destroying the Game Object Pools node loses the stock. To reuse it across scenes, make it persistent with autoload or similar.
  • Do not register the same scene in more than one pool. The source scene identifies the pool, so the later one is never used. The scene tree warning points it out.
  • Only game objects and lightweight objects can be pooled. Anything else set as “Object Path” gets no pool (“Is Prewarm Only” does not care about the type).
  • Running out of stock is quiet. With “Auto Expand” off it just falls back to normal creation, with nothing logged. To check whether the stock is enough, turn it off deliberately and try.
  • Reuse only means “an instance that came back is not rebuilt”. The object’s state is restored to the scene’s saved values on return, so it never carries on from last time — and equally, you cannot keep nodes added at runtime for the next use.
  • Do not place a Game Object Pools node under an object created at runtime. The advance creation must be planned by the time the scene is loaded.
  • What object pools aim at is cutting creation time. Memory use grows by however much you create in advance.
  • The advance creation count translates directly into loading time. How long one instance takes depends on the object’s size, but a bullet with a visual script takes a few milliseconds. Several hundred adds seconds, so decide based on how many are really on screen at once.
1 Like

Setting up GameObjectPools from GDScript

Object pools (GameObjectPools) are normally used by placing a node in a scene, but
they can also be assembled from GDScript at runtime.

Using a pool (taking objects out and returning them) is handled by the engine, so there is no API to call from a script.
Every creation path — bullets, the “Generate Object” action, connected objects — goes through the pool transparently.
What you do in GDScript is configure pools and check their state, and nothing else.


1. Parameters (the same as GameObjectPoolData)

Property Type Default What it is
pool_name String "" The name shown in the list. Not used to look anything up (it works without one)
object_path String "" The scene file path of the source object. Either res://...tscn or uid://.... Only scenes whose root is a GameObject or Area2DGameObject (no restriction with is_prewarm_only — see section 5)
is_prewarm_only bool false Only load in advance and keep no pool (see below)
create_count int 8 How many to create in advance. 0 is allowed (only the return machinery is prepared). Ignored when is_prewarm_only is on
auto_expand bool true Whether to create extra instances when the dormant objects run out

is_prewarm_only (load in advance only)

Turn it on and only the loading is done in advance; no pool is kept.

  • After the source scene has loaded, exactly one instance is created and discarded immediately. The scene and the resources it depends on stay loaded, so there is less of a hitch when something is actually created
  • Objects for that scene are created normally (they are not handed out from a pool, and they are simply freed when they disappear)
  • Because nothing is reused, the root node’s type does not matter
  • The pooled-class restriction does not apply either. Any scene can be specified, whatever its root node type
  • create_count is ignored, and becomes uneditable in the Inspector

A pool is identified by the scene in object_path. pool_name is a display name for telling entries apart.


2. The minimal sample

extends Node

func _ready() -> void:
	var data := GameObjectPoolData.new()
	data.pool_name = "Enemy bullets"
	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()
	# Configure it before it enters the tree (creation in advance starts the moment it does)
	pools.set_pool_data_list(data_list)

	add_child(pools)

3. Several pools + waiting for completion + checking stock

extends Node

# Pool settings (the same parameters as GameObjectPoolData)
const POOL_SETTINGS := [
	{
		"pool_name": "Enemy bullets",
		"object_path": "res://objects/enemy_bullet.tscn",
		"create_count": 64,
		"auto_expand": true,
	},
	{
		"pool_name": "Explosion effect",
		"object_path": "res://objects/explosion.tscn",
		"create_count": 8,
		"auto_expand": false,
	},
]

var _pools: GameObjectPools = null


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


## Assemble a GameObjectPools from a settings list and add it to the tree
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"

	# Creation in advance starts the moment it enters the tree, so configure it before add_child
	pools.set_pool_data_list(data_list)
	pools.prewarm_finished.connect(_on_prewarm_finished)

	add_child(pools)

	return pools


## Notification that creation in advance finished (called once, when everything this node handles is ready)
func _on_prewarm_finished() -> void:
	for setting in POOL_SETTINGS:
		var object_path: String = setting["object_path"]
		print("%s : %d dormant" % [object_path, _pools.get_dormant_count(object_path)])

4. Configuring it from the Inspector

Using @export_custom lets you edit the pool settings in the Inspector. That saves writing the values into the script,
which is easier when you want to tune the counts from the project side.

extends Node

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

var _pools: GameObjectPools = null


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


## Assemble a GameObjectPools from a settings list and add it to the tree
func _create_pools(p_settings: Array) -> GameObjectPools:
	var pools := GameObjectPools.new()
	pools.name = "GameObjectPools"

	# Creation in advance starts the moment it enters the tree, so configure it before add_child
	pools.set_pool_data_list(p_settings)
	pools.prewarm_finished.connect(_on_prewarm_finished)

	add_child(pools)

	return pools


## Notification that creation in advance finished (called once, when everything this node handles is ready)
func _on_prewarm_finished() -> void:
	for setting in list:
		var object_path: String = setting.object_path
		print("%s : %d dormant" % [object_path, _pools.get_dormant_count(object_path)])

Attach this script to a resident node (one not destroyed by scene transitions) and the array appears in the Inspector,
where you can add and edit GameObjectPoolData entries. object_path can be set with a file picker.

Notes

  • Passing the element’s class name as the second argument (hint_string) of @export_custom tells the Inspector the element type
  • What you get is an untyped Array, but as long as the elements are GameObjectPoolData, set_pool_data_list() takes it as-is
  • The contents of the array are saved into the scene (.tscn) the script is attached to. After changing the settings you need to save the scene
  • If you attach the same script to several nodes, be careful not to register the same scene in more than one pool (only the one registered first is used)

5. Things to watch out for

Configure it “before it enters the tree”

Creation in advance starts the moment the node enters the tree. Calling set_pool_data_list() after
add_child() has no effect.

Pass a typed array

set_pool_data_list() takes an Array[GameObjectPoolData]. When assembling it in a script, add the type
(GDScript’s type checking can otherwise reject it).

var data_list: Array[GameObjectPoolData] = []   # ← add the type

The @export_custom in section 4 gives you an untyped Array, but as long as the elements are
GameObjectPoolData it can be passed as-is.

Only GameObject / Area2DGameObject can be pooled

What can be pooled is GameObject and Area2DGameObject. They are the only two classes with a
return path, and the return is performed from both classes’ destruction paths.

PhysicsGameObject and ordinary Node2D descendants are not supported
(specifying one in object_path creates no pool).

Enabling is_prewarm_only removes this restriction. Because it is not used as a pool,
any scene can be specified regardless of its root node type (UI scenes included). Use it when you only
want to get the loading out of the way in advance.

The target object needs to be set up

Whether a pool is used is decided purely by registering it in the list. The root of the target scene must be
one of the supported classes. Otherwise no pool is created and you get this error:

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

Where to place the node

  • Dormant objects are owned by the GameObjectPools node. Destroying the node destroys the dormant stock with it
  • To reuse it across scenes, place it somewhere scene transitions do not destroy (under a resident node). Objects on loan are returned to the pool during the transition, so it is ready to use afterwards
  • Placed inside a game scene, the node is destroyed with the scene and the stock is lost (the node in the destination scene creates its own instances again)
  • Do not attach it under a game object created at runtime (it would be dragged along by that object’s lifetime)

Making it resident (Autoload)

Register it under Project Settings → Globals → Autoload → Select Script/Scene. There are two things you can specify.

What you register What it is for
A scene (.tscn) with a GameObjectPools node in it No GDScript needed. Make a scene with one node in it and lay out the pool settings in the Inspector
A node with a GDScript that assembles GameObjectPools attached (the script from section 4, for example) When you want to decide the settings at runtime, or act on creation in advance finishing

A node registered as an Autoload is created once at the start of the game and is not destroyed by scene transitions.
That means it works as a resident GameObjectPools as-is. The stock survives across scenes, and objects on loan
are returned during a transition.

With GDScript, specifying a bare script (.gd) creates a Node automatically and attaches it.
To edit the pool settings from the Inspector, make a scene (.tscn) with that script attached and
register that instead.

Creation in advance spans frames

So that the loading screen keeps running, creation in advance proceeds a little at a time within a per-frame time budget.
The stock is still 0 immediately after add_child(). To check the numbers, wait for prewarm_finished.

Objects can still be taken out while creation in advance is under way (the rest is created on the spot and handed over).

Registering the same scene on several nodes

The GameObjectPools registered first takes effect, and the pool registered later is not used
(whatever it created in advance is simply wasted). Development builds log this:

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

auto_expand = false is not a cap on the total number of objects

Setting it to false stops the pool growing, but when the stock runs out it falls back to normal creation
(that is, objects still get created). Anything created by the fallback is outside pool management and
is simply freed when it disappears.


6. Showing pool state on a Label

GameObjectPools.get_all_pool_status_list() is a static method, so no node reference is needed.
Every pool on every GameObjectPools in the tree comes back in a single call.

extends Label

## Update interval (seconds)
@export var update_interval: float = 0.2

var _elapsed: float = 0.0


func _process(delta: float) -> void:
	# There is no notification for taking out or returning, so poll for updates
	_elapsed += delta
	if _elapsed < update_interval:
		return
	_elapsed = 0.0
	_update_text()


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

	# Progress of creation in advance (x: created / y: planned)
	var prewarm: Vector2i = GameObjectPools.get_all_prewarm_count()
	lines.append("Prewarm %d/%d %s" % [
		prewarm.x, prewarm.y,
		"done" if GameObjectPools.is_all_prewarm_finished() else "creating",
	])

	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  in use %d / dormant %d / planned %d (%d left)%s%s" % [
			pool_name,
			status["lending"],
			status["dormant"],
			status["create_count"],
			status["pending_create"],
			"" if status["auto_expand"] else " [no auto expand]",
			"" if status["is_loaded"] else " [loading]",
		])

	text = "\n".join(lines)

The keys of the returned Dictionary are as follows.

Key Type What it is
owner_path String The path of the GameObjectPools holding that pool (for telling them apart when every node’s pools come back together)
pool_name String The display name you set
scene_path String The path of the source scene (the pool’s identifier)
dormant int How many are dormant (in stock)
lending int How many are on loan (in use). Includes any waiting to be returned
create_count int How many are planned for creation in advance
pending_create int How many of those are left
auto_expand bool Whether extra instances are created when it runs out
is_loaded bool Whether the source scene has finished loading and is usable

Only the pools actually held come back, not the settings list. Pools that were not created because of a
load failure or an unsupported class are not included (even though they remain in the settings).

The scope is the GameObjectPools in the tree. If several nodes register the same scene, that many entries
appear (only the one registered first is used to hand objects out; owner_path tells them apart).

There is no notification signal for taking out or returning, so update by polling. Even for a pool with
heavy turnover like bullets, an update interval of 0.1–0.5 seconds keeps the display cost negligible.


7. The API available from GDScript

API What it does
GameObjectPoolData.new() Creates a settings object. Its properties are as in section 1
GameObjectPools.new() Creates a pool node
set_pool_data_list(list) / get_pool_data_list() Sets and gets the pool settings list
get_dormant_count(packed_scene_path) The number of dormant objects for the given scene
prewarm_finished (signal) Notification that the creation in advance this node handles has finished (once only)
GameObjectPools.get_all_pool_status_list() Static. The state of every pool in the tree (no node reference needed; section 6)
GameObjectPools.get_all_prewarm_count() Static. The progress of creation in advance (x: created / y: planned)
GameObjectPools.is_all_prewarm_finished() Static. Whether creation in advance has finished

For reading state, only static methods that need no node reference are provided (so you never have to
hunt for a node or point at one with @export just to display something).

Taking out, returning and driving creation in advance all happen inside the engine, and are not exposed to GDScript.