Player position from new scene

Godot Version

4.7 stable

Question

Quick intro to what I’m working on:
I’m currently building out the core gameplay loop for a 3D treasure hunter game. The basic idea is that you fight off skeletons, hunt down keys, and loot treasure chests for crystals to unlock portals to the next level.
To shake things up, you’ll also run into bosses that force you to actually use all those gadgets you’ve been collecting along the way, rather than just letting them sit in your inventory.

I’m currently adapting this from a Godot Character Controller template I bought on GameDev.tv. I’m also using the DragonForge addon, but that shouldn’t be affecting this issue.

What is the issue?
Right now, my current level’s portal scene handles the player’s spawn position for the next level.
Inside the Portal scene (a Node3D), I have a player_spawner variable that passes a Transform3D (global_transform for rotation and position). The problem is, this doesn’t load the transform from the new scene where the portal is instantiated; instead, it keeps taking the transform from the current scene.
I could recalculate these transforms in the previous scene to get the positioning right for the next one, but that feels overcomplicated. Sure, I could just copy-paste the coordinates from an empty Node3D after placing it, but it would be much more elegant to keep the player_spawner inside the Portal scene so I can visually place it right there in the level.

What is the scene looks like ?

Nothing fancy going on here—it’s just a stone animation and a plane mesh with a shader to make a little bubbly black-hole noise effect.
As for the code responsible for the teleportation (change_scene()):
I haven’t actually touched this part. It lives inside the UI class and came straight out of the package I bought.

func change_scene(next_scene: String, player_transform: Transform3D) -> void:
	# Store a reference to the player to pass its settings onto the next player.
	var player = get_tree().get_first_node_in_group("Player")
	# Stop movement and cache settings.
	player.set_physics_process(false)
	var zoom = player.zoom
	var view = player.view
	var tween = create_tween()
	fade_out(tween)
	tween.tween_callback(func(): get_tree().change_scene_to_file(next_scene))
	# Wait at least one frame for the scene to update and ready.
	tween.tween_interval(0.1)
	tween.tween_callback(func():
		# Apply the cached variable to the new player.
		var new_player = get_tree().get_first_node_in_group("Player")
		# Set the player's position in the new level.
		new_player.global_transform = player_transform
		new_player.view = view
		new_player.zoom = zoom
		)
	fade_in(tween)

inside the portal is simply

@onready var area_next_level: Area3D = $area_next_level
@onready var player_spawner: Node3D = $PlayerSpawner
@export_file("*.scn") var next_level

func _on_area_3d_body_entered(body: Node3D) -> void:
	if body is Player:
		Disk.save_game()
		UserInterface.change_scene(next_level, player_spawner.global_transform)

What be safest way to solve a two issues:

  1. get player_spawner.global_transfrom from next_level/Portal/player_spawner instead of current portal.
  2. in case of multiple portals in scene ( one to return to previous level, next to progress to next level) what logic be best to use to avoid wrong spawn of player ?

Level scenes are looking like this

In your level code make an @export var transform3D and put your location details in there.

This way when you instantiate your level scene, you have access to that data and can pull the spawn points from there.

If your level has multiple spawn points, make it an dictionary[string, transform3D] instead. This way instead of forwarding a transform3D value, just dig into the dictionary and use the string keys to apply location data. Just in your level transition code be sure to pass the key as a variant.

I have a similar set up, but I just use Vector3 coordinates for the location and this is how I did it.

Worth a shot?

(lmk if you’re intrigued want more details)

Sounds good, it’s a bit of manual setup but simple enough to make it happen.

I’ll just leave for now space if someone maybe propose other solution ok?

Take a look at how I do it in my Game Template Plugin. The project contains a 2D and 3D example.

Specifically in my MapLevel3D code:

@icon("uid://gpt0r4tqaaem")
class_name MapLevel3D extends Node3D

@export var spawn_point: Node3D
@export var level_music: AudioStream

var player: CharacterBody3D


func _ready() -> void:
	process_mode = Node.PROCESS_MODE_PAUSABLE


func start(incoming_player: Node, incoming_spawn_position: String) -> void:
	show()
	if incoming_player == null:
		player = Game.player_scene.instantiate()
		player.set_physics_process(false)
		add_child(player)
	else:
		player = incoming_player
		player.set_physics_process(false)
		player.reparent(self)
	if incoming_spawn_position != "":
		for node in get_children():
			if node.name == incoming_spawn_position:
				print(incoming_spawn_position + " Found!")
				spawn_point = node
				break
	player.position = spawn_point.position
	player.set_physics_process(true)
	if level_music:
		Music.play(level_music)
	Disk.load_game()
	player.show()

I allow you to set a spawn_point which I typically use a Marker3D or you can pass in a text string when loading the level that matches a node in the level. I use this to allow multiple Area3D nodes to be different passages in and out of the level to other areas, Then I just link the level transitions that way.

If rotation is important to you, just apply the Marker3D’s rotation to the player when you spawn it in.