Setting global position directly offsets the scene from where it should be

Godot Version

v4.6.2.stable.mono.official [71f334935]

Question

I need help with what appears to be a bug.

I am simply trying to instantiate a new scene at the parent’s Node2D location

If I use

using Godot;
using System;

public partial class TestSpawner : Node2D
{
	[Export] PackedScene spawnScene;
	// Called when the node enters the scene tree for the first time.
	public override void _Ready()
	{
		Node2D newObject = spawnScene.Instantiate<Node2D>();	
        CallDeferred(MethodName.AddChild, newObject);		
	}
}

This works as expected. As soon as I add in newObject.GlobalPosition = GlobalPosition; things start to go wonky.

using Godot;
using System;

public partial class TestSpawner : Node2D
{
	[Export] PackedScene spawnScene;
	// Called when the node enters the scene tree for the first time.
	public override void _Ready()
	{
		Node2D newObject = spawnScene.Instantiate<Node2D>();
		newObject.GlobalPosition = GlobalPosition;
        CallDeferred(MethodName.AddChild, newObject);		
	}
}

I expect this to do the exact same thing is plainly instantiating, but it doesn’t appear so.

If the spawner is at 0,0 it works great, but at soon as I move the Node2D somewhere else there is this massive offset from where the Node2D actually is.

The reason I am wondering is I have made a spawner class that is more complex than simply placing it at the Node2D position. It looks at an Area2D’s CollisionShape2D and randomly spawns in entities within the given rectangle. The rectangle is scaled and rotated to fit the desired area.

The following code does this:

using Godot;
using System;

public partial class RunnerSpawner : Node2D
{
    [Export] PackedScene runnerScene;
    [Export] CollisionShape2D shape;
    [Export] uint amountToSpawn;
    Node2D newObject;
    float width, height;
    Vector2 localPoint, globalSpawnPosition;
    public override void _Ready()
    {
        // Set width and height based off of the collision shape's size
        width = shape.Shape.GetRect().Size.X;
        height = shape.Shape.GetRect().Size.Y;
        // Spawn in runners
        for (int i = 0; i < amountToSpawn; i++)
        {
            SpawnInArea(i);
        }
    }
    public void SpawnInArea(int number)
    {
        // Create a new runner instance
        newObject = runnerScene.Instantiate<Node2D>();
        newObject.Name = "Runner " + number;
        // Generate random local point within the collison shape
        localPoint = new Vector2(RNG.RandomFloatRange(-width / 2, width / 2), RNG.RandomFloatRange(-height / 2, height / 2));
        // Rotate the point to match the collison shape's tilt and move to it's location
        globalSpawnPosition = shape.GlobalTransform * localPoint;
        // Set the position and add it to the scene
        newObject.GlobalPosition = globalSpawnPosition;
        CallDeferred(MethodName.AddChild, newObject);
    }
}

Once again if it is at 0,0 everything works perfectly, but as soon as I move the Node2D away from there I end up with this wonky offset.

Any advice you can give me would be of great help.

P.S. I am only on day 3 of this project so bare with me

Before a child is added to the scenetree it cannot have a global position. You should be getting a soft error from the engine telling you as such. This is because parent transforms will ultimately dictate its final position and it isnt in the tree yet to do the inverse transforms for global positioning.

Try updating the global position after add child (a second deferred call). Or use to_local(global_position) and set the local position of the new child before the add.

If i think about it, the local position of the parants global position will always be zero.

Oh my god I forgot about that. There is unfortunately no soft error coming up. Adding CallDeffered to set the position definitely fix it.

Thank you so much, you were super helpful.

So i decided to do some testing on this, and it turns out, if you were to try the set the global position of a node before adding it to the scene tree, the “global position“ ends up becoming the local position relative to whatever node you parent it to. This explains why you were getting weird offsets when adding your spawned nodes as children to your spawner, because it looks like you’re taking the localPoint you get from calculating a random position within your rectangle, transforming that to global coordinates, and then trying to set that as the global position of the new object. Then you try to add it as a child of your spawner. So basically, the intended global position ends up becoming the local position relative the spawner, which is obviously going to be way off.

To be honest, you probably don’t even need to figure out the global position of whatever position you get within the rectangle. If you’re intending on adding newly spawned nodes as children of your spawner, you could just remove globalSpawnPosition = shape.GlobalTransform * localPoint and just assign localPoint to newObject.Position. That would make sure that your objects are positioned correctly relative to the spawner.

I appreciate you looking into it. This is what I expected was going on with the positions.

The instantiated scenes end up being siblings of the spawner which is why I ended up using global positions. I’m sure that logic would work, but I find it simpler to simply defer setting the position as it will work with siblings, children, or any other relationship the node might have.

Good to know, thanks.