Currently I am working on a save system for my FPS game. In my game, I have multiple levels which inherit from a base level scene. It is from this base level scene where a JSON saving and loading system is located. Right now, I am having a few problems with this system.
extends Node3D
@export var objectives = {"Open Doors": false, "Activate Particles": false}
signal update_objectives
@onready var objectiveObjects = $"NavigationRegion3D/Objective Objects".get_children()
func _ready():
Globals.objectives = objectives
update_objectives.emit()
for object in objectiveObjects:
object.connect("objective_completed", complete_objective)
func complete_objective(objective):
objectives[objective] = true
update_objectives.emit()
func save():
var save_dict = {}
var savedObjects = get_tree().get_nodes_in_group("savedObjects")
for object in savedObjects:
var object_dict = object.save_data()
save_dict.merge(object_dict)
return save_dict
func save_game():
var save_game = FileAccess.open("user://savegame.save", FileAccess.WRITE)
var json_string = JSON.stringify(save())
save_game.store_line(json_string)
func load_game():
if not FileAccess.file_exists("user://savegame.save"):
return
var save_game = FileAccess.open("user://savegame.save", FileAccess.READ)
while save_game.get_position() < save_game.get_length():
var json_string = save_game.get_line()
var json = JSON.new()
var parse_result = json.parse(json_string)
var node_data = json.get_data()
var savedObjects = get_tree().get_nodes_in_group("savedObjects")
for object in savedObjects:
var object_dict = node_data[object.save_title]
object.load_data(object_dict)
func _on_hud_save_game():
save_game()
func _on_hud_load_game():
load_game()
Mainly, I am not sure how to handle saving across different levels. For starters, how am I supposed to save what level the player is on?
Whenever I try to change the scene in this file I get this error:
“Invalid get index ‘paused’ (on base: ‘null instance’).” This occurs soon after loading. Presumably, the tree can’t be accessed so its giving an error.
func _on_load_game_button_down():
loadGame.emit()
display_message("Loaded Game")
switch_pause_menu() # Line that tries to pause/unpause game
How do I ensure the tree is loaded before calling the switch_pause_menu() function, assuming thats the issue.
Moreso, if I am loading a different level, how should that work? There would multiple enemies, and different enemies than other levels. Should different levels have different save files? How should I structure saving the data for every single enemy?
If anyone has any advice on how to go forward it would be much appreciated.
For context, I am making an FPS that has a save/load system akin to what you would find in an older shooter like Quake or Half Life.
When your game has many levels, loading, saving is a relative pain in the ass.
The general way for doing that is to use a singleton (AutoLoad in Godot) to keep track of the current level the player is on, and other level that you want to save.
Example
Assume you have an autoload called save manager to handle all level save related munipulation.
## SaveManager
extends Node
## should put init level at beginning or load from data
var curr_level: String = ""
## Save all loaded levels, the structure should like this:
## key: String | Save Level id to differentiate from other levels instance
## value: GameLevel | the game level instance, NOT packed scene
var levels: Dictionary = {}
## Your save method for save all levels
func save_game():
...
In your game level node, you may make them support save method
class_name GameLevel extends Node3D
## Save method that SaveManager will use
func get_save_data():
... # generate save data
return data
Or even more elegent, a save component depents on how many objects you want to save.
class_name GameLevel extends Node3D
var save_component: SaveComponent
## Save method that SaveManager will use
func get_save_data():
save_compoent.get_save_data()
return data
class_name SaveComponent extends Node
@export var need_save_components: Array[Saveable] = []
func get_save_data():
var save_date = []
for obj in need_save_components:
save_data += obj.get_save_data_from_saveable()
You may want to create an archive feature with multiple ported files.
I recommend using ResourceSaver singleton and ResourceLoader singleton to directly save the entire scene to a file.
If you don’t want to think about where the enemy’s data is placed.
You can directly put the enemy into the level.
Different levels have different saved files.
Loading different levels is as simple as switching scenes.
In addition to what @hakkerbarry posted, I found this article helpful:
Basically, you let each object take care of saving itself. If it’s in the scene tree, it will save, and then you just have to load it all back up. I also agree with @qmilfox that using Resources saves a lot of the io headache; however you can also just do this with a dictionary.
This is the save/load game code for the game I’m currently working on. I construct a dictionary, let the player choose a save/load path, and then these two methods take care of storing and retrieving everything. I don’t have to worry about serialization at all.
func save_file(save_information: Dictionary, path: String) -> bool:
var file = FileAccess.open(path, FileAccess.WRITE)
if file == null:
return false
file.store_var(save_information)
return true
func load_file(path: String) -> Variant:
if not FileAccess.file_exists(path):
return
var file = FileAccess.open(path, FileAccess.READ)
return file.get_var()
You could do the same thing, pass a dictionary of everything in each level, and then pack that into another dictionary, one entry per level, plus your player. Then you save and load that dictionary. It’ll get bigger as your game progresses, but that’s ok - you only need to load the current level.
I use the exact same code for my game settings. I just hardcode the path for that and save it silently whenever the player makes a change or quits the game.