Infinite loop when reparenting a CharacterBody3D while still in an Area3D - SOLVED

Godot Version

4.3

Question

Greetings! This is an interesting one for me…

I have an Area3D that is a child of a Rigidbody3D, and a player (CharacterBody3d) that are under the same “world” Node3D.

Intended Behavior:

When the player enters the Area3D, the player gets reparented to the Area3D’s parent (which is the Rigidbody3D).

Upon exited the area, the player is reparented BACK to the world node.

Actual Behavior:

When you enter the Area3D, the entered signal triggers, reparenting the player onto the Rigidbody3D and then immediately triggers the exited signal, reparenting the player back to the world again.

This cycle repeats infinitely until the player leaves the Area3D.

Attempted Solutions:

  • Using a boolean to check if the player is already inside of the Area3D before reparenting.
  • Disabling and re-enabling the Area3D’s collision when entering and exiting.
  • Disconnecting and reconnecting the Area3D’s signals then reconnecting them.

Would appreciate some help! Thanks in advance.

Here is is the code snippet with the two signals as well as a screenshot of the basic node structure:

func _ready():
	#connect signals
	player_detection_area.body_entered.connect(_on_area_3d_body_entered)
	player_detection_area.body_exited.connect(_on_area_3d_body_exited)


#~-rest of the code-~#


func _on_area_3d_body_entered(body):
	if body is Player:
		body.ship_entered(self)
		piloting_player = body
		body.reparent(self, true) #reparents player onto self (RigidBody3D)


func _on_area_3d_body_exited(body):
	if body is Player:
		body.ship_exited()
		ship_is_piloted = false
		piloting_player = null
		body.reparent(get_node("/root/DevLevel/"), true) #reparents player back onto world node

image

A question, from what im seeing this is to the player enter in a ship and start piloting, how the player leave the ship area while piloting?

1 Like

Did you try to set a boolean var reparent.

Set it to true right before reparenting it in area_entered.

Then in area_exited have the first statement be

if reparent: 
reparent = false 
return
1 Like

The player cannot move while piloting the ship; ignore that for now.

At the moment I just need to make the player not infinitely recurse when they just stand on the ship

This never allows the player to be parented back to the world, effectively trapping the player onto the ship.

You said there was a loop.

Enter->exit->enter …

Because of the reparenting.

If he enters → parents to ship.

It now calls enter → exit, you block the exit signal player doesnt reparent.

Now when player actually exits the variable is false, so player reparents.

1 Like

Something like this?

func _on_area_3d_body_entered(body):
	if body is Player:
		body.ship_entered(self)
		piloting_player = body
		reparent = true
		body.reparent(self, true) #reparents player onto self (RigidBody3D)


func _on_area_3d_body_exited(body):
	if reparent: 
			reparent = false
			return
	if body is Player:
		body.ship_exited()
		ship_is_piloted = false
		piloting_player = null
		body.reparent(get_node("/root/DevLevel/"), true) #reparents player back onto world node


Yeah, have you already tried that?

1 Like

Indeed, this doesn’t work; it prevents the loop; but the player cannot “exit” the area.

If you’re in the Godot Discord; I can show you on a screenshare

I am on my phone atm, I’ll try and see if I can make a minimal recreation once my son goes to bed, to see if I can get it to work.

1 Like

No worries! Take your time; it’s been an interesting debacle for sure

So, I got it to work, I hyper simplified it but it works now.


extends Area3D

func _on_area_3d_body_entered(body):
	body.call_deferred("reparent",self, true)

func _on_area_3d_body_exited(body):
	if overlaps_body(body):
		return
	body.call_deferred("reparent",get_node("/root/Node/"))

This was just a CharacterBody walking onto an Area, so I don’t know if what else you have will make that different, but you readd the body as player if statements at the top, and hopefully it will work. If not…

It freezes and loops, crashing the game XDD

Weird, at some point if I find time, I’ll try and recreate more exactly what your node structure is, and see if that works. Since there is something in your node structure that is causing the issue, since I was able to get it to work with minimalist code.

1 Like

Ikr, its puzzling tf outta me; appreciate the help!

I mean the only thing I’m sending the player is to set the current ship variable on the player lol

Player script:

func ship_entered(_ship: Ship):
	current_ship = _ship


func ship_exited():
	current_ship = null

I think this fails because overlaps_body will check the internal array of detected bodies to confirm if is overlapping or not, so if body_exited signal was emmited, probably the body will be already out of this array.

You can try do a manual check using the body position to confirm if the body really exited:

var _ignore_next_call := false

func _on_area_2d_body_entered(body: Node2D) -> void:
	if _ignore_next_call:
		_ignore_next_call = false
		return
	
	body.call_deferred("reparent", self)


func _on_area_2d_body_exited(body: Node2D) -> void:
	var rect := Rect2()
	# Im assuming you're using a rectangle/squared shape
	var col_size = $CollisionShape2D.shape.size
	rect.position = $CollisionShape2D.global_position - (col_size/2)
	rect.size = col_size
	
	if rect.has_point(body.global_position):
		_ignore_next_call = true
		return
	
	body.call_deferred("reparent", get_node("/root/Node/"))
1 Like

While this may work, I would have to translate this to a 3D system. Must I create a custom 3d box each time?

Will try setting up a brand new project and test it

Sorry, that is my mind too 2D focused in action hahaha. For 3D would be used AABB instead, but while testing this problem i found a more easier solution:

func _on_area_3d_body_entered(body):
	if body is Player and body.get_parent() != self:
		body.call_deferred("reparent", self, true)

func _on_area_3d_body_exited(body):
	if body is Player and body.get_parent() != get_node("/root/Node/")
		body.call_deferred("reparent", get_node("/root/Node/"))
2 Likes

My man, this was the fix. Appreciate your help very much!!

Big brain moment, idk why tf I didn’t think to check if it was already parented. Thanks again!