How to spawn player at predetermined coordinates after changing scene for 2D top-down rpg

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Xethenion

The type of game in question is 2D top-down RPG. So I know how to change scenes by using an area2d with a script on collisionshape2d to change scene(levels), but I am unable to find how to set a spawn point for the player for after changing scenes. I could just make multiple versions of world to create the illusion of going thru correct door, but I imagine this would prevent progress such as opened chests from being saved. My code is similar to HeartBeast’s Action RPG in his Godot 3.2 Tutorials if needing background to work from. Here is my script for changing scenes below.

extends CollisionShape2D

export (String, FILE, “*.tscn”) var world_exit

func _on_Area2D_body_entered(body):
get_tree().change_scene(world_exit)

:bust_in_silhouette: Reply From: exuin

Edit: Just realized that since you’re using get_tree().change_scene() to change the scene, this won’t work. Sorry. Actually, what you should do is to have a singleton that changes scenes for you which can carry information between scenes.

Make a script. Let’s call it “Global.gd”. Go to Project Settings —> AutoLoad and add it to the list. This lets you access it from anywhere in the game. Make a changing scene function, like:

func change_scene(new_scene, player_pos):
    get_tree().change_scene(new_scene)
    player.set_position(player_pos)

(make sure you define the player variable first)
Now change your CollisionShape2D script to

extends CollisionShape2D

export (String, FILE, "*.tscn") var world_exit
export(Vector2) var player_pos

func on_Area2D_body_entered(_body):
    Global.change_scene(world_exit, player_pos)

Since Global.gd is a singleton, you can call its functions that way.


Old answer (ignore)

You can just have another export variable and set the player’s position to that after changing the scene.

extends CollisionShape2D

export (String, FILE, "*.tscn") var world_exit
export(Vector2) var player_pos
onready var player = *put the path to the player here*

func on_Area2D_body_entered(_body):
    get_tree().change_scene(world_exit)
    player.set_position(player_pos)

I attempted to follow what you mentioned in your newest answer, but I am receiving an error only when I am running the game. Here’s the error.

Invalid call. Nonexistent function 'set_position' in base 'Nil'.

and additional error I found that related to line 6 of Global.gd

E 0:00:00.972   _load: Resource file not found: res://.
  <C++ Error>   Condition "!file_check->file_exists(p_path)" is true. Returned: RES()
  <C++ Source>  core/io/resource_loader.cpp:282 @ _load()
  <Stack Trace> Global.gd:6 @ change_scene()
                CollisionShape2D.gd:7 @ _on_Area2D_body_entered()

I’m still learning the language but does this mean it is basically not connected to something? Here is my script for Global.gd, which is also placed in autoload. My error is at player.set_position(player_pos) or line 7

extends Node

var player

func change_scene(new_scene, player_pos):
	get_tree().change_scene(new_scene)
	player.set_position(player_pos)

And here is my script for my collisionshape2d:

extends CollisionShape2D

export (String, FILE, "*.tscn") var world_exit
export(Vector2) var player_pos

func _on_Area2D_body_entered(body):
	Global.change_scene(world_exit, player_pos)

I think I’m forgeting a step but unsure, If needed here is the player script as well.

extends KinematicBody2D

const PlayerHurtSound  = preload ("res://Player/PlayerHurtSound.tscn")

export var ACCELERATION = 500
export var MAX_SPEED = 100
export var ROLL_SPEED =125
export var FRICTION = 500
export var ATTACK_MOVEMENT = 100

enum {
	MOVE,
	ROLL,
	ATTACK
}

var state = MOVE
var velocity = Vector2.ZERO
var roll_vector = Vector2.DOWN
var stats = PlayerStats
var player

onready var animationPLAYER = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get('parameters/playback')
onready var swordHitbox = $HitboxPivot/SwordHitbox
onready var hurtbox = $HURTBOX
onready var blinkAnimationPlayer = $BlinkAnimationPlayer

func _ready():
	randomize()
	stats.connect("no_health", self, "queue_free")
	animationTree.active = true
	swordHitbox.knockback_vector = roll_vector

func _physics_process(delta):
	match state:
		MOVE:
			move_state(delta)
	
		ROLL:
			roll_state()
	
		ATTACK:
			attack_state()
	
func move_state(delta):
	var input_vector = Vector2.ZERO
	input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
	input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
	input_vector = input_vector.normalized()
	
	if input_vector != Vector2.ZERO:
		roll_vector = input_vector
		swordHitbox.knockback_vector = input_vector
		animationTree.set("parameters/IDLE/blend_position", input_vector)
		animationTree.set("parameters/RUN/blend_position", input_vector)
		animationTree.set("parameters/ATTACK/blend_position", input_vector)
		animationTree.set("parameters/ROLL/blend_position", input_vector)
		animationState.travel("RUN")
		velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
	else:
		animationState.travel("IDLE")
		velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
	
	move()
	
	if Input.is_action_just_pressed("ROLL"):
		state = ROLL

	if Input.is_action_just_pressed("ATTACK"):
		state = ATTACK

func roll_state():
	velocity = roll_vector * ROLL_SPEED
	animationState.travel("ROLL")
	move()

func attack_state():
	velocity = roll_vector * ATTACK_MOVEMENT
	animationState.travel("ATTACK")
	move()

func move():
	velocity = move_and_slide(velocity)

func roll_animation_finished():
	velocity = velocity * 1
	state = MOVE

func attack_animation_finised():
	state = MOVE
	
func _on_HURTBOX_area_entered(area):
	stats.health -= area.damage
	hurtbox.start_invinciblity(0.6)
	hurtbox.create_hit_effect()
	var playerHurtSound = PlayerHurtSound.instance()
	get_tree().current_scene.add_child(playerHurtSound)

func _on_HURTBOX_invincibility_started():
	blinkAnimationPlayer.play("START")

func _on_HURTBOX_invincibility_ended():
	blinkAnimationPlayer.play("STOP")

Xethenion | 2020-09-16 03:11

Okay, so I see your script for global is

extends Node

var player

func change_scene(new_scene, player_pos):
    get_tree().change_scene(new_scene)
    player.set_position(player_pos)

Now, you’ve declared player, but you need to assign a node to it. I don’t know where your player node is located, however, so I can’t do that for you. I’m assuming that your player node is attached to the map’s node. So in that case, the path to the player node would be something like get_tree().get_root().get_child(0).get_node("Player") and you would have this code:

extends Node

func change_scene(new_scene, player_pos):
    get_tree().change_scene(new_scene)
    var player = get_tree().get_root().get_child(0).get_node("Player")
    player.set_position(player_pos)

But your player node might be in a different location altogether.

exuin | 2020-09-16 04:11

Alright, I placed my player scene as a autoload and it accepted it so that got rid of the error, but now when I go through my collision shape to change scenes I still end up at same position of new scene instead of the coordinates I set. I also have been receiving another error

E 0:00:01.016   _load: Resource file not found: res://.
  <C++ Error>   Condition "!file_check->file_exists(p_path)" is true. Returned: RES()
  <C++ Source>  core/io/resource_loader.cpp:282 @ _load()
  <Stack Trace> Global.gd:4 @ change_scene()
                CollisionShape2D.gd:8 @ _on_Area2D_body_entered()

for the Global.gd script at get_tree().change_scene(new_scene)

extends Node

func change_scene(new_scene, player_pos):
	get_tree().change_scene(new_scene)
	var player = get_tree().get_root().get_child(0).get_node("/root/Player")
	player.set_position(player_pos)

Also here is the collisionShape2D script if needed.

extends CollisionShape2D

export (String, FILE, "*.tscn") var world_exit
export(Vector2) var player_pos


func _on_Area2D_body_entered(body):
	Global.change_scene(world_exit, player_pos)

If needed I can provide more info or even photos of work if needed.

Xethenion | 2020-09-16 17:11

Okay, what does your node layout look like? The path I put should go like this:
get_tree().get_root().get_child(0).get_node("Player")
tree ← root <— world map node (the one that gets changed out) <— player
But if your node layout is different, it won’t work.
You put
get_tree().get_root().get_child(0).get_node("/root/Player")
this is
tree ← root <— world map node (the one that gets changed out) <— node called “/root” ← player

Edit: I can probably make you a sample project if you need it.

exuin | 2020-09-16 17:45

That would be appreciated. I still can’t seem to connect the player node correctly. I’m pretty sure I’m overlooking something that’s causing the problem when placing the player node, so a sample project would be helpful to find where it is.

Xethenion | 2020-09-21 18:36

Okay. In the meantime, what about this?

If your node layout is like this:
World Scene <-- Player
Your path should be
"/root/World Scene/Player"
However, I don’t expect you to name all of your root nodes “World Scene”, so I didn’t use the names of nodes to get them and instead used functions. The equivalent without names would be this:

# Gets the root node.
var root = get_tree().get_root()
# Gets the player node by first getting the world node, which should be the last node of the root node since it should be the one most recently added.
var player = root.get_child(root.get_child_count()-1).get_node("Player")

Earlier I put get_child(0), which was wrong, sorry.

exuin | 2020-09-21 20:09

I uploaded the project to github.

Anyway, if your player character is a child of the world scene, you can do what I did in the example. If your player character is not a child of the world scene, and they’re both siblings in some kind of main scene, you should get the player node and change its position in the Global script.

exuin | 2020-09-21 21:16

The sample project you provided helped get it running finally. Thank you very much. I was actually unsure how to find the coordinates at first but I used this code to find it using a postion2d as a reference in each of my scenes.

extends Position2D

func _ready():
	print(get_position())

Xethenion | 2020-09-28 04:04

Glad to help! I got the coordinates by looking at the position property of the nodes in the inspector.

exuin | 2020-09-28 04:52

EDIT: Never mind about how to do timer, I just used the area2d entered to trigger it for me. Here is my script for anyone who might want it.

func _on_Area2D_area_entered(area):
	for area in area_2d.get_overlapping_areas():
			if area.has_method("change_scene"):
				area.change_scene()
				return

Ignore below
So one more thing to add. I am wanting to be able to walk up to these doors or ladders and have it automatically enter them. This code provided from my player lets me enter it as long as I am standing on it and then move, but if I run through it, it doesn’t trigger.

 func _unhandled_input(event):
	if event.is_action_pressed("ui_up"):
		for area in area_2d.get_overlapping_areas():
			if area.has_method("change_scene"):
				area.change_scene()
				return
	
	if event.is_action_pressed("ui_right"):
		for area in area_2d.get_overlapping_areas():
			if area.has_method("change_scene"):
				area.change_scene()
				return
	
	if event.is_action_pressed("ui_left"):
		for area in area_2d.get_overlapping_areas():
			if area.has_method("change_scene"):
				area.change_scene()
				return
	
	if event.is_action_pressed("ui_down"):
		for area in area_2d.get_overlapping_areas():
			if area.has_method("change_scene"):
				area.change_scene()
				return

What I want to do is set a automated key that is pressed and released repeatedly through a timer set for something like (0.5), allowing me to bypass this problem. I have a timer already set as a onready var for my player but not sure how to format it to work. If this is possible then this way I only need one input to work such as this.

func _unhandled_input(event):
    	if event.is_action_pressed("repeat"):
    		for area in area_2d.get_overlapping_areas():
    			if area.has_method("change_scene"):
    				area.change_scene()
    				return

Xethenion | 2020-09-29 05:30