Projectile global_position setting to 0,0 despite my best efforts

Godot Version

Godot 4.4.1 stable

Question

Strap in.
Trying to spawn a projectile from the player facing and an offset position based on that facing. This all works EXCEPT the projectile position appears at 0,0 in my level. It flies fine, it’s direction is set correctly, it disappear at the end of it’s range.

It’s a bit convoluted as this is being triggered by a “use_item” action off my player action script. I have four 2d markers in my player tree for determining offset to instantiate weapons, tools and other effects (four direction game, basically a zelda-like) based on facing. It tells the inventory to use the item.

func use_item() -> void:
	is_iteming = true
	var item = equipped_items.front()
	if item != null:
		var item_pos: Vector2
		if last_direction == Vector2.UP:
			item_pos = marker_up.position
		elif last_direction == Vector2.RIGHT:
			item_pos = marker_right.position
		elif last_direction == Vector2.LEFT:
			item_pos = marker_left.position
		elif last_direction == Vector2.DOWN:
			item_pos = marker_down.position
#this is the line that leads to the next script
		PlayerInventory.use_item_action(item, last_direction, item_pos)
		play_animation("interact", last_direction)
		await animated_sprite_2d.animation_finished
	else:
		is_iteming = false
		return
	is_iteming = false

To PlayerInventory Global:
In the inventory global script, the inventory determines what kind of item it is (world affecting or player affecting). If it’s “world effecting” it takes the player facing and the offset position and passes them to the loaded script through the execute function. if the execute was completed it returns true and it removes one item from the stack.

func use_item_action(item, facing, position):
	var effect = load(item["source"])
	if item["use_type"] == 0:
#this is the line that leads to the next script
		var used = effect.item_execute_effect(player_node, facing, position)
		if used:
			# remove one quantity of item
			PlayerInventory.remove_item(item)
			PlayerInventory.remove_equip_item(item)
		else:
			pass

In the effect execute script, this is a ‘thrown item’, like a shuriken or axe. The projectile scene is an export on the specific resource so I can use one instantiation script for multiple types of items. The shot scene is instantiated, and then I parent it to my current scene (the level loaded, I print it immediately and it is showing up as my level). I then trigger the projectile script to ‘fire’.

@export var shot: PackedScene

#player variable is inherited from ItemEffect parent but I'm not using it in this script

func item_execute_effect(p, dir, pos):
	print("item executed")
	player = p
	var thrown = shot.instantiate()
Engine.get_main_loop().current_scene.get_tree().get_first_node_in_group("ActiveRealm").add_child(thrown)
	print(thrown.get_parent())
#this is the line that leads to the next script
	thrown.fire_shot(dir, pos)

Projectile scene Script:
I set the direction and global_position based on the variable I passed in, and then free it to begin movement until it reaches the end of it’s range:

func fire_shot(shoot_dir, pos):
	print("axe shot")
	direction = shoot_dir
	global_position = pos
	start_pos = pos
	can_fly = true

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	if can_fly:
		global_position += direction * speed * delta
		if global_position.distance_to(start_pos) > shot_range:
			queue_free()
	pass

What’s weird is all of the script for instantiating the weapon and the projectile is lifted from a ranged weapon I have that works perfectly, the only difference being that the weapon is already part of the tree so parenting it to the scene is less complicated: get_tree().get_first_node_in_group("ActiveRealm").add_child(instance)

What am I missing?

Because global_position is a derivative value of all the transforms on the node (all parent nodes in the hierarchy) it can only be set once the node enters the tree. This signal you can await before calling thrown.fire_shot(...)

Not sure which is better, actually : tree_entered or ready

So before thrown.fire_shot(..):

await thrown.ready

Your logic doesn’t have a fail case. If last_direction doesn’t match any of those values you will return an unset item_pos. I believe that will be seen as a Vector2(0,0).

Good thought, but I tried both .ready and .tree_entered and it hung, and never triggered the throw.fire_shot. The projectile appeared stationary at 0,0.
I tried
```gdscript

await Engine.get_main_loop().current_scene.get_tree().process_frame
thrown.fire_shot(dir, pos)

And it started moving again, but is still starting at 0,0 so I don’t think the issue is some type of race condition, because awaiting the .ready or .tree_entered hangs because the event it’s listening for already fired before it started listening?

Hm. I cannot help out without my laptop to reproduce, so I’m out. But it remains a fact that setting global_position will only work once the node has successfully entered the tree, for which your code right now does not provide anything.

A workaround would be to add an additional prop to the thrown thing var initial_global_pos , which you set after instantiation and assign to global_position in its _ready() func.

It has a default value (Vector2.DOWN) when I declare it at the top of the script, but good call.
I added a default position based on the player position (just in case) and I put in a print on the projectile step to see what was coming through, and the dir based on last position is carrying through:

func fire_shot(shoot_dir, pos):
	print(pos)
	direction = shoot_dir
	global_position = pos
	start_pos = pos
	can_fly = true

BUT that reveals that its only passing through the position as an offset from the parent:

item executed
axe exists
(-16.0, -8.0)
item executed
axe exists
(0.0, 8.0)
item executed
axe exists
(0.0, -24.0)
item executed
axe exists
(16.0, -8.0)

Which works great when it’s getting added to the tree from a child of the parent, but when it’s just directly getting jammed in from nowhere it’s basing that offset on 0,0 so it’s positioning it globally just a few pixels off of the origin. So I switched the use_item function to use the 2D marker’s global_position and it worked!

		var item_pos: Vector2
		if last_direction == Vector2.UP:
			item_pos = marker_up.global_position
		elif last_direction == Vector2.RIGHT:
			item_pos = marker_right.global_position
		elif last_direction == Vector2.LEFT:
			item_pos = marker_left.global_position
		elif last_direction == Vector2.DOWN:
			item_pos = marker_down.global_position
		else:
			item_pos = global_position

Recording 2026-06-06 104330

Thanks for the help with this!