How-to-guide: save/load persistence in your project

edited January 28 in Tutorials
Cross-posted on the Godot Forum here.
Find in the Godot Asset Library here.

Overview

SaverLoader (saver_loader.gd) can save procedural scene trees of arbitrary structure and rebuild them on load. It persists data from procedural and non-procedural objects – but only what you tell it to persist! Saves and loads are very fast because we don't save whole objects. 

I'm pulling SaverLoader out of I, Voyager for distribution on the Godot Asset Library, so this tutorial is for users that may or may not be developing an I, Voyager-derived project. However, it might be useful to have a look in our code to see SaverLoader used in context.

I, Voyager has >100 planets & moons and typically runs with ~65,000 asteroids. When starting a new game, the entire Solar System is built "procedurally" from external data files. When a game is saved, the present state of all of that goes in the save file. On load, the Solar System "tree" (with Sun as root) is rebuilt from the save file. Our save/load times with an ssd drive are on the order of ~1 second!

How does SaverLoader know which objects to persist?

The presence of the constant "PERSIST_AS_PROCEDURAL_OBJECT" tells SaverLoader that an object is a "persist object". The value of that constant tells SaverLoader whether:
    [true] the object needs to be freed and recreated on load (preserving parent-child structure of nodes), or
    [false] the object may have some persist data but it shouldn't be freed.
SaverLoader can persist objects of class Node or Reference (but only Nodes can have PERSIST_AS_PROCEDURAL_OBJECT = false). There are some rules to follow so that SaverLoader can find the object for persistence; these are in A bit more detail below.

I'm doing Scenes, not Scripts!

SaverLoader sees everything as Scripts, not Scenes. But it can instance scenes if the persist node's script tells it where to find the scene. Either of these two constants in a persisted Node's script would do that:
    const SCENE := "res://.../my_scene.tscn"
    const SCENE_OVERRIDE := "res://.../my_override_scene.tscn"
The second is useful for a subclass that has a scene different than its parent class. 

What can SaverLoader persist?

Within objects that are persisted (defined above), SaverLoader can persist properties that contain built-in types, arrays & dictionaries (of arbitrary nesting structure), other persist objects (as defined above), and weak references to persist objects. I, Voyager uses this mostly for script vars, but also for some simple properties like Node.name. In theory it should work for something like a mesh resource, but I've never tested this.

What should I NOT persist?

The whole point of SaverLoader is to NOT persist what you don't need to persist. A whole lot of your object probably doesn't change or can be rebuilt from a little bit of persisted data. That's the strategy we take in I, Voyager.

How do I tell SaverLoader which properties to persist?

There are some additional constants you add to your persist object to tell SaverLoader what exactly to persist. The constant names can be changed but out-of-the box we have:
    const PERSIST_PROPERTIES := []
    const PERSIST_OBJ_PROPERTIES := []
The contents of those arrays are the names (strings) of properties you want to persist in the object. If the property holds a "persist object" (either directly or nested in an array or dict) then it needs to be in the second array. Anything in the 1st array could be in the 2nd, but it's faster to keep non-object stuff in the 1st. For example, in I, Voyager we persist orbital data for 100000s of asteroids in pool arrays that are named in PERSIST_PROPERTIES.

How do I add this to my game?

(You can skip if making an I, Voyager project.)
SaverLoader doesn't have any GUI. You'll have to have your own save/load dialog popups. The simplified code below shows how to use SaverLoader's save_game() and load_game() functions in context. The rest of the game would use the signals here to know when to run, stop, finish threads, etc. (I, Voyager has a lot of extra save/load infrastructure; see here if you really want to.)
extends Node
class_name MyMain

signal game_save_started()
signal game_save_finished()
signal game_load_started()
signal game_load_finished()

var _saver_loader := SaverLoader.new()

func save_game(path: String) -> void:
	var save_file := File.new()
	save_file.open(path, File.WRITE)
	emit_signal("game_save_started")
	_saver_loader.save_game(save_file, get_tree())
	yield(_saver_loader, "finished")
	emit_signal("game_save_finished")

func load_game(path: String) -> void:
	var save_file := File.new()
	save_file.open(path, File.READ)
	emit_signal("game_load_started")
	_saver_loader.load_game(save_file, get_tree())
	yield(_saver_loader, "finished")
	emit_signal("game_load_finished") 

A bit more detail...

(Copied from saver_loader.gd file header.)
# SaverLoader can persist specified data (which may include nested objects) and
# rebuild procedurally generated node trees and references on load. It can
# persist four kinds of objects (in addition to built-in types):
#    1. Non-procedural Nodes
#    2. Procedural Nodes (including base nodes of scenes)
#    3. Procedural References
#    4. WeakRef to any of above
# A "persist" node or reference is identified by presence of the constant:
#    const PERSIST_AS_PROCEDURAL_OBJECT: bool
# Lists of properties to persists must be named in constant arrays:
#    const PERSIST_PROPERTIES := [] # names of properties to persist (no nested objects!)
#    const PERSIST_OBJ_PROPERTIES := [] # as above but allows nested persist objects
#    const PERSIST_PROPERTIES_2 := []
#    const PERSIST_OBJ_PROPERTIES_2 := []
#    etc...
#    (These list names can be modified in project settings below. The extra
#    numbered lists are needed for subclasses where a list name is taken by a
#    parent class.)
# To reconstruct a scene, the base node's gdscript must have one of:
#    const SCENE: String = ""
#    const SCENE_OVERRIDE: String # as above; may be useful in a subclass
# Additional rules for persist objects:
#    1. Nodes must be in the tree.
#    2. All ancestor nodes up to root must also be persist nodes.
#    3. A non-procedural node cannot be child of a procedural node.
#    4. Non-procedural nodes must have stable names (path cannot change).
#    5. Inner classes can't be persist objects
#    6. For references, PERSIST_AS_PROCEDURAL_OBJECT = true
#    7. Virtual method _init() cannot have any args.
# Warnings:
#    1. A table or dict persisted in two places will become two upon load.
#    2. Persisted strings cannot begin with object_tag.
Sign In or Register to comment.