Hello! Would some people be interested in forming a one-off task group to write and propose a new example for saving and loading for the docs?
The current example is not too bad, but as such a system seems to be crucial and at the same time causing a headache for many fresh developers (I include myself) I feel inclined to gather some kind of best practice steps.
I’d like to keep this concise and productive, hence I’d suggest forming a small team of max. 6 people, first gather some of the solutions the team has come up with in the past, compare those if needed and extract the major pain points from discussions on github (and the forum if applicable). After the initial research phase discuss and decide how to structure the proposal.
This should and could be done in 5 to 6 sessions an hour each within two or three weeks. The group would set the timing depending on availability. I’d suggest june as a rough point in time.
I can host a collaboration document for that via etherpad or spin up a forum software (wanted to try Loomio for a while now, this could be a good time) and I’m also willing to do the writing work to compensate for the little of experience I bring in building save/load systems.
If we can get a team of at least three experienced people together here, I’d be happy to set everything up based on the initial wishes of the team (timeline, communications, input).
1 Like
I’ve not worked on those systems yet unfortunately. But if there’s anything I can do (proof read as inexperienced user for example) I’d be happy to help!
Thanks for the initiative 
Have you looked at my Disk Plugin? It’s an implementation of the example. It also does settings for games. It’s 118 lines of code (including comments). It does the following:
- Allows for a configurable save paths for settings and game files.
- It has a Save on Quit feature that automatically saves the game whenever Quit is called anywhere in your game.
- It comes with a README that includes installation and usage instructions.
- Has a cute little icon.

- Is open source.
- I’ve used it in a number of games.
disk.gd
@icon("res://addons/dragonforge_disk/assets/textures/icons/floppy-disk-red.svg")
extends Node
const DEFAULT_SETTINGS_PATH = "user://configuration.settings"
const DEFAULT_SAVE_GAME_PATH = "user://game.save"
## The path to save all settings.
@export var settings_path: String = DEFAULT_SETTINGS_PATH
## The path to save all game data.
@export var save_game_path: String = DEFAULT_SAVE_GAME_PATH
## If this value is On, save_game() will be called when the player quits the game.
@export var save_on_quit: bool = false
var configuration_settings: Dictionary
var game_information: Dictionary
var is_ready = false
func _ready() -> void:
if FileAccess.file_exists(settings_path):
configuration_settings = _load_file(settings_path)
ready.connect(func(): is_ready = true)
func _notification(what) -> void:
match what:
NOTIFICATION_WM_CLOSE_REQUEST: #Called when the application quits.
if save_on_quit:
save_game()
## Returns true if the save was successful, otherwise false.
## Calls every node added to the Persist Global Group to save data. Works by
## calling every node in the group and running its `save_node()` function, then
## storing everything in the save file. If a node is in the group, but didn't
## implement the `save_node()` function, it is skipped.
func save_game() -> bool:
var saved_nodes = get_tree().get_nodes_in_group("Persist")
for node in saved_nodes:
# Check the node has a save function.
if not node.has_method("save_node"):
print("Setting node '%s' is missing a save_node() function, skipped" % node.name)
continue
game_information[node.name] = node.save_node()
print("Saving Info for %s: %s" % [node.name, game_information[node.name]])
return _save_file(game_information, save_game_path)
## Call this to call the `load_node()` function for every node in the Persist
## Global Group. The save game, if it exists, will be loaded from disk and the
## values propagated to the game objects.
func load_game() -> void:
game_information = _load_file(save_game_path)
if game_information.is_empty():
return
var saved_nodes = get_tree().get_nodes_in_group("Persist")
for node in saved_nodes:
# Check the node has a load function.
if not node.has_method("load_node"):
print("Setting node '%s' is missing a load_node() function, skipped" % node.name)
continue
# Check if we have information to load for the value
if game_information.has(node.name):
print("Loading Info for %s: %s" % [node.name, game_information[node.name]])
node.load_node(game_information[node.name])
## Stores the passed data under the indicated setting catergory.
func save_setting(data: Variant, category: String) -> void:
configuration_settings[category] = data
_save_file(configuration_settings, settings_path)
## Returns the stored data for the passed setting category.
## Returns null if nothing is found.
func load_setting(category: String) -> Variant:
if !is_ready:
if FileAccess.file_exists(settings_path):
configuration_settings = _load_file(settings_path)
if configuration_settings.has(category):
return configuration_settings[category]
return null
## Takes data and serializes it for saving.
func _serialize_data(data: Variant) -> String:
return JSON.stringify(data)
## Takes serialized data and deserializes it for loading.
func _deserialize_data(data: String) -> Variant:
var json = JSON.new()
var error = json.parse(data)
if error == OK:
return json.data
else:
print("JSON Parse Error: ", json.get_error_message(), " in ", data, " at line ", json.get_error_line())
return null
func _save_file(save_information: Dictionary, path: String) -> bool:
var file = FileAccess.open(path, FileAccess.WRITE)
if file == null:
print("File '%s' could not be opened. File not saved." % path)
return false
file.store_var(save_information)
return true
func _load_file(path: String) -> Variant:
if not FileAccess.file_exists(path):
print("File '%s' does not exist. File not loaded." % path)
var return_value: Dictionary = {}
return return_value
var file = FileAccess.open(path, FileAccess.READ)
return file.get_var()
I also have code in my Controller Plugin that does keybinding (action remapping) and saves them to disk - which is a whole different nightmare of saving data.
Keybinding code
## Sets the passed event for the given action in the InputMap and saves it to
## disk for loading the next time the game is loaded.
func rebind_action(action: String, event: InputEvent) -> void:
_set_binding(action, event)
_save_binding(action, event)
## Sets the passed event for the given action in the InputMap for the game.
## This can result in either an additional option or overriding of an existing
## option.
##
## (E.G. if the action "move_up" was only mapped to the `W` key, passing in an
## InputEventKey event which presses the Up Arrow would overwrite the entry.
## However passing an InputEventJoypadMovementEvent of the left stick being
## moved up would add a new event.)
func _set_binding(action_to_remap: String, event: InputEvent) -> void:
var events = InputMap.action_get_events(action_to_remap)
for existing_event in events:
if existing_event.get_class() == event.get_class():
InputMap.action_erase_event(action_to_remap, existing_event)
break
InputMap.action_add_event(action_to_remap, event)
## Saves on disk the passed event for the passed action.
func _save_binding(action_name: String, event: InputEvent) -> void:
var action: Dictionary
if _action_list.action_events.has(action_name):
action = _action_list.action_events[action_name]
action[event_to_string(event)] = event
_action_list.action_events[action_name] = action
_action_list.save()
## Returns a string representation of the passed InputEvent. Returns a string
## of "Unknown" if the InputEvent was not listed here.
func event_to_string(event: InputEvent) -> String:
if event is InputEventKey:
return "InputEventKey"
elif event is InputEventMouseButton:
return "InputEventMouseButton"
elif event is InputEventMouseMotion:
return "InputEventMouseMotion"
elif event is InputEventJoypadButton:
return "InputEventJoypadButton"
elif event is InputEventJoypadMotion:
return "InputEventJoypadMotion"
elif event is InputEventGesture:
return "InputEventGesture"
return "Unknown"
## Deletes the keybindings.tres file, resets the action_list variable
## to a default of empty, and then reloads all the settings the developer
## set in the inital game.
func _on_restore_default_keybindings() -> void:
_action_list.delete()
_action_list = ActionList.load_or_create()
InputMap.load_from_project_settings()
I’d be happy to help, but I guess I’m curious what you mean by best practice steps? What exactly do you think is missing from the docs?
1 Like
[Off topic sort of]
This seems interesting, I’ve been dreading asking saving to my game so I might take a look into this
1 Like
Cool. I made all my plugins for myself, but I shared them so others could use them too.
1 Like
Not sure what would you expect to see in docs regarding this. It shows a more or less generic way to write dictionaries into json.
Most people don’t realize that “game saving” has very little to do with files. The file part is simple and more or less always the same - you serialize some data structures into text or binary files.
The main problem is figuring out those data structures. They will always be highly specific to your project. There’s no “formula” for it. You need to figure out the smallest set of data structures that can (re)store your complete game state.
As with most “how do I…” questions we see here, the answer is - learn basic data structures and algorithms, and learn them well. That will help immensely in solving precisely the kinds of problems like game state data architecture.
To @normalized’s point, that’s why my Disk plugin is so small. It accepts Dictionary or single values from nodes in the game and adds them to a large Dictionary object. The primary purpose is to store data in a way that doesn’t break between versions of your game. Old data doesn’t prevent loading, and gets wiped out with a new save. And new data just gets added in.
But the user has to decide what data gets sent to the game. I’ve considered adding some sort of Save/Load components, but haven’t decided what that would look like. (And I know @normalized would hate it.
)