Getting line is .Tres save resource in script for level save

Godot Version

4.4

Question

I’m currently reworking my level save system so it is more streamlined and works better with my game. It all save always on a level_save.tres and in that it stores all the info I need, there’s only one issue I’m stuck as to how to get the player level information. For my scene manager I need the Level the player is on and the door tag to spawn the player next to it. That info is in the level_save.tres, but after reading the docs for a while I have not figured out how to get that info off the file.

This is the current save file: (I need that level_id and scene_file_path. )

da[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_ce52g"]

[resource]
script = ExtResource("1_ce52g")
data = {
NodePath("CharacterBody2D/SaverNode"): {
"health_for_save": 2,
"inventory_for_save": {
"Key1": "22",
"Key2": "33",
"Officekey": "42",
"TempKey1": null
},
"xeno_for_save": {
"Xeno-coin 1": true,
"Xeno-coin2": true
}
},
NodePath("SaverNode"): {
"level_id": 5,
"scene_file_path": "res://Levels/level_4.tscn"
}
}

Seems like you have the level information, specifically the scene_file_path. So do you only need to add a node path to the level’s door and/or where you want to spawn the player?

Maybe you are trying to store too much complex data instead of base types? For your data property the dictionary should only store values that do not have a .duplicate() function. Nodes for example will not store exactly right.

But it’s hard to tell, sharing your save/load script would help more.

These are the save and load script:

func get_level_save_file_path()-> String:
	return LEVEL_SAVES_FOLDER + str("level_save") + ".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])

the savernode 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])

All I need is to access the property Level_id (in this case the “5” int that connects to my level 5) in my game manager (that is a global script and not connected to any of these scripts) from the save. I’ve tried getting access via the packedstringarray and directly from the file but I cannot find a way.

I need that level id because to transition to the right scene I need the string that connect to the level and the door such as: SceneManager.transiton_to_scene(“Level 1”, “2”) → this would transition me to my level 1 and door 2.

This way if I am able to get the saved level_id of the last level the person was in I am able to transition them into the right level and door (doors are always named 1 less then the level name) they were in upon loading the game in the continue game.

I know it may seem complex, but it is necessary for my game, pls I just need to get that level id.

You’ll have to modify your load function, your current one only sets properties, it never calls transition_to_scene. Not all data can store and load as a base type, some data has consequences that must be replicated.

Assuming your new_save.data is a dictionary you could add it as a special key and read from that special key just as well. If you are using current_scene/change_scene_to_file properly then this may work for you.

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()

	# Store level path, and/or your id
	new_save.data["level_path"] = get_tree().current_scene.scene_file_path
	new_save.data["level_id"] = get_tree().current_scene.level_id

	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

	# Loading scene first
	var level_path: String = loaded_save.data["level_path"]
	var level_id: int = loaded_save.data["level_path"]
	SceneManager.transition_to_scene(level_path, level_id)

	# then path-based node data
	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])

Hey thank you so much for all the help. I like your method, but how can I call the load function on the game manager to continue game if the load and save functions are on the level scene (not global)?

I’m honestly starting to think I’m gonna need to overhaul all of this save system and alter it to be more global or at least based on a global save manager, because currently it is giving me so many issues. I have made it work in most things, but any time I try to expand it, it bugs and breaks.

Open to all opinions and ideas because creating this save system is honestly breaking me.

Hey same dev, just different account. I like your method I’m just finding one issue: I can’t access the load game function on my game manager to call this. That’s why I was trying to get the info directly from the save file. The save and load function are in the level scene which is not global. How would I do this?

This looks like a consistent path, it seems like you could call this load function from anywhere. Why couldn’t these be static functions? Why not call them from a global/your game manager?

Yes, this is the point where the save coordinator should become an autoload. The level scene should provide state to save, but it should not own the only function capable of loading the game; otherwise the main menu cannot continue into a level that does not exist yet.

Also, the sample in the previous reply has a typo:


var level_id: int = loaded_save.data["level_path"]

That should read loaded_save.data["level_id"].

I would separate metadata from scene-node data:


new_save.data = {
    "meta": {
        "scene_path": get_tree().current_scene.scene_file_path,
        "level_id": current_level_id,
        "door_id": current_door_id,
    },
    "nodes": {},
}

var scene := get_tree().current_scene
for saver: SaverNode in get_tree().get_nodes_in_group(SaverNode.SAVER_NODE_GROUP):
    var path := scene.get_path_to(saver)
    new_save.data["nodes"][path] = saver.get_save_dict()

Then put continue_game() in a SaveManager autoload:


func continue_game() -> void:
    var save := ResourceLoader.load(get_level_save_file_path()) as LevelSaveData
    if save == null:
        return

    var meta: Dictionary = save.data.get("meta", {})
    SceneManager.transition_to_scene(
        meta.get("scene_path", ""),
        meta.get("door_id", "")
    )

    await SceneManager.transition_finished

    var scene := get_tree().current_scene
    var node_data: Dictionary = save.data.get("nodes", {})

    for path in node_data:
        var saver := scene.get_node_or_null(path)
        if saver is SaverNode:
            saver.apply_save_dict(node_data[path])

Have SceneManager emit transition_finished only after the new level is in the tree and ready. That gives you the required order: read metadata → change level → wait for it → restore its nodes. Your Continue button can now call SaveManager.continue_game() from anywhere.

I run CodingQuests. The related Save + Load System develops the same central-manager/persist-group idea; the first four lessons are free and the rest require membership. It is architectural guidance rather than a drop-in replacement for your current Resource format.

Thank you all so much! I can finally sleep easy that the system is working (need to work on my level naming so it matches the level ids but nothing too complicated).

I’m still going to try to use this to save on the checkpoints in the levels as well, but that’s a challenge for a another day.

For reference so anyone want to know how it turned out here it is:

My Scene_manager global (still needs to be reworked with the level names):

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
signal transition_finished

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()
		transition_finished.emit()
		

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

My game_manager:

extends Node

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

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

var reset_xeno : int = 0


# 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:
	if DirAccess.dir_exists_absolute(LEVEL_SAVES_FOLDER):
		
		continue_game()
		return

	if not DirAccess.dir_exists_absolute(LEVEL_SAVES_FOLDER):
		
		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():
	var save := ResourceLoader.load(get_level_save_file_path()) as LevelSaveData
	print_debug(save)
	if save == null:
		return
		
	var meta : Dictionary = save.data.get("meta", {})
	print_debug(meta)
	#var level_name = str("Level " + meta.get("level_id", ""))
	#var door_id = meta.get("door_id", "")
	
	print_debug("Level " + str((meta.get("level_id"))))
	print_debug(meta.get("door_id"))
	
	SceneManager.transiton_to_scene("Level " + str((meta.get("level_id"))),str(meta.get("door_id")))
	
	#await  SceneManager.transition_finished
	print_debug(save.data.get("node_data", {}))
	var scene := get_tree().current_scene
	var node_data: Dictionary = save.data.get("node_data", {})
	for path in node_data:
		var saver := scene.get_node_or_null(path)
		if saver is SaverNode:
			saver.apply_save_dict(node_data[path])
		
	get_tree().paused = false
	
func new_save():
	if DirAccess.dir_exists_absolute(LEVEL_SAVES_FOLDER):
		DirAccess.open("user://level_saves/")
		DirAccess.remove_absolute("user://level_save")
		
		CollectibleManager.total_award_amount = reset_xeno
		CollectibleManager.xeno_collected.clear()
		InventoryManager.inventory.clear()
		get_tree().paused = false
		SceneManager.transiton_to_scene("Intro Cut Scene", "0")
		return

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

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

func get_level_save_file_path()-> String:
	return LEVEL_SAVES_FOLDER + str("level_save") + ".tres"

func save_level_data()-> void:
	var new_save = LevelSaveData.new()
	new_save.data = {
	"meta": {
		"scene_path": get_tree().current_scene.scene_file_path,
		"level_id": get_tree().current_scene.level_id,
		"door_id": get_tree().current_scene.level_id - 1,
	},
	"node_data": {},
	}
	for saver: SaverNode in get_tree().get_nodes_in_group(SaverNode.SAVER_NODE_GROUP):
		var path = get_path_to(saver)
		new_save.data["node_data"][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:
	var save := ResourceLoader.load(get_level_save_file_path()) as LevelSaveData
	print_debug(save)
	if save == null:
		print_debug("no save")
		return
		
	var meta : Dictionary = save.data.get("meta", {})
	print_debug(meta)
	#var level_name = str("Level " + meta.get("level_id", ""))
	#var door_id = meta.get("door_id", "")

	#await  SceneManager.transition_finished
	print_debug(save.data.get("node_data", {}))
	var scene := get_tree().current_scene
	var node_data: Dictionary = save.data.get("node_data", {})
	for path in node_data:
		var saver := scene.get_node_or_null(path)
		if saver is SaverNode:
			saver.apply_save_dict(node_data[path])

My base level scene:

class_name Level extends Node2D



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()
	

	GameManager.load_level_data()
	
	GameManager.save_level_data()
	
	if SceneManager.spawn_door_tag != null:
		_on_level_spawn(SceneManager.spawn_door_tag)
	
	

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)

My player:

class_name player
extends CharacterBody2D


signal respawned()

var bullet = preload("res://Player/bullet.tscn")
var player_death_effect = preload("res://Player/player_death_effect/player_death_effect.tscn")


@onready var muzzle : Marker2D = $Muzzle
#@onready var hit_animation_player = $HitAnimationPlayer
@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var jump_sound = $Sounds/Jump
@onready var run_sound = $Sounds/Run
@onready var death_sound = $Sounds/Death
@onready var damage_sound = $Sounds/Damage
@onready var respawn_sound = $Sounds/Respawn
@onready var actionable_finder = $ActionableFinder


const GRAVITY = 1000
const SPEED = 1000
@export var speed : int = SPEED
@export var max_horizontal_speed: int = 300
@export var slow_down_speed : int = 1500


@export var jump : int = -300
@export var jump_horizontal_speed : int = 1000
@export var max_jump_horizontal_speed : int = 300
@export var jump_height :  float = -250
#change value below for multiple jumps
@export var jump_count : int = 1 
@export var max_jump_count : int = 2
@export var character_body_2d : CharacterBody2D


@export var knockbackPower : int = 500
@export var enemy_position : Vector2

@export var dialogue_resource : DialogueResource 
@export var dialogu_start : String = "Health_states"


#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



enum State {Idle, Run, Jump, Shoot}

var current_state : State
var muzzle_position
var current_jump_count : int
var character_sprite : Sprite2D
var coyote_jump : bool
var is_dead : bool
var checkpoint_manager


func _ready():
	#current_state = State.Idle
	
	
	muzzle_position = muzzle.position
	checkpoint_manager = get_parent().get_node("CheckpointManager")
	SceneManager.on_trigger_player_spawn.connect(_on_spawn)
	
	
	
	
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)
	
	CollectibleManager.on_collectible_award_received.emit(xeno_for_save.size())
	CollectibleManager.total_award_amount = xeno_for_save.size()
	


func _physics_process(delta: float):
	player_falling(delta)
	player_idle(delta)
	player_run(delta)
	player_jump(delta)
	
	player_muzzle_position()
	player_shoting(delta)
	move_and_slide()
	player_animations()
	
	
	
	#print("State:", State.keys()[current_state])


func player_falling(delta: float):
	if !is_on_floor():
		velocity.y += GRAVITY * delta

func player_idle(delta: float):
	var direction = Input_movement()
	if is_on_floor() and direction == 0:
		current_state = State.Idle
		


func player_run(delta: float):
	var direction = Input_movement()
	
	
	if direction:
		velocity.x += direction * speed * delta
		velocity.x = clamp(velocity.x, -max_horizontal_speed, max_horizontal_speed)
	else:
		velocity.x = move_toward(velocity.x, 0, slow_down_speed * delta)
	
	if direction != 0 and HealthManager.current_health != 0:
		current_state = State.Run
		run_sound.play()
		
		
		
		
		
		animated_sprite_2d.flip_h = false if direction > 0 else true
	if direction == 0:
		run_sound.stop()

func player_jump(delta: float):
	var jump_input : bool = Input.is_action_just_pressed("jump")
	
	
	if is_on_floor() and jump_input:
		current_jump_count = 0
		velocity.y = jump_height
		coyote_jump = false
		current_jump_count += 1
		current_state = State.Jump
		jump_sound.play()
	
	if coyote_jump:
		character_body_2d.velocity.y = jump_height
		coyote_jump = false
		current_jump_count += 1
		
		# multiple jumps
	if !is_on_floor() and jump_input and current_jump_count != max_jump_count:
		character_body_2d.velocity.y = jump_height
		current_jump_count += 1
		current_state = State.Jump
		jump_sound.play()
		
	
	
	if !is_on_floor() and current_state == State.Jump:
		var direction = Input_movement()
		velocity.x += direction * jump_horizontal_speed * delta
		velocity.x = clamp(velocity.x, -max_jump_horizontal_speed, max_jump_horizontal_speed)
		


func player_shoting(delta:float):
	var direction = Input_movement()
	
	if direction != 0 and Input.is_action_just_pressed("shoot"):
		var bullet_instance = bullet.instantiate() as Node2D
		bullet_instance.direction = direction
		bullet_instance.global_position = muzzle.global_position 
		get_parent().add_child(bullet_instance)
		current_state = State.Shoot

func player_muzzle_position():
	var direction = Input_movement()
	
	if direction > 0:
		muzzle.position.x = muzzle_position.x
	elif direction < 0 :
		muzzle_position.x = -muzzle_position.x


func player_animations():
	if current_state == State.Idle:
		animated_sprite_2d.play("idle")
		
	elif current_state == State.Run and animated_sprite_2d.animation != "Run_shoot":
		animated_sprite_2d.play("Run")
		
		
	elif current_state == State.Jump:
		animated_sprite_2d.play("Jump")
		
	elif current_state == State.Shoot:
		animated_sprite_2d.play("Run_shoot")


func player_death():
	var player_death_effect_instance = player_death_effect.instantiate() as Node2D
	if is_dead == true:
		print_debug("calling player death func")
		player_death_effect_instance.global_position = global_position
		get_parent().add_child(player_death_effect_instance)
		death_sound.play()
		DialogueManager.dialogue_ended
		visible = false
		set_process_input(false)
		set_process_unhandled_input(false)
		set_process(false)
		set_physics_process(false)
	
		await get_tree().create_timer(2).timeout
		get_tree().paused = true
		
		print_debug("BEFORE called respawn")
		respawned.emit()
		is_dead = false
		HealthManager.increase_health(3)
		velocity.x = 0
		global_position = checkpoint_manager.last_location
		respawn_sound.play()
		get_tree().paused = false
		
	
	#get_tree().reload_current_scene()
		visible = true
		set_process_input(true)
		set_process_unhandled_input(true)
		set_process(true)
		set_physics_process(true)
	
		if DialogueManager.dialogue_ended:
			DialogueManager.show_dialogue_balloon(dialogue_resource, dialogu_start)
		if DialogueManager.dialogue_started:
			pass

	
	if is_dead == false:
		pass
	
	

func Input_movement():
	var direction : float = Input.get_axis("move_left", "move_right")
	
	return direction


func _on_hurtbox_body_entered(body : Node2D):
	if body.is_in_group("Enemy"):
		if is_dead == false:
			enemy_position = body.global_position
			var tween = get_tree().create_tween()
			tween.tween_property(animated_sprite_2d, "material:shader_parameter/enabled", true, 0)
			tween.tween_property(animated_sprite_2d, "material:shader_parameter/enabled", false, 0.2)
			damage_sound.play()
		
			HealthManager.decrease_health(body.damage_amount)
			knockback(1,1500,300)
		if is_dead == true:
			pass
	
	if body.is_in_group("ouchy"):
		if is_dead == false:
			var tween = get_tree().create_tween()
			tween.tween_property(animated_sprite_2d, "material:shader_parameter/enabled", true, 0)
			tween.tween_property(animated_sprite_2d, "material:shader_parameter/enabled", false, 0.2)
			damage_sound.play()
		
			HealthManager.decrease_health(1)
			knockback(0.5,500,400)
		if is_dead == true:
			pass

	if HealthManager.current_health == 0:
		if is_dead == false:
			is_dead = true
			
			player_death()
		if is_dead == true:
			
			pass
			
		
	
	
func knockback(force: float, y_pos: float, up_force: float):
	if (character_body_2d.position.y - enemy_position.y > 0):
		character_body_2d.velocity = Vector2(-force * 2 * y_pos, -force * up_force)
		print(enemy_position.x)
		print(character_body_2d.global_position.x)
	else: 
		character_body_2d.velocity = Vector2(force * 2 * y_pos, -force * up_force)