Working on a bullet hell-adjacent game and at around 150+ projectiles onscreen, the instantiate/free overhead started showing up in the profiler even in Godot 4. Wrote a pool class to deal with it, but my approach to tracking which instances are "in use" feels slightly off and I want a gut check.
Instead of a separate is_active boolean or a custom interface, I'm using process_mode as the free indicator:
class_name ObjectPool
extends RefCounted
var _pool: Array[Node] = []
var _scene: PackedScene
var _parent: Node
func _init(scene: PackedScene, parent: Node, prewarm: int = 10) -> void:
_scene = scene
_parent = parent
for i in prewarm:
var obj := _scene.instantiate()
obj.process_mode = Node.PROCESS_MODE_DISABLED
obj.visible = false
_parent.add_child(obj)
_pool.append(obj)
func acquire() -> Node:
for obj in _pool:
if obj.process_mode == Node.PROCESS_MODE_DISABLED:
obj.process_mode = Node.PROCESS_MODE_INHERIT
obj.visible = true
return obj
# pool exhausted, grow
var obj := _scene.instantiate()
_parent.add_child(obj)
_pool.append(obj)
return obj
func release(obj: Node) -> void:
obj.process_mode = Node.PROCESS_MODE_DISABLED
obj.visible = false
The reason I avoided visible alone is that some pooled objects might be legitimately invisible while active: trigger zones, audio emitters, that kind of thing. PROCESS_MODE_DISABLED also has the nice side effect of stopping _process and _physics_process on inactive instances automatically, so they don't eat update budget while sitting in the pool.
The obvious weakness is the linear scan through _pool to find a free slot. For pools under ~50 objects it's negligible, but I haven't benchmarked it at scale yet. The clean fix is a separate free-list queue so acquire is O(1), but I'm trying to figure out if the scan is even a real problem before adding complexity.
The other thing I'm not sure about: the add_child call on the grow path. If the pool exhausts mid-frame and has to grow, that's a scene tree modification during gameplay. Haven't seen it spike yet but it's the kind of thing that would be annoying to debug later.
Anyone doing pooling in Godot 4? What's your approach? Specifically curious if the process_mode trick is going to bite me somewhere I'm not thinking of, and whether a free-list queue is worth it at the scale I'm describing.