Note : Godot 3.2.
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 :
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.
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) ...
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.
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 ...
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 :
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 :
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.
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 :
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.