How to properly set saved position

Godot Version

4.4.1

Question

`I have a save system (also set as global under ‘Save’) a separate player scene with attached script (set under global as ‘Guyman’) and a scene where the player is put inside.
My problem was that, when saving and loading, the player was set automatically on the position I put it in inside the viewport of the scene. So I’m trying to put in the script of the scene, that if there is a save file, the player should be put there, but I think I’m doing something wrong. My code is

func _ready() -> void:
	if FileAccess.file_exists("user://savegame.json"):
		Guyman.global_position = Vector2(Save.saved_data["player_global_positionX"], Save.saved_data["player_global_positionY"])   
#here I get an error that says Invalid access to property or key 'saved_data' on a base object of type 'Node(SaveSystem.gd)#
	else:
		Guyman.global_position   #here I'm not sure how to set it to the place I have placed it in the viewport#

The “saved_data” is what the JSON file is called, where everything I want saved is stored inside. I know the first part is wrong, but I don’t know how to properly do it`

What is Save in your case? Is it a singleton? The error is there because “Save” does not have the saved_data property.

Also your else statement does nothing.

Guyman.global_position

If by this…

…you mean where you have placed the player in your scene, then you don’t have to do anything, it will just be there anyway.

(Also you do not need closing # for comments - just in case you didn’t realise that)

You should try to keep file access stuff seperate from game logic really. What is your Save and c an you show us the code in there that relates to the property saved_data. I am guessing you are trying to access it too early and it is not ready to be accessed yet.

My Save System is this
extends Node

@onready var player = Guyman.player


func save():
	
	print(get_path())
	var save_file = FileAccess.open("user://savegame.json",FileAccess.WRITE)

	var saved_data = {}
	saved_data["scene_file_path"] = get_tree().get_current_scene().scene_file_path
	saved_data["player_global_positionX"] = player.global_position.x
	saved_data["player_global_positionY"] = player.global_position.y
	

	var json = JSON.stringify(saved_data)
	
	match FileAccess.get_open_error():
		OK:
			save_file.store_string(json)
			save_file.close()
			print("Save successful.")
		ERR_CANT_CREATE:
			print("Error: Cannot create the save file.")
		ERR_CANT_OPEN:
			print("Error: Cannot open the save file.")
		ERR_FILE_NOT_FOUND:
			print("Error: Save file not found, and could not be created.")
		_:
			print("An unknown error occurred while opening the save file.")
	

func load_game():
	var save_file = FileAccess.open("user://savegame.json", FileAccess.READ)
	load_one(save_file)
	load_two(save_file)

func load_one(save_file: FileAccess):

	match FileAccess.get_open_error():
		OK:
			var json = save_file.get_as_text()
			var saved_data = JSON.parse_string(json)
			
			
			

			if typeof(saved_data) == TYPE_DICTIONARY:
				
				if saved_data.has("scene_file_path"):
					get_tree().change_scene_to_file(saved_data["scene_file_path"])
					print ("got scene")
				else: 
					print ("Scene not found")
					
				
					
				
			else:
				print("Error parsing save file JSON.")
		
		ERR_FILE_NOT_FOUND:
			print("Save file not found.")
		_:
			print("Error opening save file: ", FileAccess.get_open_error())
			
	
func load_two(save_file: FileAccess):
	
	print ("AHHHHHH")
	match FileAccess.get_open_error():
		OK:
			var json = save_file.get_as_text()
			var saved_data = JSON.parse_string(json)
			
			save_file.close()
			

			if typeof(saved_data) == TYPE_DICTIONARY:
				
				if saved_data.has("player_global_positionX") and saved_data.has("player_global_positionY"):
					player.global_position = Vector2(saved_data["player_global_positionX"], saved_data["player_global_positionY"])
					print("Retrieved position X = %s and Y = %s" % [saved_data["player_global_positionX"], saved_data["player_global_positionY"]])
					print("Set player position X = %s and Y = %s" % [player.global_position.x, player.global_position.y])
				else:
					print("Position data not found in save file.")
					
			else:
				print("Error parsing save file JSON.")
		
		ERR_FILE_NOT_FOUND:
			print("Save file not found.")
		_:
			print("Error opening save file: ", FileAccess.get_open_error())
			

func _process(delta: float) -> void:
	print("Current player position X = %s and Y = %s" % [player.global_position.x, player.global_position.y])

It works in loading the scene, and if I open the save file or look in the Output what it prints, it shows the right position, but the player still appears in the place it was placed in the scene (if I remove the whole part that loads the scene, and only sets the position, that part works though)

Your saved_data definition is inside the save function and not global to your save script, it do not exist outside of this function, just move the line var saved_data = {} just after the @onready var player.

You also need to remove the var before this line in the load function : var saved_data = JSON.parse_string(json)
Why do you need 2 function to load your json file? By now the second load function will override the previous one…

1 Like

Thank you I will try that!

The second one I have, because in my last attempt to fix the problem, I tried to separete the Loading function into loading the scene and loading the position, but if putting it in the scene script will fix the issue, I will delete it.

When I try to start it, I now get the error "Invalid access to property or key ‘player_global_positionX’ on a base object of type ‘Dictionary’

It’s because your dictionary do not contains the given key at the moment you want it. You need to check if it exist or do the loading of your data (and the dictionary population) before your node creation. This can be done a scene before like when you enter the game from main menu or you will need to assign this information for your node later, not in the _ready function.

I changed it to

if FileAccess.file_exists("user://savegame.json"):
		if Save.saved_data.has("player_global_positionX") and Save.saved_data.has("player_global_positionY"):
			Guyman.global_position = Vector2(Save.saved_data["player_global_positionX"], Save.saved_data["player_global_positionY"])

And now i don’t get an error anymore.

But sadly that whole thing didn’t solve my problem, the player still appears in the original position, instead of the saved one, when I load. Do you maybe know why that happens?

It’s because your load occurs after the append of your node in the scene tree. The ready function of your node is called before your load function and because it’s not trigger after that, your character stay at the same place. In your save script, you define a var containing your player, you can force a call to the ready function in your player at the end of the load function. (I write from my phone and cannot give you more exemple right now)

Edit :
Call Guyman.request_ready() at the end of the load function or create a specific function

1 Like

Thank you!
I tried adding that to the ‘load_game’ function, between load_one and load_two, and at the end of ‘load_one’ and neither placement changed anything. Did I place it wrong?

I have tested your script more deeply as I got my computer this time.
There is some pretty strange part in your code.

first of all why do you get a player variable inside your Guyman node ? (here : @onready var player = Guyman.player)
Second one, I think you got other error each time you load your game since you don’t call the scene change deferred and I don’t know if you set the global node creation in the right order.

Here is the DataSaver I modified and test it work as espected

DataSaver.gd
extends Node

@onready var player = Guyman

var saved_data = {}

func _ready():
	load_game()

func save():
	
	print(get_path())
	var save_file = FileAccess.open("user://savegame.json",FileAccess.WRITE)

	saved_data = {}
	saved_data["scene_file_path"] = get_tree().get_current_scene().scene_file_path
	saved_data["player_global_positionX"] = player.global_position.x
	saved_data["player_global_positionY"] = player.global_position.y
	

	var json = JSON.stringify(saved_data)
	
	match FileAccess.get_open_error():
		OK:
			save_file.store_string(json)
			save_file.close()
			print("Save successful.")
		ERR_CANT_CREATE:
			print("Error: Cannot create the save file.")
		ERR_CANT_OPEN:
			print("Error: Cannot open the save file.")
		ERR_FILE_NOT_FOUND:
			print("Error: Save file not found, and could not be created.")
		_:
			print("An unknown error occurred while opening the save file.")
	

func load_game():
	var save_file = FileAccess.open("user://savegame.json", FileAccess.READ)
	
	match FileAccess.get_open_error():
		OK:
			var json = save_file.get_as_text()
			saved_data = JSON.parse_string(json)

			if typeof(saved_data) == TYPE_DICTIONARY:
				if saved_data.has("scene_file_path"):
					get_tree().call_deferred("change_scene_to_file", saved_data["scene_file_path"])
					call_deferred("after_scene_change_load")
					print ("Scene loaded")
				else: 
					print ("Scene not found")
			else:
				print("Error parsing save file JSON.")
		
		ERR_FILE_NOT_FOUND:
			print("Save file not found.")
		_:
			print("Error opening save file: ", FileAccess.get_open_error())
			
	
func after_scene_change_load():
	# Do whatever you want here
	if saved_data.has("player_global_positionX") and saved_data.has("player_global_positionY"):
		player.global_position = Vector2(saved_data["player_global_positionX"], saved_data["player_global_positionY"])
		print("Retrieved position X = %s and Y = %s" % [saved_data["player_global_positionX"], saved_data["player_global_positionY"]])
		print("Set player position X = %s and Y = %s" % [player.global_position.x, player.global_position.y])
	else:
		print("Position data not found in save file.")

My global node are set in this order :
GlobalNodes

and my Guyman node is just :

Thank you so much for your help!
I tried your modified script, and sadly I still have that same issue of the player not appearing at the saved position, even if the printed text in the debugger says that the right position is loaded.

Could it be that, because I have a separate scene where the player node is in and I drag it into the level scene, that it’s somehow set as the ‘proper’ place for it that can’t be changed?

More likely you’re applying the saved position before the object is fully constructed and it’s getting overwritten.

Ohh, is there a way for the load function to wait for everything else to be loaded?

There’s lots of ways. I don’t know that you need to do that though. My recommendation would be in your _ready() function for Gunman you load the value. If you get back null, use the default. Otherwise, use the value you just loaded.

To answer your question though, every object has a ready signal that fires after an object is constructed.

func _ready() -> void:
	ready.connect(_on_ready)


func _on_ready() -> void:
	#Do stuff here
1 Like

I like to have my main game level node, when it is ready, to fire off initialisation calls to child nodes, that in turn can initialise their own children. This means I can inject node references throughout my scenes knowing they are now all ready.

func _ready() -> void:
	CAMERA.initialise()
	UI.initialise()
	PLAYER.initialise()
	CHUNK_MANAGER.initialise()
	TILE_MANAGER.initialise()
	PLANT_MANAGER.initialise()
	CREATURE_MANAGER.initialise()

These functions only contain the stuff that I cannot do in the _ready function directly. For instance I cannot generate my plants and creatures until the tilemap is ready, and I cannot do the tilemap until I know which chunk I am doing, and I cannot do the chunk generation until the player is ready etc etc.

2 Likes

I do something similar with my StateMachines

Each State has an _activate_state() method. The StateMachine cycles through all the States after it is ready and runs through them to do all the initialization.

# Sets up every [State] for this [StateMachine], and monitors any [State] being
# added or removed to the machine by being added or removed as child nodes
# of this [StateMachine] instance.
func _ready() -> void:
	# Keep intitalization from happening until the parent and all its dependants are constructed.
	# This prevents race conditions from happening where a State needs to reference things that
	# do not exist yet.
	subject.ready.connect(_on_ready)

func _on_ready() -> void:
	for state in get_children():
		if state is State:
			state._activate_state()
	self.connect("child_entered_tree", _on_state_added)
	self.connect("child_exiting_tree", _on_state_removed)

This way I am guaranteed to have everything created. It then also monitors any States entering or exiting the tree after it is setup.

2 Likes

P.S. I don’t put anything in the _ready() function for States as an intentional habit. This is because a lot of them have _process() or _physics_process() functions that I don’t want running until everything is setup. So I disable them in the base State _ready() function and re-enable them in _activate_state().

2 Likes

Thank you! I tried that by putting

func _ready():
	if Save.saved_data.has("player_global_positionX") and Save.saved_data.has("player_global_positionY"):
		player.global_position = Vector2(Save.saved_data["player_global_positionX"], Save.saved_data["player_global_positionY"])
		print("Retrieved position X = %s and Y = %s" % [Save.saved_data["player_global_positionX"], Save.saved_data["player_global_positionY"]])
		print("Set player position X = %s and Y = %s" % [player.global_position.x, player.global_position.y])
	else:
		print("Position data not found in save file.")

into the Guyman script. Now I have a problem I used to have, at the beginning , where when I save and load, the player appears in a wildly different position, than the one I saved in, often outside anything that is in the scene (somewhere where there is only the gray background or right below all the other things)

I had that problem, when I put the save system into the Guyman script, and I think it was cause the script is global.

Before we talk about what’s going on with your player position, we should talk about naming conventions. It took some time to parse your code. Save indicates saving something, but you also apparently are using it for loading? At first it looked like you were saving the data when you load it. I’d recommend you come up with a different name for Save such as SaveLoad or Disk.

Here’s some modified code. First, I separated the loading of the data from the testing and using of the data. Second, I reduced the number of calls to the data. A retrieval of data into a variable takes virtually the same amount of time as seeing if it’s there. In this case, if it’s there you want to use it, so just load it.

When data is loaded, if it doesn’t exist, the vaariable will be assigned the value of null, and an if statement interprets that as false. So if the pos_x isn’t null, it will be assigned. Otherwise, it won’t. This is a good way of handling nulls so they don’t cause you problems later.

You’ll also notice there’s only one print statement. This is because once you assign the values to a variable and print that out - you know the value of the variable isn’t going to change. Plus it can be used to assign to your player.

You’ll also notice my use of constants (keyword const) at the top of the file. This is to eliminate what are know as “magic strings”. Whenever a string (or number) doesn’t change, make it const. That prevents a lot of frustrating typo errors that are hard to track down.

The load_player_position() name makes it clear what this script is doing, regardless of the named autoload Save and makes your code easier to read.

The last thing to mention is I added a call to what I think is your call to load the data. You may be doing this somewhere else, but for now - this ensures the data is loaded.

const SAVED_PLAYER_POSITION_X = "player_global_positionX"
const SAVED_PLAYER_POSITION_Y = "player_global_positionY"


func _ready():
	var loaded_position = load_player_position()
	if loaded_position != Vector.ZERO:
		player.global_position = loaded_position
	else:
		print("Position data not found in save file.")


func load_player_position() -> Vector2:
	Save.load_game() #We want to make sure the data is loaded. If you are doing this somewhere else, you do not need it her.
	var return_vector := Vector2.ZERO #This is our error vector. If it's possible that (0,0) is a valid position, use Vector2.INF instead
	var pos_x = Save.saved_data[SAVED_PLAYER_POSITION_X]
	if pos_x:
		return_vector.x = pos_x
	var pos_y = Save.saved_data[SAVED_PLAYER_POSITION_Y]
	if pos_y:
		return_vector.y = pos_y
	print("Retrieved position X = %s and Y = %s" % [return_vector.x, return_vector.y])
	return return_vector

So, now while you said your character is gettign teleported wildly, you didn’t say where. So here’s some questions for you:

  1. What was the player’s position when you saved it?
  2. What was the player’s global_position when you saved it?
  3. What was the player’s position before the load?
  4. What was the player’s global_position before the load?
  5. What was the player’s position after the load?
  6. What was the player’s global_position after the load?
  7. Has the player changed order or parent in the node hierarchy so that the relative position of the player might have changed?
  8. Are you sure you are saving global_position and not position?
  9. Are you loading into the exact same level with nothing changed as when the save happened, or has anything about the level changed?