Issue with enemy object pool and _on_body_entered()

Godot Version

v4.4.1

Question

I have re-spawning enemies in an object pool. When an enemy dies (and is removed from scene, returning to the object pool), it drops a coin at the position of its death. The coin(s) use on_body_entered to detect when they come in contact with the player or another enemy (as enemies can pick up coins as well).

Once the enemy-that-died re-spawns and is placed back into the scene at its “start” location (which is far away from the location of its death/dropped coin), I’m seeing _on_body_entered() in the coin being instantly triggered at the enemy’s return to the scene, which then triggers the re-spawned enemy to pick up the coin right away despite being nowhere near it.

I’ve 100% confirmed that it is indeed the coin’s former owner picking it up automatically via the debugger, and I’m using self.global_location.distance_to(body.global_distance) in the coin’s _on_body_entered() to confirm that the physical distance between the coin and enemy is very large – so its not a matter of the enemy being briefly placed at its location-of-death upon entering the scene before relocating to the enemy start position on the other side of the map.

Turning on “visible collision shapes” does not visibly show any collision shapes touching/overlapping the coins Area2D when _on_body_entered() is being fired.

Any tips/workarounds on how I can avoid this type of behavior?

When “respawning” the enemy, do you activate it first or do you set its new position first?

1 Like

I’m setting global position first and then adding to scene. I’ve confirmed that monster.global_position is correct (i.e., at it’s starting position, and not it’s death position) before add_child() is run to return it to the scene.

Here is how the monster is being removed from the pool and added to scene:

image

and here is the monster’s .activate() code:

Removing the pooled-object from the scene-tree somewhat defeats the purpose of your object-pool, since adding and removing objects from the scene-tree is an expensive task, while creating objects isnt.

can you show the code where you drop coins?

2 Likes

Ah, I was using the design i found here https://www.reddit.com/r/godot/comments/1f5dtai/question_about_pooling/:

 Sorry, that was supposed to be a reply to somdat! Not very helpful as a reply to your question! So let me try and help now...

Short answer: no, I don't keep them on the tree. Here's my process in a nutshell:

Startup (I use a singleton but that's not required):

    Load a scene from a resource

    Instantiate loads of it

    Put each one in an array

Running:

    Pop objects from the array when you need them, e.g. enemy is spawned. array.shuffle() is good for randomisation

    Use something_in_tree.add_child(object_from_array) to put them in the tree

    The object will do whatever it does

    When you no longer need the object, e.g. enemy is killed, use something_in_tree.remove_child(object_from_array) to orphan the object

    Add the object to the array again

That works for me, I hope there's something in there you can use. Good luck!

can you help me with a better approach to activating/deactivating pooled objects?

For coin dropping, I have a signal being emitted by the monster on “death” that is heard by the object manager (coins are also pooled) with the following:

func _on_create_coin(pos: Vector2, worth: int) -> void:
	if _coin_pool.size() == 0 or worth < 1:
		return
		
	var c: Coin = _coin_pool[0]
	_coin_pool.remove_at(0)
	c.setup(worth)
	call_deferred("add_object", c, pos)
func add_object(obj: Node, my_global_pos: Vector2) -> void:
	add_child(obj)
	obj.global_position = my_global_pos

Coin.setup():

func setup(my_worth: int) -> void:
	self.worth = my_worth
	$Sprite2D.visible = true
	$CurrentWorthLabel.visible = true
	$CurrentWorthLabel.text = "%d" % self.worth
	self.active = true
	set_deferred("monitoring", true)

I should also note that there is at least 5 seconds between when a monster dies (and drops a coin) and when it is respawned, so the coin is sitting out in the world untouched for a good amount of time before the monster respawns and triggers the coin’s on_body_entered from across the map somehow.

I once wrote a scene-pooler. (it automatically creates temporary objects if the pool limit is exceeded)

ScenePooler
extends Node
# Use as Global-Autoload
## Enums
enum {
	BULLET,
	EXPLOSION,
	HORNET
}
## Constants

## Exports

## Public Variables
var active_scene: Node
var pool: Dictionary = {}

## Private Variables

## Onready Variables

## Built-In Override Methods

## Public Methods
func spawn_scene(pool_name: int, scene_pool_data: RefCounted) -> void:
	if pool.has(pool_name):
		pool[pool_name].get_next_scene().spawn(scene_pool_data)
	else:
		push_warning("ScenePooler: Trying to spawn a scene that isn't pooled")
	
func pool_scene(pool_name: int, packed_scene: PackedScene, amount: int) -> void:
	if pool.has(pool_name):
		clear_scene_pool(pool_name)
	pool[pool_name] = ScenePool.new(packed_scene, amount)

func clear() -> void:
	for pool_name: int in pool:
		pool[pool_name].clear_pool()
	pool.clear()

func clear_scene_pool(pool_name: int) -> void:
	if pool.has(pool_name):
		pool[pool_name].clear_pool()
		pool.erase(pool_name)


class ScenePool extends RefCounted:
	var packed_scene: PackedScene
	var scene_pool: Array[Node] = []
	
	signal clear
	
	func _init(_packed_scene: PackedScene, amount: int) -> void:
		packed_scene = _packed_scene
		_pool_scenes(max(amount, 1))

	func clear_pool() -> void:
		clear.emit()

	func get_next_scene() -> Node:
		var scene: Node
		if not scene_pool.is_empty():
			scene = scene_pool.pop_back()
			
			## Activate scene process
			scene.process_mode = Node.PROCESS_MODE_INHERIT
			scene.show()
		else:
			print_rich("[color=LightBlue]ScenePooler: Scene Pool is empty. Creating new Instance")
			
			## Instantiate new scene
			scene = packed_scene.instantiate()
			scene.finished(scene.queue_free)
			ScenePooler.active_scene.call_deferred("add_child", scene)
		
		return scene

	func _pool_scenes(amount: int) -> void:
		for i: int in amount:
			var scene: Node = packed_scene.instantiate()
			
			ScenePooler.active_scene.add_child(scene)
			clear.connect(scene.queue_free)
			
			scene.finished.connect(_on_scene_finished.bind(scene))
			scene.hide()
			scene.process_mode = Node.PROCESS_MODE_DISABLED
			scene_pool.append(scene)
	
	func _on_scene_finished(scene: Node) -> void:
		scene.hide()
		scene.set_deferred("process_mode", Node.PROCESS_MODE_DISABLED)
		
		scene_pool.append(scene)

For the coin problem: can you connect the body_exited-signal and check how often it gets called when this bug appears?

1 Like

added a bunch of print statements:

Monster_1 has died at position (1374.414, 1373.522)
signal 'on_create_coin()' heard by ObjectManager
signal 'on_create_coin()' emitted for position (1374.414, 1373.522)
coin being added at position (1374.414, 1373.522)
activate() started for Monster_1. current pos: (1375.168, 1373.23)
global position was reset. new position: (648.0, 1081.0)
activate() has finished
preparing to add_child() for 'Monster_1'. Monster current global_pos: (648.0, 1081.0)
_activate() has finished
on_body_entered() triggered by 'Monster_1' for coin:
...coin position is at (1374.414, 1373.522)
...triggering body is at (648.7548, 1080.705)
distance from body to coin = 782
on_body_EXITED() triggered by 'Monster_1' for coin:
...coin position is at (1374.414, 1373.522)
...triggering body is at (649.5107, 1080.406)

Here’s something else: I added a call to change CollisionShape2D.disabled to true when the monster dies. I wait for the monster to respawn and visually appear in the scene. I press pause in the debugger and then manually uncheck the monster’s CollisionShape2D.disabled value so that it is no longer disabled. The coin stays in position and is not picked up by the monster.

However, if i introduce CollisionShape2D.disabled = false into the monster.activate() function, it is again picking up the coin when it respawns

some additional things i just tried:

  • Replaced add_child and remove_child to bring monster in and out of scene with show()/hide() and changing process_mode (similar to your scene pooler) – same result
  • before the monster dies and emits the signal to generate a coin, reset the monster’s global position to 0,0 – same result

I only read through quickly, but it sounded a bit like perhaps the coins are children to the monster node?
If so and in combination with some other line of code here or there, perhaps some unexpected behavior causes the coins to respawn at the same spot as the monster and then get picked up - or some other weird thing.
I would keep the coin in a separate container node separate from all other monster stuff and spawn it in with a signal, to try to sever whatever strange tie there is that causes the issue.

1 Like

Thanks for the help! Unfortunately, monsters and coins are already pretty divorced. The monster emits a “drop a coin here” signal, which is heard by an independent ObjectManager scene that will have the coin as its child.

Not going to have time testing it today, but I’m wondering if it has to do with the monster being a CharacterBody2D and I’m manually changing its global position outside of physics_process (according to docs, “you should not set the CharacterBody2D position directly”).

Maybe disabling physics_process when the monster dies, and then modifying global_position before physics_process is reenabled (ie, after the monster is respawned) will help?

Yes i also think the problem is most likely you changing something during process-frame, while the physics-frame hasnt updated that yet

1 Like

Yeah, this makes sense to me. Unfortunately all my messing around with set_physics_process(false) and changing process_mode today has been fruitless.

At this point, I think I’m going to keep using pooling for simple non-physics stuff like projectiles, coins, etc, but instantiate/queue_free CharacterBody2D scenes like Enemy without doing any pooling.

Thanks for the help!

set_physics_process doesnt disable the physics-collision i believe. It only disables the physics-process-method. Try to set the collision-shape to disabled

1 Like

If I disable the collision shape during die(), and then wait for the respawn to visually appear before manually pausing the debugger and re-enabling the collision shape, then everything works as desired (ie the coin isn’t picked up).

However, if I try to re-enable the collision shape anywhere in the code, it goes back to undesired behavior.

Is there any way to “await” for .show() to be completely finished before I allow the collision shape to be re-enabled?

Ok, this is weird.

I’m now setting CollisionShape2D.disabled = true during monster.die().

Trying to have CollisionShape2D.disabled = false set during monster.activate() wasn’t helping.

So, instead, I put CollisionShape2D.disabled = false into _physics_process() and had physics process re-enable collision like this:

func _physics_process(delta: float) -> void:
	if not self.targetable:
		return
	
	if $CollisionCircle.disabled:
			$CollisionCircle.disabled = false
       
        # other stuff

And it still wasn’t working. But then I added this:

func _physics_process(delta: float) -> void:
	if not self.targetable:
		return
	
	if $CollisionCircle.disabled:
		if self.frame_count == 0:
			self.frame_count += 1
			return
		else:
			$CollisionCircle.disabled = false
			self.frame_count = 0
        
    # other stuff

So it’s essentially not turning back on collision until the second physics frame after the monster is re-spawned – basically just skipping the first physics frame after respawn completely.

And now it’s working.

I would prefer a more elegant solution, but I could live with this. Could you think of anything in particular to look that would cause this issue to appear in the first physics frame after respawn, but not the second?

I have no idea. May i ask how many spawners do you use? Because object pooling isnt really necessary in godot and you might just get away with just creating new monsters. This would also get rid of this problem

2 Likes

Just the one spawner for the monsters.
Right now I’m not so much making a game as I am with both learning godot and working to understand common game design patterns — and for this mini-project, its object pooling.

So I think my take away from this is object pooling works fine in godot for simple non-physics stuff (eg projectiles, coins), but is more trouble than it’s worth for CharacterBody2D (and likely other physics based nodes)