What is the best way to do object pooling?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By jarlowrey

This question is the top Google result for “godot pool”, but offers no real details.

I understand GDScript does not garbage collect, and thus there won’t be intermittent pauses due to GC. However, pooling can still be useful if you are creating a lot of objects at once: a large enemy wave, a massive amount of bullets, various visual effects, etc.

Is pooling a good idea in Godot? And if so, how can you ‘reset’ an objects state when selecting it from a “dead” pool? How can you ensure it does not draw processing power when dead?

My gut is to remove them from the scene tree to prevent unneeded processing, and adding them back in should mostly reset state due to calling _ready again. Though that won’t reset any class vars that don’t use onready.

Edit: Trying various ways of making a node remove itself from the scene is giving lackluster results. It feels like Godot is not built with pooling in mind, even though it seems like it would be essential for good performance.

Not a solution but a question really. What if the objects are pooled “off-screen” - is godot smart enough not to waste cycles processing them because they’re not visible?

Just wondering if there’s any performance penalty in removing and then re-inserting the objects into the scene tree.

SleepyTom | 2018-04-04 06:59

The term “engine” has lost its meaning for many developers. They tend to think that an engine was “that UI that you see when you open Unity”, but that’s not the case. An engine is a library that has the purpose of filtering out information that is irrelevant to the result before sending it to the graphics card / sound card / network device / physics solvers. So yes, Godot is an engine, so it is culling objects that aren’t on screen.

That doesn’t mean however that these objects didn’t “waste cycles”, since their location offscreen means that they don’t need to be processed by the graphics card, but they still exist in your game world, so their scripts will still be called. Instead of putting them off screen remove them from the tree. That will keep them in memory but tell the engine to completely ignore them.

Warlaan | 2018-04-04 08:08

I’d call what you’re describing there Warlaan a rendering engine, while a game engine is a rendering engine plus probably a physics engine, a sound engine, etc, all glued together as consistently as possible.

Norgg | 2023-06-05 21:00

I would agree if I had mentioned graphics specifically, but I explicitly said “graphics card / sound card / network device / physics solver” because I didn’t specify if I was talking about a graphics / rendering engine, a sound engine, a network engine or a physics engine.

The “filtering out” is what all of these have in common, it’s just that the criteria are different. A sound might be filtered out because it is too far away, even if it is in view of the camera, while a physics object might be filtered out because it is not moving, regardless of the position.

But yes, a game engine is basically just a combination of the above.

Warlaan | 2023-06-07 10:19

:bust_in_silhouette: Reply From: Zylann

Pooling in gameplay code is a concept, so it depends on what you want to pool, so there is no “best” way in general, but it’s often common sense on a per case basis:

Pooling sprites or meshes requires to just hide them.
Pooling physics objects requires to remove them from all layers they are into.
Pooling sample players requires to stop all their sounds.
Pooling AnimationPlayers requires to stop their animation.
Pooling a scene instance which contains all of the above requires to take care of all of them, unless your gameplay decisions allows you to take some shortcuts.

Pooled nodes can stay in the tree, unless it’s inconvenient for you, in which case you can indeed remove them from the tree (which also takes care of much special cases listed above, though it might be a bit slower).
However, if you do so (and even in other methods), you should have a list per type of object in which you put the “unused” objects, otherwise they will leak.

About removing nodes from tree: in the question you linked, I was removing a node from within a signal handler, which doesn’t seem to be allowed currently. The solution is to use call_deferred to postpone the removal to the end of the frame.

In Godot 3, _ready is not called again when nodes get re-added to the tree: _ready() only called in first add_child() for instanced scene · Issue #17182 · godotengine/godot · GitHub
So you must think about pooling a little bit when you design your scenes.
Don’t do too much fluff for pooling either, because that could defeat the purpose, and there is no efficient way of doing “general” pooling (which would end up being what the Godot internals do in the first place, which you want to avoid with your own pooling!).

Instancing an object from the pool again requires a custom spawning logic. It can range from calling show to having your own on_spawn function in which you do everything needed.
Another catch is that if you want to also preallocate objects to not have a first-creation stutter, you should not assume that _ready or _enter_tree means the object spawns, because depending on your pooling strategy they could get called when you preallocate them. Being “pooled” is like a “dead” state, it has to be taken into account in your logic.

And importantly, profile your game to see if pooling improves things, because there is no point doing that otherwise. Also, remember there are other ways to optimize things, like not using nodes in the first place and use servers directly.

When usingget_parent().remove_child(self) to attempt to pool a colliding bullet, If I use call_deferred I get the error

0:00:10:0997 - Condition ' !body_in && !E ' is true.
----------
Type:Error
Description: 
Time: 0:00:10:0997
C Error: Condition ' !body_in && !E ' is true.
C Source: scene\2d\area_2d.cpp:161
C Function: Area2D::_body_inout

If I do not use call_deferred the error is

0:00:08:0625 - This function can't be used during the in/out signal.
----------
Type:Error
Description: This function can't be used during the in/out signal.
Time: 0:00:08:0625
C Error: Condition ' locked ' is true.
C Source: scene\2d\area_2d.cpp:324
C Function: Area2D::_clear_monitoring

Though they both seem to be working fine. Can I just suppress the error and move on? How risky would it be to do something like that?

jarlowrey | 2018-04-11 23:13

What is your code with call_deferred?

Zylann | 2018-04-11 23:19

	call_deferred("_unparent")
	
func _unparent():
	get_parent().remove_child(self)

this is the only thread I could find about this error, changing the mask did not help, but setting the position did. Really, really weird.

self.global_position = Vector2(-50,-50)
call_deferred("_unparent")
...

jarlowrey | 2018-04-11 23:25

Jeez, that’s pretty unintuitive…
It’s not the first time I have to build convoluted workarounds when dealing with 2D physics though…

Zylann | 2018-04-11 23:56