Logo 3.5Cats_AndHalfAFish


Saving & loading.

February 7, 2020 Shade, Godot : saving & loading games
Keeping track of scene, player, items, and inventory.

Note : Godot 3.2.

load-save menu A little while ago, I talked about getting data -in- the game (JSON, CSV).
Next, I needed to figure out how to save & load game data (what scene is the player currently in, what location is the player at and in what direction is he looking, what items are visible in the scene, what icons are present in the inventory, etc).

It seems as if this should not be a big deal, but it took me a surprisingly long time to get it working.

First, I would like to mention the Godot documentation for saving games. The docs propose tagging nodes of interest with a 'persist' label (group). When the game is saved, all nodes in the group 'persist' are consulted and asked to execute their own 'save()' method.
While it would certainly be possible to make this technique work for point&click games with more than one room (scene), it would be a bit complicated. The reason is this :

Code : get nodes in group.
Code used to get all nodes in group 'Persist'.

If you check the documentation, you'll find that 'get_tree()' 'returns the SceneTree that contains this node'. In other words : you can only use this for nodes that are still part of the scene !
So, you would need to think carefully about how you are going to make this work for items that were deleted from the scene, or even when switching scenes (rooms).

I have chosen to use a somewhat different approach : the state of an object (stored in a Dictionary) is updated whenever it changes (eg. player picks up a yellow key). When the game is saved, all I have to do is write these Dictionaries to a file (see below).

As the game is still in a very early state, it seemed prudent to save all game data as text (JSON). Later on, I will probably switch to something binary, but I don't have to think about that yet.
Using 'JSON' means I have to store game data as an array (an ordered list of values) or a dictionary (an unordered collection of key:value pairs). A dictionary 'key' has to be a string in double quotes ; a 'value' can be an object, array, string, number, boolean, or null.

Save Game

save menu The 'save game' menu has a small screenshot of the world (on my todo list) and a place for the player to type some short description.
Both pieces of information are sent to an autoload script (a script that's always loaded and that can be accessed easily by all other scripts) that will prepare the game data for writing to file.

This may be a good time to mention that you should also think about sanitizing the user input : are you going to allow the player to type anything he wants, or are you going to limit input to for instance alpha-numerical characters only ?
If you decide to sanitize user input, you might want to use the 'LineEdit' node 'text_changed' signal to replace illegal characters with an empty string ("") while the player is typing. Check out the Godot documentation for Regular Expressions ('regex') for this !

Right. So, what else do we need to save ?
Well, we need the level (scene_id) and the location/rotation of the player (Transform). We also need to know if items in the scene have been picked up by the player (they may now be visible as icons in the inventory). Other items, such as doors or chests, may have been unlocked. Still other items may have been combined, eg. ingredients to make a potion. Lots of stuff to think about !

I have chosen to create a couple of Dictionaries to keep track of these things in the game. It is by no means certain that I will keep it this way later on (just so you know), but it's definitely convenient to do it like this (see img below) ...

  • Dictionary 'db_state' : always contains -all- interactable items in the 3d world (eg. the yellow key is visible & active, the small chest is visible & inactive (locked), the pink key is invisible & inactive).
    Db_state is updated whenever something happens in the game. For instance, when the player picks up the yellow key, the corresponding 'yellow key' entry is immediately updated to reflect this (invisible & inactive (used)).
  • Dictionary 'db_invent' : is empty at the start of the game. It is updated whenever a game item has been picked up by the player. Db_invent contains the item_id.
  • Dictionary 'db_player' : is also empty at the start of the game. It will only be updated once the player is saving a game (inside the save_data() function). Db_player contains the player transform (location + rotation) and the current scene (scene_id).

To save this information in a JSON-file, the contents of the 3 db's is combined into a newly created 'db_current_game_save', together with 2 extra pieces of information : the current version of the game (in case I change the way I store game data), and the 'save_game' data from the save menu (description, reference to screenshot (todo)) + the date and time of the save.

Code for saving game data.
Code used to save game data.

While we're on the topic of player location/rotation ... if you ever tried to store a 'Transform' or a 'Vector3' in JSON, you'll know that that doesn't work !
As I mention earlier, JSON -values- can be objects, arrays, strings, numbers, booleans, or null. Transform and Vector3 however are custom Godot types that do -not- inherit from Object ! Meaning : Transforms and Vector3's are not one of the allowed 'value' types for JSON ! But, rather than throwing an error, JSON.print() silently converts things that it doesn't understand into harmless strings !
In other words : JSON.print(Vector3(1.2,3,0)) will be stored as "(1.2,3,0)". Not very convenient if you were planning to use the Vector3 as a location vector in your game ...

The solution is simple, but not well documented.
I found it mentioned in an Issue Report ; not in the Godot documentation ...

Code using var2str().
Using var2str() and str2var().

JSON.print( var2str(Vector3(1.2,3,0)) ) will be stored as "Vector3(1.2,3,0)" inside the JSON file.
You can also see the difference inside the Godot editor :

Editor : before using str2var().
Editor : before using str2var() the Vector3 is a string.
Editor : after using str2var().
Editor : after using str2var() the Vector3 is a Vector3.

Load Game

load menu
Load menu (wip).

The 'load menu' is very simple. It consist of a scrollable list of entries. Each entry contains the small screenshot of the 3d world (todo) and the short description that was entered by the player when he saved that particular game.

Now, how did I create that list of entries ?
Also pretty simple to do in GDScript :

Code : create ordered list of save_game files.
Code : create ordered list of save_game files.

In GDScript, you can easily obtain a reference to the directory where save_game files are stored (and this works on all OS's) by using the method 'get_user_data_dir()'. Parsing the contents of a directory with 'list_dir_begin()' results in a list of file and directory names (not the full path !). You can also specify if you want to include hidden files and navigational folders in this list.
The 'get_next()' method gives you the next file/dir name in the list, or an empty string if you've reached the end. Note that 'get_next()' gives you back the name of a file -or- a directory ! If you want to check this, you can use the 'current_is_dir( )' method in the Directory class.
In my case, I'm only interested in file names that match "gamesave_x.dat", with 'x' a number.

The final bit of code is used to sort the files array before I use it in the game_load menu. I do this to make sure that the files will be listed in numerical order (sg_1.dat, sg_2.dat, ..., sg_10.dat).
The syntax used is a bit obscure, but the principle is easy to understand. If my files array has at least 2 files, it is sorted with the 'sort_custom()' method in the Array class.
In 'process_save_files_arr()', 'a' and 'b' represent 2 file names in my list. There are several ways to get a substring (number) from a string (file name), but in this case, I chose to replace the parts before and after the number with an empty string. The substring number is then converted into an integer with int(substring), and the 2 numbers are compared to find the smallest one.

The actual 'load menu' is created dynamically so that it can be updated whenever the player saves the game and then continues playing. The empty ScrollContainer with VBox is filled with 'load_menu_items' from code.

Load menu container.
Load menu container.
Load menu item.
Load menu item.
Code : dynamically created load menu.
Code : dynamically created load menu.

Selecting an item in the list and clicking 'load' sends a signal to the autoload file (singleton) which then parses that save_game file and restores the game state from the db's I mentionend in 'save game'.

One thing to keep in mind, especially if you come from a different node-based engine or framework (like me), is that in Godot children are rendered -on top- of the parent !
So, if you want the entire 'entry' to be clickable by the player, you need to make sure that both children (TextureRectangle and Label) have the mouse filter set correctly :

Mouse filter setting.
Mouse filter setting for child nodes.

So. That's it for now for saving and loading game state.
Next, thing to work on, is 'game item interactions' : picking up stuff, unlocking doors and chests, looking through the inventory, combining items to create something new, ... Quite a big chunk of work, I expect.