Interesting collision behavior when removing and adding nodes to the scene tree

Godot Version

4.3

Question

I observed an interesting (weird) behavior regarding collisions when adding/removing nodes in the scene tree.

I created a scene that just contains an Area2D and a CharacterBody2D. They are stacked on top of each other (collision triggers on game start but this is not necessary to reproduce this, they just need to be on top of each other when removing the node from the scene tree, I explain this more later).

Then I made a function that just moved the CharacterBody2D by 50 pixels to the right (putting it outside of the Area2D collider). It triggers if I press space. I started the project, collision triggers, I press “space”, the body moves out, I can even move it around with WASD and collision does no re-trigger as one would expect (unless I go back into the collision zone).

Then I put this scene as a child of a Node2D and made it so the Node2D calls remove_child on it (but still preserves it in a variable). So the CharacterBody is moved 50px and the scene gets removed from the scene tree. I made it so another press of space just adds the node back to the tree with add_child and observed the collision triggering again even though the CharacterBody is outside the collision area.

I assume Godot requires a tick to actually move the CharacterBody and that does not happen when detached. Then when it inserts the subtree back into the scene tree the collision is triggered before the move is registered which kinda makes sense but is till a bit weird to me. I would expect collision to not re-trigger.

My question here is: Is there a way to prevent this behavior? Is it even expected or should I report it?

If anyone wants it I can post a minimal reproduction project. This is how the scene tree looks, Sprite2Ds are just added to have a visual representation. ColliderTestScene is the one that gets removed and added.

ColliderTest
    ┖╴ColliderTestScene
       ┠╴Area2D
       ┃  ┠╴Sprite2D
       ┃  ┖╴CollisionShape2D
       ┖╴CharacterBody2D
          ┠╴Sprite2D
          ┠╴CollisionShape2D
          ┖╴Camera2D

I’m encountering something similar to the second issue you mention. I’ve included a minimal script that triggers the unexpected behavior for anyone who would like to try this out.

The basic issue is that I have a CharacterBody2D and an Area2D monitoring for it. When the character body enters the area, I want to remove the character body from the scene and then, some time later, respawn it at another location outside of the Area2D.

I’ve registered a callback for the area’s on_body_entered signal, from which I do a call_deferred on remove_child to remove the character body. For the purpose of the test script below, I’m just listening for a keypress (the R key), which makes a deferred call to re-add the character body.

The behavior I’m seeing is that after the scene starts, I move the character body into the area, and the body leaves the scene as expected. When I hit R the first time, the body briefly flashes at the spawn point but leaves the scene again immediately because the area has emitted its on_body_entered signal again, even though the body is no longer in the area. If i hit R a second time, the body respawns again and there is no duplicate signal. I can move it into the area again and see the same behavior repeatedly.

This looks like a bug to me, and browsing around the bug database suggests it’s been there for a while. In playing around with this, it seems to be something specific to character bodies. When I created a similar test with a RigidBody2D moving into the monitoring area under constant velocity, I did not observe the unexpected on_body_entered signals.

If anyone from the project thinks this should be filed, I’ve got a very small MRP you can use to repro the issue.

My script (just requires a scene containing the character body, area2d, and the marker for the respawn location):

extends Node2D

const WALK_SPEED = 200.0

@onready var _obj : CharacterBody2D = $CharacterBody2D
@onready var _loc : Marker2D = $Marker2D

func _physics_process(delta: float) -> void:
	if _obj.is_inside_tree():
		_obj.velocity =  Input.get_vector(&"move_left", &"move_right", &"move_up", &"move_down") * WALK_SPEED
		if _obj.velocity.length() > 0:
			_obj.move_and_slide()


func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventKey and event.pressed:
		if event.keycode == KEY_R and not _obj.is_inside_tree():
			_obj.position = _loc.position
			add_child.call_deferred(_obj)


func _on_area_2d_body_entered(body: Node2D) -> void:
	body.position = _loc.position
	remove_child.call_deferred(body)

You can maybe add your project and comments to this issue Colliders not overlapping but colliding · Issue #102971 · godotengine/godot · GitHub

Done! Fingers crossed for both of us. FWIW, I’m working around this temporarily by turning off monitoring on the area while I the character body respawns.