Help on Save system Based on Queble's System

Godot Version

4.7

Question

I was following this great tutorial on how to create a save system by Queble (https://www.youtube.com/watch?v=OXp25cZDpEw) and it worked beautifully, but now I’m having issues making sure it connects and works with my game.

I’m having trouble figuring out how to access the load and save functions from outside the map (for menus and special maps that needs different scripts) and have it be all just 1 save and not per level like he has it in his video.
I want for my game for the save to just update per level adding the new info and saving which level the person is on. All the properties saving is working perfectly, I just need help with making sure it’s all just 1 save, saving where the player is and having access to it on my global scripts for the menus and maps.
Currently I have the save scripts as Queble and it is working just like his I just need help with these small changes and connections.

This is the Saver_node script:

@tool
class_name SaverNode extends Node

const SAVER_NODE_GROUP : String = "saver_node"

@export var properties_to_save: Array[String] = []

var suggested_properties : Array[String] = []


func _ready() -> void :
	add_to_group(SAVER_NODE_GROUP)
	if Engine.is_editor_hint():
		_update_property_list()

func _update_property_list()-> void:
	var parent = get_parent()
	if not parent:
		return
		
	suggested_properties.clear()
	var all_props = parent.get_property_list()
	
	for p in all_props:
		var pname = p.name
		suggested_properties.append(pname)
		
	notify_property_list_changed()


func _validate_property(property: Dictionary)-> void:
	if property.name == "properties_to_save":
		var options = ",".join(suggested_properties)
		property.hint = PROPERTY_HINT_TYPE_STRING
		property.hint_string = "%d/%d:%s" % [TYPE_STRING, PROPERTY_HINT_ENUM, options]


func get_save_dict()-> Dictionary:
	var parent = get_parent()
	var node_data = {}
	for prop in properties_to_save:
		print("Saving property:", prop)
		if prop in parent:
			node_data[prop] = parent.get(prop)
	return node_data


func apply_save_dict(node_data: Dictionary)-> void:
	var parent = get_parent()
	for prop in node_data:
		if prop in parent:
			parent.set(prop, node_data[prop])

This is the Level_save_data script:

class_name LevelSaveData extends Resource

@export var data: Dictionary = {}

This is my base_level script:

class_name Level extends Node2D

const LEVEL_SAVES_FOLDER : String = "user://level_saves/"

enum LevelIDs{
	NONE = 0,
	LEVEL_1 = 1,
	LEVEL_2 = 2,
	LEVEL_3 = 3,
	OFFICE_LEVEL_1 = 4,
	LEVEL_4 = 5,
	LEVEL_5 = 6,
	LEVEL_6 = 7,
	LORE_LEVEL = 8,
	LEVEL_7 = 9,
	LEVEL_8 = 10,
	FINAL_LEVEL = 11,
}

@export var level_id : LevelIDs


func _ready() -> void:
	GlobalAudioManager.base_level_music()
	
	DirAccess.make_dir_absolute(LEVEL_SAVES_FOLDER)
	
	load_level_data()
	save_level_data()
	
	if SceneManager.spawn_door_tag != null:
		_on_level_spawn(SceneManager.spawn_door_tag)
	
func get_level_save_file_path()-> String:
	return LEVEL_SAVES_FOLDER + str("level_save_", level_id) + ".tres"

func save_level_data()-> void:
	var new_save = LevelSaveData.new()
	
	for saver: SaverNode in get_tree().get_nodes_in_group(SaverNode.SAVER_NODE_GROUP):
		var node_path = get_path_to(saver)
		new_save.data[node_path] = saver.get_save_dict()
		
	ResourceSaver.save(new_save, get_level_save_file_path())
	print("Saved Level data to",get_level_save_file_path())
	
func load_level_data()-> void:
	if not FileAccess.file_exists(get_level_save_file_path()):
		return
	
	print("loading map data from", get_level_save_file_path())
	var loaded_save = ResourceLoader.load(get_level_save_file_path()) as LevelSaveData
	
	for path in loaded_save.data:
		var saver_node = get_node_or_null(path)
		if saver_node:
			saver_node.apply_save_dict(loaded_save.data[path])




func _on_level_spawn(destination_tag: String):
	var door_path = "Door_" + destination_tag
	var door = get_node(door_path) as Door 
	SceneManager.trigger_player_spawn(door.spawn.global_position, door.spawn_direction)

This is the game manager that controls the menus (cannot access load and save from here):

extends Node

var main_menu = preload("res://UI/MainMenu.tscn")
var pause_menu = preload("res://UI/PauseMenu.tscn")


# Called when the node enters the scene tree for the first time.
func _ready():
	RenderingServer.set_default_clear_color(Color(0,0,0,1))


func _toggle_fullscreen(toggle):
	if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_WINDOWED:
		DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
	else:
		DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)


func start_game():
	if get_tree().paused:
		
		continue_game()
		return

	
	SceneManager.transiton_to_scene("Intro Cut Scene", "0")
	
	

func exit_game():

	get_tree().quit()



func pause_game():
	get_tree().paused = true


	var pause_menu_instance = pause_menu.instantiate()
	get_tree().get_root().add_child(pause_menu_instance)


func continue_game():
	get_tree().paused = false
	


func backtomain_menu():
	var main_menu_instance = main_menu.instantiate()
	get_tree().get_root().add_child(main_menu_instance)

And this is my scene manager that controls the changing scenes (cannot access load and save from here):

extends Node

var scene_transition_screen = preload("res://UI/screen_transition/scene_transition_screen.tscn")

var scenes : Dictionary = {"Level 1": "res://Levels/level_1.tscn",
							"Level 2": "res://Levels/level_2.tscn",
							"Level 3": "res://Levels/level_3.tscn",
							"Level 4": "res://Levels/level_4.tscn",
							"Level 5": "res://Levels/level_5.tscn",
							"Level 6": "res://Levels/level_6.tscn",
							"Level 7": "res://Levels/level_7.tscn",
							"Level 8": "res://Levels/level_8.tscn",
							"Final Level": "res://Levels/Final_level.tscn",
							"Lore Level": "res://Levels/lorelevel.tscn",
							"office Level 1":"res://Levels/office_level_1tscn.tscn",
							"Credits":"res://UI/credits.tscn",
							"Final Cut Scene": "",
							"Intro Cut Scene": "res://UI/CutScenes/beginning_cut_scene.tscn"
							}

						
var level : String

signal on_trigger_player_spawn

var spawn_door_tag 

func transiton_to_scene(level : String, destination_tag):
	var scene_path : String = scenes.get(level)
	
	
	
	
	if scene_path != null:
		var scene_transition_screen_instance = scene_transition_screen.instantiate()
		spawn_door_tag = destination_tag
		
		get_tree().get_root().add_child(scene_transition_screen_instance)
		await get_tree().create_timer(2.5).timeout
		get_tree().change_scene_to_file(scene_path)
		scene_transition_screen_instance.queue_free()
		
		

func trigger_player_spawn(position: Vector2, direction: String):
	on_trigger_player_spawn.emit(position,direction)



# Called when the node enters the scene tree for the first time.
func _ready():
	pass

Hey!

So if I understand correctly, you’re just trying to figure out how to access the save/load system from outside the level?


This would depend on when you need to access it.

If you’re accessing the level while it exists in the scene tree, and since you’ll only have one level active at a time, you could use a static variable for this.

In your level.gd script, add:

static var current_level: Level

func _init() -> void:
   current_level = self

This way, you could then access the current level from any node like:

var current_level: Level = Level.current_level

Let me know if that helps, or if I misunderstood the question :slight_smile:

Thank you so so much for responding, it is a honor.

From my understanding it was already a Static variable and I needed it to be an instance (got an error that said that), because my main issue with accessing it is accessing it on scenes that do not have the base level node in them, such as the main menu and global manager scripts (game manager and scene manager).

Also I would love to figure out a way to save 1 continuous save that updates per level, instead of having 1 save per level. That way when the game begins it would be a question of loading in the saved game or just creating a new one and updating that save with player’s progression. (Still figuring this out, but would welcome some light because I am very new, this is my first save system)

Ah I see!

So you can’t access data on a node that doesn’t exist in the scene tree, but you could obviously still load and edit the save data resource without the level being present.

It might help if you could share what you’re looking to edit in the save data from outside the level?


And yes, creating one save file for all levels can also work, although this would mean that you’d have a much larger file (obviously depending on how many levels are in your game).
The size won’t really matter too much though, since it’s small either way.

So to do this, I would just add an additional layer to your data dictionary in the LevelSaveData class.

Basically your structure would turn into:

data {
   "level_1": {
      // save data
      },
   "level_2": {
      // save data
      },
}

You would only need to edit this strucutre in the get_save_dict() and apply_save_dict() functions

I am very sorry about how many questions and help I am needing and thank you so much for your patience. My game is very small just 11 levels and all of them very small too, so the file size is probably not gonna be an issue (hopefully).

How do I add that layer of data? Here? (this is giving error so probs not)

class_name LevelSaveData extends Resource

@export var data: Dictionary = {"level_1": {// save data},"level_2": {// save data},}

or this?

func get_save_dict()-> Dictionary:
	var parent = get_parent()
	var node_data = {}
	var data = {"level_1": {// save data},"level_2": {// save data},}
	for prop in properties_to_save:
		print("Saving property:", prop)
		if prop in parent:
			node_data[prop] = parent.get(prop)
	return node_data


func apply_save_dict(node_data: Dictionary)-> void:
	var parent = get_parent()
	for prop in node_data:
		if prop in parent:
			parent.set(prop, node_data[prop])

again very sorry
And as for what I am trying to edit the save and load is that I want to add into my GameManager Start_game func the capability to go into the game and create a new save or load a save if it already exists. Also as a quality of life thing I want to make a new game button that overwrites that current save to make a new one in case the player wants to start over.

This is my main menu:

and gamemanager script:

extends Node

var main_menu = preload("res://UI/MainMenu.tscn")
var pause_menu = preload("res://UI/PauseMenu.tscn")



# Called when the node enters the scene tree for the first time.
func _ready():
	RenderingServer.set_default_clear_color(Color(0,0,0,1))
	

func _toggle_fullscreen(toggle):
	if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_WINDOWED:
		DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
	else:
		DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)


func start_game():
	if get_tree().paused:
		
		continue_game()
		return

	
	SceneManager.transiton_to_scene("Intro Cut Scene", "0")
	

func exit_game():

	get_tree().quit()



func pause_game():
	get_tree().paused = true


	var pause_menu_instance = pause_menu.instantiate()
	get_tree().get_root().add_child(pause_menu_instance)


func continue_game():
	get_tree().paused = false
	

func backtomain_menu():
	var main_menu_instance = main_menu.instantiate()
	get_tree().get_root().add_child(main_menu_instance)

No worries!

So the code I sent was more of an example of the data structure, anything after the // is a comment, so it will throw an error if you actually put it inside a dictionary.

If you’re simply trying to create save slots that you can add/remove dynamically, then you could actually just keep the system as is in the video, and put the different save slots into different folders.
This would keep things simple without changing your code too much.


func get_level_save_file_path()-> String:
	return LEVEL_SAVES_FOLDER + str("level_save_", level_id) + ".tres"

The function above (in your level script) figures out where the level save file is stored.
Currently, it’s something like user://level_saves/level_save_0.tres, so you’d basically want to inject another part of the save path before the file name.

You could add a save_slot variable to your Global script, which could be set through the main menu (when you select a save).

Then, you could edit this function to also include the specific save slot in the file path:

func get_level_save_file_path()-> String:
	return LEVEL_SAVES_FOLDER + str(Global.save_slot) + str("/level_save_", level_id) + ".tres"

This would allow you to have multiple “saves” on a single computer.


To read if a save exists, or delete an entire “slot”, you can use the built-in DirAccess functions (docs: DirAccess — Godot Engine (4.3) documentation in English)

It’s also important to note that the save/load system will create new save files automatically (if they don’t exist), so you don’t need to explicitly call that function.
You will probably only need to worry about deleting the entire saves/slot directory if a user wants to clear their slot

Okay I am so close it hurts.

I decided to not have the slots because it’s not really needed just want one save at a time for now.

I pretty much kept it as you have in your video, added a SaveNode to my levels to save the Level_id and Level path to get where the player is, but now I can´t figure out how to get the latest save so the game loads into the right level the player was last on. I can use the DirAccess to get the file, but how do I always get the latest file.

Once again sorry and thank you so much <3

Just an update, I’m even closer now because I am able to load in the right level and door. The current issue is just that the information in the save such as Keys, health, collectables etc are not coming with the save to the UI and managers, but it is being saved and the information is in the player.

This is the save file:

[gd_resource type="Resource" script_class="LevelSaveData" load_steps=2 format=3]

[ext_resource type="Script" path="res://Scripts/Level_save_data.gd" id="1_o8wt0"]

[resource]
script = ExtResource("1_o8wt0")
data = {
NodePath("CharacterBody2D/SaverNode"): {
"global_position": Vector2(0, 0),
"health_for_save": 1,
"inventory_for_save": {
"Key1": "22",
"Key2": "33"
},
"xeno_for_save": 1
},
NodePath("SaverNode"): {
"level_id": 3,
"scene_file_path": "res://Levels/level_3.tscn"
}
}

and this is the right information stored in player:

but here’s the empty inventory manager:

The collectable manager dos show the 1 coin:

health manager not updated with the right health:

and the game’s UI is not updated:

This is the GameScreen script:

extends CanvasLayer

@onready var collectible_label = $MarginContainer/VBoxContainer/HBoxContainer/CollectibleLabel


# Called when the node enters the scene tree for the first time.
func _ready():
	CollectibleManager.on_collectible_award_received.connect(on_collectible_award_received)
	CollectibleManager.get_xeno_amount()
	#GameManager.base_level_music()
	


func on_collectible_award_received( total_award: int):
	collectible_label.text = str(total_award)
	

func _physics_process(delta):
	if Input.is_action_just_pressed("pause"):
		GameManager.pause_game()

func _on_pause_texture_button_pressed():
	GameManager.pause_game()

and these are the variables I’m using on the player to set the saves for the SaverNode:

#for save 
@export var health_for_save : int = HealthManager.current_health
@export var xeno_for_save : int = CollectibleManager.total_award_amount
@export var inventory_for_save : Dictionary = InventoryManager.inventory

I’m fully brainless for today, but I think the next steps is that I’m going to try to add the savernode to the gamescreen and see if that connects better with the UI and information. Not sure if I’m going to keep the info in the player or not.

oh and this is the current GameManager script that load the game:

func start_game():
	
	#if get_tree().paused:
	if DirAccess.dir_exists_absolute(Level.LEVEL_SAVES_FOLDER):
		print(Level.LEVEL_SAVES_FOLDER)
		continue_game()
		return

	if not DirAccess.dir_exists_absolute(Level.LEVEL_SAVES_FOLDER):
		print(Level.LEVEL_SAVES_FOLDER)
		SceneManager.transiton_to_scene("Intro Cut Scene", "0")


func continue_game():
	var all_saves: PackedStringArray = DirAccess.get_files_at(Level.LEVEL_SAVES_FOLDER)
	var number_saves = Array(all_saves).size()
	print(number_saves)
	var latest_save := str("level_save_", number_saves) + ".tres"
	print(latest_save)
	var current_save_level := str("Level ", number_saves)
	var current_save_door := str(number_saves - 1)
	print(current_save_door)
	FileAccess.open(latest_save, FileAccess.READ)
	SceneManager.transiton_to_scene(current_save_level, current_save_door)
	get_tree().paused = false

Your latest update points to the handoff after loading. This line initializes a player variable; it doesn’t keep it linked to the manager:

@export var health_for_save: int = HealthManager.current_health

When SaverNode restores health_for_save, HealthManager.current_health stays unchanged. After all player properties have loaded, copy them into the managers:

HealthManager.current_health = health_for_save
CollectibleManager.total_award_amount = xeno_for_save
InventoryManager.inventory = inventory_for_save.duplicate(true)

Then notify or refresh the UI using those managers’ existing signals/methods. Before saving, do the reverse: copy current manager values into the player’s save fields so health/coins aren’t stale.

In GameScreen, get_xeno_amount() also discards the return value. If it returns the current total, initialize the label with:

on_collectible_award_received(CollectibleManager.get_xeno_amount())

Run that after connecting the signal, and refresh again when loading finishes if the HUD was already present. Connecting a signal won’t replay an earlier update.

Could you share the manager scripts? That would let us wire the refresh to the actual signals.

Hello there, thanks for all the support in this.

Once again it¡s so close it HURTS but not there yet.

Currently Health is updating beautifully and the inventory as well. Where I am having trouble is the UI of the collectables. In the manager we have all the right values, but the UI lies. It starts the save correct but as soon as the player picks up another collectable it resets to one in value (while the dictionary stay correct.

This is from the player is connecting to the managers (player is not global but has class_name player):

#for save 
@export var health_for_save : int = HealthManager.current_health
@export var xeno_for_save : Dictionary = CollectibleManager.xeno_collected
@export var inventory_for_save : Dictionary = InventoryManager.inventory

func _on_spawn(position: Vector2, direction: String):
	global_position = position
	
	current_state = State.Run
	HealthManager.change_health(health_for_save)
	InventoryManager.inventory = inventory_for_save.duplicate(true)
	CollectibleManager.xeno_collected = xeno_for_save.duplicate(true)
	print("From player xeno.size" , xeno_for_save.size())
	CollectibleManager.on_collectible_award_received.emit(xeno_for_save.size())

This is the collectable manager (global script):

extends Node

var xeno_collected : Dictionary = {}

static var total_award_amount : int

signal on_collectible_award_received


func give_pickup_award(collectible_award : int):
	total_award_amount += collectible_award
	
	on_collectible_award_received.emit(total_award_amount)
	
func get_xeno_amount():
	on_collectible_award_received.emit(total_award_amount)
	print("xeno collected" ,xeno_collected.size())
	print("Total award" , total_award_amount)

This is the health manager (global script)

extends Node

var max_health : int = 3
var current_health : int 

signal on_health_changed

func _ready():
	
	current_health = max_health

func get_player_health():
	on_health_changed.emit(current_health)

func decrease_health(health_amount : int):
	current_health -= health_amount
	
	if current_health < 0:
		current_health = 0
	
	on_health_changed.emit(current_health)

func increase_health(health_amount : int):
	current_health += health_amount
	
	if current_health > max_health:
		current_health = max_health
	
	on_health_changed.emit(current_health)

func change_health(health_amount : int):
	health_amount == current_health

	if current_health > max_health:
		current_health = max_health
		
	if health_amount < max_health:
		current_health = health_amount
	
	
	on_health_changed.emit(current_health)

This is the inventory manager (global script)

extends Node

var inventory : Dictionary = {}

var no_value = null

var door_block = 0


func add_to_inventory(type : String, value : String ):
	inventory[type] = value
	

func add_door_block():
	door_block += 1


func remove_from_inventory(type: String):
	inventory[type] = no_value
	

func has_inventory_item(value : String) -> bool:
	if value == null:
		return false
	
	var item = inventory.find_key(value)
	
	if item:
		return true
	
	return false

This is the game screen script that controls the UI (not global)

extends CanvasLayer

@onready var collectible_label = $MarginContainer/VBoxContainer/HBoxContainer/CollectibleLabel


# Called when the node enters the scene tree for the first time.
func _ready():
	CollectibleManager.on_collectible_award_received.connect(on_collectible_award_received)
	CollectibleManager.get_xeno_amount()
	print("from label get xeno amount", CollectibleManager.get_xeno_amount())
	#GameManager.base_level_music()

func on_collectible_award_received(total_award: int):
	collectible_label.text = str(total_award)
	print("from label", str(total_award))

func _physics_process(delta):
	if Input.is_action_just_pressed("pause"):
		GameManager.pause_game()

func _on_pause_texture_button_pressed():
	GameManager.pause_game()

This is the result of the print on entering the save:

I’m currently suspecting the label script is the biggest culprit as to the error, but still need to investigate more.

finally all done with connections just needed to connect

CollectibleManager.total_award_amount = xeno_for_save.size()

Now I just need to figure out how to reset the saves.

turns out you can´t delete a folder if it has items inside so I had to first delete all possible saves inside the folder then the folder.

Not pretty but it works <3

func new_save():
	if DirAccess.dir_exists_absolute(Level.LEVEL_SAVES_FOLDER):
		DirAccess.open("user://level_saves/")
		DirAccess.remove_absolute("user://level_saves/level_save_1.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_2.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_3.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_4.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_5.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_6.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_7.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_8.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_9.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_10.tres")
		DirAccess.remove_absolute("user://level_saves/level_save_11.tres")
		DirAccess.remove_absolute("user://level_saves")
		var err = DirAccess.remove_absolute("user://level_saves/")
		if err == OK:
			print("Success")
		else:
			print("Failed with error code: ", err)
		
		
		queue_free()
		SceneManager.transiton_to_scene("Intro Cut Scene", "0")
		return

	if not DirAccess.dir_exists_absolute(Level.LEVEL_SAVES_FOLDER):
		
		SceneManager.transiton_to_scene("Intro Cut Scene", "0")