Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Area2D body_entered signal is generated incorrectly if a body is moved prior to scene initialization #61584

Open
superlazyname opened this issue Jun 1, 2022 · 8 comments

Comments

@superlazyname
Copy link

superlazyname commented Jun 1, 2022

Godot version

3.4.4 and 3.3.4

System information

Ubuntu 20.04 x86_64

Issue description

What doesn't work:

If a body is moved before a scene is loaded, body_entered will be generated even though the body is nowhere near the Area2D.

How I expected it to work:

The scene would check where the body is when it loaded, and only emit the signal if the body was actually intersecting the area.

What my tree looks like

Main
    KinematicPlayer
    Room2 (dynamically added)
        Room2_Area2D
        ...
    ...

Controls:

Note: When I refer to 1,2,3, I mean the number keys 1, 2, and 3 on your keyboard. Not Keypad 1,2,3.

  • 1 : Reload scene (don't move)
  • 2 : Move player (toggle between position A and B)
  • 3 : Move player, then reload scene

Context / Why this is important:

  • I use something like this to handle moving from one "room" to another, I found that sometimes when moving to a room it would mysteriously trigger the Area2D to go to a different room immediately upon entry.
  • I move the player before loading the scene because a room might have more than one entrance, e.g., if they entered a room from a door to the left I'll want to put them in front of that door.
  • I ran into this trying to follow the advice of the docs: https://docs.godotengine.org/en/stable/tutorials/best_practices/scene_organization.html#choosing-a-node-tree-structure , it felt like a good code decision to leave the player node outside of the scene instead of recreating it / deleting it every time a room changed.

References

This is possibly related to #14578 but that issue talks about changing node parents, there is no node reparenting happening here.

  • The player node does not change its position, it remains as a child of the Main scene node
  • The only node that changes is the "Room" node getting deleted and re-added.
  • I commented on that issue but after playing with this more I'm not really sure it's the same issue.

Workarounds

This is not a great workaround but dropping the signal system for body_entered and using _process(delta) with Area2D.get_overlapping_bodies() does work as expected, and does not find "ghost bodies".

At the moment I'm early enough in my project that I can change to some other system of going between rooms.

What it isn't

  • The Area2D isn't getting moved / stretched / warped, its position and extents stay constant
  • The player isn't in the wrong place when the scene loads or when the body_entered event happens
  • It's not a coordinate space issue. The coordinate space of the player never changes, the player is not a child of the loaded room scene, they're both children of the root "Main" node.

Extra rambling

  • I vaguely remember somebody saying in one of the issue threads that the root cause of this might be the body being placed at (0,0) for a frame while the scene is loaded. In a previous revision of this project I tested that but I don't think this is the case. In that test project I had the player change between two rooms, with a big Area2D rectangle centered at (0,0), and the player being at (-100, -100) in one room and (100,100) in the other room. The body entered signal never fired (as expected) regardless of whether I moved the player before or after loading the scene.
  • Somebody else mentioned that this might be a race condition in _process/_physics_process but it's reproducible 100% of the time.

My guess as to why this is happening

My (wild) guess is that maybe there's some kind of signal caching / queue-ing going on?

Steps to reproduce

Note: When I refer to 1,2,3, I mean the number keys 1, 2, and 3 on your keyboard. Look at the output to see when a body_entered event fires.

  1. Start the project, note that the player is at position A
  2. Hit 1, note that no body_entered events fire just from reloading the scene (as expected)
  3. Hit 2, to move the player to PositionB, note that the body_entered signal is emitted because the kinematic body (labeled "player") is now intersecting Area2D
  4. Hit 1 to reload the scene, note that body_entered signals fire every time you do this (this is OK)
  5. Hit 2 again to move the player back to Position A, note that no body_entered signal is emitted (as expected)
  6. Hit 3 to move the player to position B and reload the scene, note that a body_entered signal is emitted (as expected)
  7. Hit 3 again to move the player to position A and reload the scene, note that a body_entered signal is emitted even though the player is nowhere near the Area's extents. This is the bug I'm reporting.

Step 7 produces this output:

Player moved to (-100, -100)
Changing scene to res://Room2.tscn
Room2_Area2D_body_entered, intersecting body is at (-100, -100) my position is (74.692101, 96.940903) my extents are (35.8983, 24.1007)

Carefully note the order of events:

  1. The player got moved out of the Area
  2. The scene changed
  3. After the scene loaded the event fired, even though the player has not moved since the scene changed

Minimal reproduction project

NodeParentArea2DFalseTrigger.zip

@superlazyname
Copy link
Author

Also, just FYI, I can also reproduce this in the latest commit as of right now, cd78718

@superlazyname
Copy link
Author

superlazyname commented Jun 9, 2022

More details

If I do

b servers/physics_2d/godot_area_2d.h:165

I notice that this breakpoint fires when hitting 3 to move from position B to position A, then reload the scene
(image for reference)

screenshot showing positions

(gdb) bt
#0  GodotArea2D::add_body_to_query (this=0x5555625fb0d0, p_body=0x55556252b4c0, p_body_shape=0, p_area_shape=0) at servers/physics_2d/godot_area_2d.h:165
#1  0x000055555b57bdbd in GodotAreaPair2D::pre_solve (this=0x5555624c3ce0, p_step=0.0166666675) at servers/physics_2d/godot_area_pair_2d.cpp:73
#2  0x000055555b153838 in GodotStep2D::_pre_solve_island (this=0x555562235960, p_constraint_island=...) at servers/physics_2d/godot_step_2d.cpp:84
#3  0x000055555b1540d5 in GodotStep2D::step (this=0x555562235960, p_space=0x5555622da110, p_delta=0.0166666675) at servers/physics_2d/godot_step_2d.cpp:254
#4  0x000055555b133761 in GodotPhysicsServer2D::step (this=0x5555622ba070, p_step=0.0166666675) at servers/physics_2d/godot_physics_server_2d.cpp:1254
#5  0x000055555b4f6bc5 in PhysicsServer2DWrapMT::step (this=0x5555622ba3b0, p_step=0.0166666675) at servers/physics_server_2d_wrap_mt.cpp:74
#6  0x0000555557937c0b in Main::iteration () at main/main.cpp:2718
#7  0x00005555578ed6f8 in OS_LinuxBSD::run (this=0x7fffffffd990) at platform/linuxbsd/os_linuxbsd.cpp:441
#8  0x00005555578e8935 in main (argc=9, argv=0x7fffffffde88) at platform/linuxbsd/godot_linuxbsd.cpp:68

However it does not fire when I hit 2 to move from position B to position A (without reloading the scene).

I'm not sure exactly what _pre_solve_island is doing yet, or what an island (looks like a list of constraints) or constraint is.

A bit of code feedback: it would be nice to have more comments explaining what these classes/structs/etc. represent. Even a one liner like "A constraint is an object that can be solved for collisions" or "an island is a group of constraints, grouped by ..." would help.

@superlazyname
Copy link
Author

Another quick update,

Unfortunately setting the collision layer and mask to 0 before doing the scene change, and restoring it afterwards is not a workaround. No change in behavior.

It also does not make a difference whether you create the new scene before or after moving the player.

In main.gd:

func initialize_scene(scene_path, move_player):
	# Setting collision layer and mask to 0 (collide with nothing) at the start of the function,
	# then resetting it to 1 later, does not help
	var scene_type = load(scene_path)
	if scene_type == null:
		print('Unable to find scene ', scene_path)
		return
	
	var old_collision_layer = Player.collision_layer
	var old_collision_mask = Player.collision_mask
	Player.collision_layer = 0
	Player.collision_mask = 0

	
	
	# NOTE: It does not matter if you create an instance of the new scene before or after moving the player
	print('Instanced scene')
	var scene =  scene_type.instance()
	
	if move_player:
		move_player()
	
	Player.collision_layer = old_collision_layer
	Player.collision_mask = old_collision_mask
	print('Changing scene to ', scene_path)
	do_scene_change(scene)

@superlazyname
Copy link
Author

One more update, I found what seems to be a workaround.

Hierarchy changes:

  • Instead of having the KinematicBody (the player) as a child of the root, equal in hierarchy to the scene, have the player node be a child of the scene.

New hierarchy is

Main
  Scene (dynamically allocated)
    Player / Kinematic body (dynamically allocated)

On every scene change:

  1. Create an instance of the new scene
  2. Delete the kinematic body (the player)
  3. Create a new kinematic body instance
  4. Move the player to where they need to be in the new scene
  5. Add the player to the new scene's tree
  6. Delete the old scene
  7. Add the new scene to the tree
extends Node2D

onready var PlayerType = preload("res://Player_Scene.tscn")
onready var Player = PlayerType.instance()

var PositionToggle = false
var CurrentScene = null

func initialize_scene(scene_path, move_player): 	
	var scene_type = load(scene_path)
	if scene_type == null:
		print('Unable to find scene ', scene_path)
		return

	print('Instanced scene')
	var scene =  scene_type.instance()
	
	if move_player:
		PositionToggle = not PositionToggle
		
	if Player != null:
		Player.queue_free()
		Player = null		
	
	Player = PlayerType.instance()
	
	move_player()
	
	scene.add_child(Player)
	
	print('Changing scene to ', scene_path)
	
	if CurrentScene != null:
		self.remove_child(CurrentScene)
		CurrentScene.queue_free()
		CurrentScene = null
		
	CurrentScene = scene
	
	self.add_child(CurrentScene)

func _ready():
	initialize_scene('res://Room2.tscn', false)
	
func move_player():
	if PositionToggle:
		Player.position = Vector2(-100.0, -100.0)
	else:
		Player.position = Vector2(100.0, 100.0)
	
	print('Player position set to ', Player.position)

func _process(delta):
	if Input.is_action_just_pressed('room_2'):
		initialize_scene('res://Room2.tscn', false)
	elif Input.is_action_just_pressed('room_2_and_move'):
		initialize_scene('res://Room2.tscn', true)
	elif Input.is_action_just_pressed('position_toggle'):
		PositionToggle = not PositionToggle
		move_player()

@superlazyname
Copy link
Author

As of 880a017 (the latest from master as of right now) it looks like it's behaving better.

When I hit 3 from position B I am not getting that body_entered event anymore, which is great!

@jtuttle
Copy link

jtuttle commented Jan 12, 2023

I'm experiencing this same issue with Godot 3.5.1. I think it's due to #18748 which says this is intended behavior due to physics optimizations.

Your workaround(s) didn't do the trick for me. After finding the issue I linked above, I figured out a workaround where I disable the player's CollisionShape2D on exit, then in the Player's _physics_process I add a listener for the NEXT physics frame and use it to re-enable the CollisionShape2D and remove the listener:

player.gd

func _physics_process(delta: float) -> void:
        ...
	if $CollisionShape2D.disabled:
		get_tree().connect("physics_frame", self, "on_SceneTree_physics_frame")

func on_SceneTree_physics_frame() -> void:
	get_tree().disconnect("physics_frame", self, "on_SceneTree_physics_frame")
	$CollisionShape2D.disabled = false

Not great, but it does ensure that a full physics frame passes before the collider is re-enabled. This should work until I have to disable the player's CollisionShape2D for some other reason!

@KoBeWi
Copy link
Member

KoBeWi commented Jul 6, 2023

I'm experiencing a similar bug in 4.1. I have a body and area that never really overlap, but the area receives entered signal. It happens randomly.

@dalexeev
Copy link
Member

dalexeev commented Jan 8, 2024

I confirm that the bug is reproducible in 4.3 dev 1. Trying to workaround this by temporarily changing process_mode doesn't work, the only workaround is to change the collision_layer (and set_physics_process(), so as not to process a body with collision disabled).

I'm guessing the reason is that the body doesn't update the coordinates in the physics server when needed (you can ensure that the node coordinates are updated at the right time).

area-bug.zip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants