Top level child changing position after parent is reparent()

Godot Version

v4.2.2.stable.official [15073afe3]

Background

I’m making a train game so my player is a PathFollow3D that I use to follow Path3Ds in the level.
It’s possible for the player to switch tracks, and to do so I reparent() them to another Path3D

Setup

I have a Player scene that has a top level child CameraController. The CameraController is top_level so I can manually lerp() the position etc to smoothly follow the player.

I have a TitleScene that instantiates my Level and Player scenes. The Player then gets added to the Level. This currently just involves adding the Player as a child of a random Path3D.

When the level loads, the camera is in the correct position behind the player. Following it also works as expected, hurrah :tada:

However, when I reparent the Player, the camera suddenly jumps to the level “origin” (and then smoothly moves back to following the player) :scream:

What I think is happening

It seems like when the Player is first added to the Level, the CameraController gets positioned relative to the Player.

For example:
Say in the Player scene the CameraController is positioned (0, 1.5, 6).
The Player is added to the Level at position (-85, 0.25, 40)
Which gives the CameraController a position of (-85, 1.75, 46)

Once I reparent() the Player the CameraController suddenly realises its top level so “removes” the Players position.

Here is a log of the CameraControllers position (without any lerping code moving it), when I just keep switching the Player back and forth between two different parents.

(-86.00563, 1.75, -5.999998)
(-5.852806, 2, 3.41007)
(-70.56184, 2.25, 47.84929)
(-84.25036, 2.5, -9.260246)
(-47.54408, 2.75, -22.9677)
(-7.655383, 3, -4.111481)
(24.74981, 3.25, 13.84217)

As you can see the position isn’t even remaining constant after the first reparent, or switching between two different values. Which implies to me every reparent call is changing the position for some reason. This is probably easiest to see with the y value which is increasing by 0.25 every time (which is actually the Players vertical offset (a property on PathFollow3D))

What I did

I’ve saved the camera transform before I call reparent then set it back afterwards. This works but doesn’t feel correct…

Questions

  1. Is this expected behaviour, a bug?
  2. Am I doing something stupid?
  3. Is there a better/more correct way of fixing this?

Have you tried reparent(new_parent, false) to disable “keep_global_transform” i could see that editing the position of the player, then the pathfollow editing the position of the player for an all-around bad time for children who are trying to ignore their parent’s transform.

Any code samples you could post?

I created a little test scene to simplify the problem:

This is the tree with nothing but Node3D instances.

Main
    A (position x: -7)
    B (position x:  5)
    Player (position z: 3)
        TopLevel (top_level: true)

This script is attached to Main:

extends Node3D

@onready var player_node = $Player
@onready var top_level_node = $Player/TopLevel
@onready var previous = $A

func _ready():
	player_node.reparent($A)
	printt("_ready()", "Player.position", player_node.position, "TopLevel.position", top_level_node.position)

func _process(_delta):
	if Input.is_action_just_pressed("ui_accept"):
		if previous == $A:
			player_node.reparent($B, false)
			previous = $B
		else:
			player_node.reparent($A, false)
			previous = $A
		printt("_process", "Player.position", player_node.position, "TopLevel.position", top_level_node.position)

This prints out:

_ready()	Player.position	(7, 0, 3)	TopLevel.position	(-7, 0, 6)
_process	Player.position	(7, 0, 3)	TopLevel.position	(5, 0, 9)
_process	Player.position	(7, 0, 3)	TopLevel.position	(5, 0, 12)
_process	Player.position	(7, 0, 3)	TopLevel.position	(17, 0, 15)
_process	Player.position	(7, 0, 3)	TopLevel.position	(17, 0, 18)
_process	Player.position	(7, 0, 3)	TopLevel.position	(29, 0, 21)
_process	Player.position	(7, 0, 3)	TopLevel.position	(29, 0, 24)
_process	Player.position	(7, 0, 3)	TopLevel.position	(41, 0, 27)
_process	Player.position	(7, 0, 3)	TopLevel.position	(41, 0, 30)

Switching keep_global_transform to true prints out:

_ready()	Player.position	(7, 0, 3)	TopLevel.position	(-7, 0, 6)
_process	Player.position	(-5, 0, 3)	TopLevel.position	(5, 0, 9)
_process	Player.position	(7, 0, 3)	TopLevel.position	(-7, 0, 12)
_process	Player.position	(-5, 0, 3)	TopLevel.position	(5, 0, 15)
_process	Player.position	(7, 0, 3)	TopLevel.position	(-7, 0, 18)
_process	Player.position	(-5, 0, 3)	TopLevel.position	(5, 0, 21)
_process	Player.position	(7, 0, 3)	TopLevel.position	(-7, 0, 24)
_process	Player.position	(-5, 0, 3)	TopLevel.position	(5, 0, 27)
_process	Player.position	(7, 0, 3)	TopLevel.position	(-7, 0, 30)

Neither of these results seem like the correct behaviour to me. I’m new to Godot and game programming in general, but the fact the TopLevel keeps “accumulating” position changes seems wrong, or at least unintuitive from reading the docs.

Given I’m new I’m assuming I’ve done something stupid, but I really don’t know what at this point :confused:

Edit:
Changing the reparent to setting the position works as expected.

_ready()	Player.position	(0, 0, 3)	TopLevel.position	(0, 0, 3)
_process	Player.position	(5, 0, 0)	TopLevel.position	(0, 0, 3)
_process	Player.position	(-7, 0, 0)	TopLevel.position	(0, 0, 3)
_process	Player.position	(5, 0, 0)	TopLevel.position	(0, 0, 3)
_process	Player.position	(-7, 0, 0)	TopLevel.position	(0, 0, 3)
_process	Player.position	(5, 0, 0)	TopLevel.position	(0, 0, 3)
_process	Player.position	(-7, 0, 0)	TopLevel.position	(0, 0, 3)
_process	Player.position	(5, 0, 0)	TopLevel.position	(0, 0, 3)
_process	Player.position	(-7, 0, 0)	TopLevel.position	(0, 0, 3)

I think maybe this is starting to make some sense to me…

When a top_level child first gets added to the scene (not sure if that is the correct way to phrase it, but hopefully you get what I mean) it is positioned relative to its parent. This makes sense to me, because in my case I’ve positioned the CameraController relative to the Player, if the CameraController ended up relative to the root when I added the Player that would be annoying and unexpected.

So it seems to me calling reparent is basically triggering the same (or very similar) logic as when the top level node first gets added to the scene. This is why it is getting adjusted by the parents position every time.

Maybe? :man_shrugging: :confounded:

FYI, using reparent(new_parent, false) also didn’t fix the issue in my actual scene (just in case you were wondering)

1 Like

FYI, this no longer happens in Godot 4.3, so it seems like it was indeed a bug.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.