Unparenting a Node2D while retaining its position, scale, rotation, skew

Godot Version

Godot_v4.7-stable_linux.x86_64

Question

So initially, I (naively) have Utility functions for parenting/unparenting:

func parent_add_child_without_transform(parent: Node2D, child: Node2D) -> Transform2D:
	var global_transform = child.global_transform
	parent.add_child(child)
	child.global_transform = global_transform
	return global_transform


func parent_remove_child_without_transform(parent: Node2D, child: Node2D) -> Transform2D:
	var global_transform = child.global_transform
	parent.remove_child(child)
	child.global_transform = global_transform
	return global_transform

# for debugging
func print_gl_lc_sc_rot(card: Node2D) -> void:
	print("%s:\nG sx sy r %+.2f %+.2f %+.2f    L sx sy r %+.2f %+.2f %+.2f\n" %
	[card, card.global_scale.x, card.global_scale.y, card.global_rotation, card.scale.x, card.scale.y, card.rotation])

However, I encountered some weird behavior, where transitioning from a positive to negative (and vice-versa) global_scale.x would not change global_scale.x itself, but would instead flip the sign of global_scale.y and set global_rotation = -PI.

This only happens for the x value of scaling, y works normally.

And to be clear, this is already documented and intentional.

Vector2 scale [default: Vector2(1, 1)] set_scale(value) setterget_scale() getter
The node’s scale, relative to the node’s parent. Unscaled value: (1, 1). See also global_scale.
Note: Negative X scales in 2D are not decomposable from the transformation matrix. Due to the way scale is represented with transformation matrices in Godot, negative scales on the X axis will be changed to negative scales on the Y axis and a rotation of 180 degrees when decomposed.

So,

func remove_cards(p_cards: Array[Card]) -> void:
	for card in p_cards:
		if card.temp_tracker:
			print("start removing from pile")
			Utils.print_gl_lc_sc_rot(card)

		if card.get_parent() == card_container:
			var xform: Transform2D = Utils.parent_remove_child_without_transform(card_container, card)
			if card.temp_tracker:
				print("Transform2D:\n%s\n" % [xform])

		if card.temp_tracker:
			print("finish removing from pile")
			Utils.print_gl_lc_sc_rot(card)

gives

start removing from pile
Card:<Node2D#56186898182>:
G sx sy r +0.09 -0.09 -3.14    L sx sy r -1.00 +1.00 +0.00

Transform2D:
[X: (-0.09, 0.0), Y: (0.0, 0.09), O: (1697.5, 966.5)]

finish removing from pile
Card:<Node2D#56186898182>:
G sx sy r +0.09 -0.09 -3.14    L sx sy r +0.09 -0.09 -3.14

Note that the global scale and rotation are maintained, but y/rotation flip happens locally.

Arbitrary examples

Here are some arbitrary examples:

  • 1a, 1b modify y so they work fine,
  • 2a modifies the local scale.x, so it works fine for the local variables, but you do see the effect I’m describing on the global scale and
  • 2b completely bugs out, as we’re modifying the global scale x
func print_gl_lc_sc_rt(node: Node2D) -> void:
	print("G sx sy r %+.2f %+.2f %+.2f  L sx sy r %+.2f %+.2f %+.2f" %
	[node.global_scale.x, node.global_scale.y, node.global_rotation, node.scale.x, node.scale.y, node.rotation])


func _ready() -> void:
	var node: Node2D
	node = Node2D.new()



	print("\n\n\nExample 1")
	var node_ex1: Node2D = node.duplicate()
	for _i in range(9):
		node_ex1.scale.y -= 0.1
		print_gl_lc_sc_rt(node_ex1)


	var node_ex1a: Node2D = node_ex1.duplicate()
	print("\nExample 1a: CORRECT")
	for _i in range(11):
		node_ex1a.scale.y -= 0.1
		print_gl_lc_sc_rt(node_ex1a)

	var node_ex1b: Node2D = node_ex1.duplicate()
	print("\nExample 1b: CORRECT")
	for _i in range(11):
		node_ex1b.global_scale.y -= 0.1
		print_gl_lc_sc_rt(node_ex1b)



	print("\n\n\nExample 2")
	var node_ex2: Node2D = node.duplicate()
	for _i in range(9):
		node_ex2.scale.x -= 0.1
		print_gl_lc_sc_rt(node_ex2)


	var node_ex2a: Node2D = node_ex2.duplicate()
	print("\nExample 2a: CORRECT")
	for _i in range(11):
		node_ex2a.scale.x -= 0.1
		print_gl_lc_sc_rt(node_ex2a)

	var node_ex2b: Node2D = node_ex2.duplicate()
	print("\nExample 2b: INCORRECT")
	for _i in range(11):
		node_ex2b.global_scale.x -= 0.1
		print_gl_lc_sc_rt(node_ex2b)
	
	breakpoint

This returns

Example 1
G sx sy r +1.00 +0.90 +0.00  L sx sy r +1.00 +0.90 +0.00
G sx sy r +1.00 +0.80 +0.00  L sx sy r +1.00 +0.80 +0.00
G sx sy r +1.00 +0.70 +0.00  L sx sy r +1.00 +0.70 +0.00
G sx sy r +1.00 +0.60 +0.00  L sx sy r +1.00 +0.60 +0.00
G sx sy r +1.00 +0.50 +0.00  L sx sy r +1.00 +0.50 +0.00
G sx sy r +1.00 +0.40 +0.00  L sx sy r +1.00 +0.40 +0.00
G sx sy r +1.00 +0.30 +0.00  L sx sy r +1.00 +0.30 +0.00
G sx sy r +1.00 +0.20 +0.00  L sx sy r +1.00 +0.20 +0.00
G sx sy r +1.00 +0.10 +0.00  L sx sy r +1.00 +0.10 +0.00

Example 1a: CORRECT
G sx sy r +1.00 +0.00 +0.00  L sx sy r +1.00 +0.00 +0.00
G sx sy r +1.00 -0.10 +0.00  L sx sy r +1.00 -0.10 +0.00
G sx sy r +1.00 -0.20 +0.00  L sx sy r +1.00 -0.20 +0.00
G sx sy r +1.00 -0.30 +0.00  L sx sy r +1.00 -0.30 +0.00
G sx sy r +1.00 -0.40 +0.00  L sx sy r +1.00 -0.40 +0.00
G sx sy r +1.00 -0.50 +0.00  L sx sy r +1.00 -0.50 +0.00
G sx sy r +1.00 -0.60 +0.00  L sx sy r +1.00 -0.60 +0.00
G sx sy r +1.00 -0.70 +0.00  L sx sy r +1.00 -0.70 +0.00
G sx sy r +1.00 -0.80 +0.00  L sx sy r +1.00 -0.80 +0.00
G sx sy r +1.00 -0.90 +0.00  L sx sy r +1.00 -0.90 +0.00
G sx sy r +1.00 -1.00 +0.00  L sx sy r +1.00 -1.00 +0.00

Example 1b: CORRECT
G sx sy r +1.00 +0.00 +0.00  L sx sy r +1.00 +0.00 +0.00
G sx sy r +1.00 -0.10 +0.00  L sx sy r +1.00 -0.10 +0.00
G sx sy r +1.00 -0.20 +0.00  L sx sy r +1.00 -0.20 +0.00
G sx sy r +1.00 -0.30 +0.00  L sx sy r +1.00 -0.30 +0.00
G sx sy r +1.00 -0.40 +0.00  L sx sy r +1.00 -0.40 +0.00
G sx sy r +1.00 -0.50 +0.00  L sx sy r +1.00 -0.50 +0.00
G sx sy r +1.00 -0.60 +0.00  L sx sy r +1.00 -0.60 +0.00
G sx sy r +1.00 -0.70 +0.00  L sx sy r +1.00 -0.70 +0.00
G sx sy r +1.00 -0.80 +0.00  L sx sy r +1.00 -0.80 +0.00
G sx sy r +1.00 -0.90 +0.00  L sx sy r +1.00 -0.90 +0.00
G sx sy r +1.00 -1.00 +0.00  L sx sy r +1.00 -1.00 +0.00



Example 2
G sx sy r +0.90 +1.00 +0.00  L sx sy r +0.90 +1.00 +0.00
G sx sy r +0.80 +1.00 +0.00  L sx sy r +0.80 +1.00 +0.00
G sx sy r +0.70 +1.00 +0.00  L sx sy r +0.70 +1.00 +0.00
G sx sy r +0.60 +1.00 +0.00  L sx sy r +0.60 +1.00 +0.00
G sx sy r +0.50 +1.00 +0.00  L sx sy r +0.50 +1.00 +0.00
G sx sy r +0.40 +1.00 +0.00  L sx sy r +0.40 +1.00 +0.00
G sx sy r +0.30 +1.00 +0.00  L sx sy r +0.30 +1.00 +0.00
G sx sy r +0.20 +1.00 +0.00  L sx sy r +0.20 +1.00 +0.00
G sx sy r +0.10 +1.00 +0.00  L sx sy r +0.10 +1.00 +0.00

Example 2a: CORRECT
G sx sy r +0.00 +1.00 +0.00  L sx sy r +0.00 +1.00 +0.00
G sx sy r +0.10 -1.00 -3.14  L sx sy r -0.10 +1.00 +0.00
G sx sy r +0.20 -1.00 -3.14  L sx sy r -0.20 +1.00 +0.00
G sx sy r +0.30 -1.00 -3.14  L sx sy r -0.30 +1.00 +0.00
G sx sy r +0.40 -1.00 -3.14  L sx sy r -0.40 +1.00 +0.00
G sx sy r +0.50 -1.00 -3.14  L sx sy r -0.50 +1.00 +0.00
G sx sy r +0.60 -1.00 -3.14  L sx sy r -0.60 +1.00 +0.00
G sx sy r +0.70 -1.00 -3.14  L sx sy r -0.70 +1.00 +0.00
G sx sy r +0.80 -1.00 -3.14  L sx sy r -0.80 +1.00 +0.00
G sx sy r +0.90 -1.00 -3.14  L sx sy r -0.90 +1.00 +0.00
G sx sy r +1.00 -1.00 -3.14  L sx sy r -1.00 +1.00 +0.00

Example 2b: INCORRECT
G sx sy r +0.00 +1.00 +0.00  L sx sy r +0.00 +1.00 +0.00
G sx sy r +0.10 -1.00 -3.14  L sx sy r -0.10 +1.00 +0.00
G sx sy r +0.00 -1.00 +0.00  L sx sy r +0.00 -1.00 +0.00
G sx sy r +0.10 +1.00 -3.14  L sx sy r -0.10 -1.00 +0.00
G sx sy r +0.00 +1.00 +0.00  L sx sy r +0.00 +1.00 +0.00
G sx sy r +0.10 -1.00 -3.14  L sx sy r -0.10 +1.00 +0.00
G sx sy r +0.00 -1.00 +0.00  L sx sy r +0.00 -1.00 +0.00
G sx sy r +0.10 +1.00 -3.14  L sx sy r -0.10 -1.00 +0.00
G sx sy r +0.00 +1.00 +0.00  L sx sy r +0.00 +1.00 +0.00
G sx sy r +0.10 -1.00 -3.14  L sx sy r -0.10 +1.00 +0.00
G sx sy r +0.00 -1.00 +0.00  L sx sy r +0.00 -1.00 +0.00

So my questions are:

  • Is “unexpected” behavior exclusively restricted to cases where scale.x moves between positive and negative?
  • What is the cleanest approach for defining parenting/unparenting while retaining the physical properties of my Node2D, while avoiding all of the weirdness above? Is it just to perform the global translation, and then compare and adjust local variables before/after within the parenting/unparenting method?

Appreciate any help, thanks!

I believe so, yes.

Don’t know if you’re willing, bus as - under the hood - the actual horizontal flip transform is -scale.y and rotate 180 degrees… Proactively set those values in stead? Or does that mess with a transition of yours?

Clean is a pretty subjective term.

Anyway, without reproducing your project myself I 'm not sure, but at least my answer gave your question a little bump. :wink:

Dont do it in the first place, its an inherently heavy process for the engine, unparenting updates the whole tree as each sub section is its own array, which then need to be updated.

Take a step back and ask yourself why you are doing it and whether there is another approach, which there almost certainly will be.

If you really need the exact properties with the exact values, might as well use position and rotation (and whatever else is needed) instead of transform.

Those dont include scale or skew though. Transform is best.

Global transform is actually not stored as a direct value. It’s always calculated/cached. So it only makes sense in the context of some node hierarchy, i.e. the node must have some top ancestor whose transform serves as a reference point for how node’s local transform value is interpreted in “global” context.

An orphaned node doesn’t have any “global” context. As soon as you unparent it, querying its global_transform will return its local transform.

If you want to re-instate orphan node’s former global transform when it’s reparented - store the global transform before unparenting. You can keep it in a custom property or node’s metadata if there is no script on the node. Then re-assign it when you reparent. Note that this will work as expected only if you re-parent to exactly the same context, i.e. the top node in the hierarchy you’re reparenting to, musn’t have changed its transforms since unparenting.

That said, as @OriginalBadBoy noted, this looks like a bit of a XYproblem question. There’s probably a better way to solve your actual problem, that doesn’t require unparenting.

Depending on the requirements, I personally would:

  • Take the old objects transform information and store it (as @normalized said)
  • Destroy the old object
  • Instaniate a new object in the correct hierarchy location with the correct transform information.
  • Make it visible (if it isnt already by default)

If you are doing that in the same frame then the player shouldn’t notice.

(post deleted by author)