Hour 12. File System
What You’ll Learn in This Hour:
Special Godot paths
Creating and saving configuration files
Saving general files, such as save games
Using encrypted files
Managing files and directories with scripting
Godot has capabilities to deal with the file system of the player’s device, which is important when creating and managing configuration and
save files. This hour will focus on the scripting API that helps you handle this in a portable, multi-platform way.
Special Paths
Apart from the standard file system paths available from the user’s operating system, Godot provides two special portable file paths: The
Resources directory and the User directory.
Resource Path
The root folder of your project is the Resource Root. From there, all the resources are loaded to the game. It is identified with the res://
prefix, which acts as a protocol in standard paths (such as file:// or http:// ), and can be used like a regular path in the functions.
The Resource path is where your project lives. It is great for reading project resources in a portable way, since it’s the same for every platform.
However, you should not try to write on it. You can write on the Resource path inside the editor, because it’s just your project folder, but it will
likely be a read-only path when the game is exported. This is especially true for mobile, as the file system access is restricted. Even on
desktop platforms, the game is exported in a single read-only package (a Godot-specific file, as we’ll see in the hour about exporting).
TIP
Case Sensitiveness
The file system on Windows is not case-sensitive; therefore, it does no
t
matter if you change a capital letter to a lowercase correspondent. This
is not true for other operating systems, however, so it’s important to keep the case of the file names in your code as they are in the system.
A general rule of thumb is to name all files in lowercase.
User Path
In a similar manner, there’s a special path that can be accessed by the user:// prefix. This path is meant to store user-specific information,
such as configurations and game saves. The actual location of this directory varies from one operating system to another, but it uses the
common user-data location. It is a reliable place to write files in-game so they don’t get lost.
NOTE
User Path Location
The user path has different locations for each platform to follow the standards of the user’s operating system. If you want to find the files, here’s
a list of locations per OS:
Windows: %APPDATA%/.godot/appuserdata/project_name
Linux and MacOS: ~/.godot/app_userdata/project_name
HTML5: The file system is mocked using the LocalStorage API.
Android, iOS, and UWP: These platforms have restricted access to the application data.
Note that you can remove the .godot/app_userdata from the path and just use project_name (which is a path-safe conversion of your
project’s name). To do that, enable Use Shared User Dir on Project Settings under the Application/Config section.
Game Configuration
One very common thing in games is the options menu. The user can select the difficulty, video and audio settings, control configuration, etc.
The player also expects the game to remember these options between sessions. To deal with that, you need to save a file in the player’s
machine with the selected options so that the game can save and load the next time.
Godot provides a special class called ConfigFile. This class has methods to add, edit, and retrieve options in a INI-like formatted file. It can
deal with sections in the file to better organize the content, and is responsible for loading and saving the file in the disk. An example usage of
the ConfigFile class is shown in Listing 12.1.
LISTING12.1 Saving Configurations
111111111
22222222Click here to view code image
extends Node
func _ready():
save_config()
func save_config():
var path = “user://config.ini” # The path to save the file
var config = ConfigFile.new() # Create a new ConfigFile object
config.set_value(“options”, “difficulty”, “hard”) # Save the game difficulty
config.set_value(“audio”, “music_volume”, 42) # Save the music volume
var err = config.save(path) # Save the configuration file to the disk
if err != OK: # If there’s an error
print(“Some error occurred”)
In this sample, we add two values to the ConfigFile: the game difficulty and the music volume. The set_value() function receives three
arguments: the section, the property name, and the property value. After setting the values, we need to call save() with the path to actually
write the file to the disk. This function returns an Error value that can be checked against any of the ERR prefixed constants from the
@Global Scope.
Loading Configuration Files
After you save the file to disk, only half the work is done. When the game starts again, you need to load the file from the disk and change the
options to match the file. ConfigFile will also help you with that (see Listing 12.2).
LISTING12.2 Loading Configurations
Click here to view code image
extends Node
func ready():
print(load_config()) # Should do something meaningful, but let’s print for test
purposes
func load_config():
var path = “user://config.ini” # The path to load the file
var config = ConfigFile.new() # Create a new ConfigFile object
var default_options = { # Create a dictionary of defau
l
t options
“difficulty”: “easy”,
“music_volume”: 80
}
var err = config.load(path) # Load the file from the disk
if err != OK: # If there’s an error return the default options
return default_options
var options = {} # Create a dictionary to store the loaded options
# Get the values from the file or the predefined defaults if missing
options.difficulty = config.get_value(“options”, “difficulty”, default_options.
difficulty)
options.music_volume = config.get_value(“audio”, “music_volume”, default
_options.music_volume)
return options # Return the loaded options
The load_config() function starts by creating a dictionary of default options. Then it tries to load the file from the disk. If it fails (which may
be the case in the first run of the game), it returns the default options. This ensures the output is always in a valid state. After that, it creates a
new dictionary to store the loaded options.
There are two calls to the get_value() function from the ConfigFile class. This function receives three arguments: the section, the name
of the property, and the default value (the default is an option argument, and will be null if not provided). In the code, try to load the settings
defined earlier. If for some reason they are not defined (maybe the player deleted them manually), load from the default options.
After building the options dictionary, return it for the calling function so it can do the appropriate task such as changing the music volume or, in
this case, printing the options to the output.
TIP
Avoiding Redundancies
The ConfigFile class has methods for retrieving the sections and the properties in a section. You can use them to make a loop that fills the
options dictionary based on the file instead of listing the properties manually once again, which might be troublesome if you forget to add or
remove one option, or if you change the section of another.
If you centralize the places where options are stored and retrieved, you can save work in the future and avoid the nasty hard-to-find bugs
caused by the redundancy.
111111111
22222222Dealing with Files
While the ConfigFile class is great for storing configuration, it is very limited in what it can do. It is not recommended to deal with more
complex save games. Godot provides a general class to read and write to files in a general way. The File class can create files with any kind
of content, including binary formats. It has options to store and retrieve any type of data, and is able to compress and encrypt files.
Creating and Writing to Files
The first thing you need to do to write to a file is create a new File object. Then you need to open a file from the disk. The class has functions
to write Variant type to files, which may help you deal with the content. Listing 12.3 gives an example of how to write player data to a file.
LISTING12.3 Writing Data to File
Click here to view code image
extends Node
# Some variables to store
var player_name = “Link”
var player_score = 550
func _ready():
create_file()
func create_file():
var path = “user://save.dat”
# Create a new File object
var file = File.new()
# Open the file for writing
var err = file.open(path, File.WRITE)
# Simple error checking
if err != OK:
print(“An error occurred”)
return
# Store the player data
file.store_var(player_name)
file.store_var(player_score)
# Release the file handle
file.close()
The code opens a file for writing. Note that if the file doesn’t exist, it will be created; otherwise, it will be truncated (cleared). The
store_var() function stores a Variant type with metadata (such as type and size) so that it can be retrieved later.
TIP
Open Modes
The second argument of the File.open() method is the mode to open the files. There are four modes available, all of which are constants
in the File class:
READ: Open for reading. Will return an error if the file exists.
WRITE: Open for writing. Create the file if it doesn’t exist and truncate if it exists.
READ_WRITE: Open for reading and writing. Return an error if it doesn’t exist; don’t truncate if it exists.
WRITE_READ: Open for reading and writing. Create the file if it doesn’t exist and truncate if it exists.
These may be a bit confusing to remember, but you can always check the built-in help if needed.
Reading from Files
Writing to a file can only be useful if you can read the data later. The File class has similar methods for reading as for writing (e.g., if you use
store_double(), you can later retrieve with get_double()). Let’s try to get back the player data from the file (see Listing 12.4).
LISTING12.4 Retrieving Data fromthe File
Click here to view code image
extends Node
func _ready():
read_file()
func read_file():
var path = “user://save.dat”
# Create a new File object
var file = File.new()
# Open the file for reading
111111111
22222222var err = file.open(path, File.READ)
# Simple error checking
if err != OK:
print(“An error occurred”)
return
var read = {}
read.player_name = file.get_var()
read.player_score = file.get_var()
file.close()
return read
This code is like that in Listing 12.3: it creates a File object and opens the file for reading. Note that an error will be returned if the file doesn’t
exist. After it’s open, retrieve the variables and put them into a dictionary so the values can be returned.
NOTE
Data Order Matters
If you are writing binary data to a file, be sure to use the same order when reading. If the order is changed during the development process, the
old files may be wrongly read or simply unreadable. This may also be needed to update the game (add data or change a data type). Be sure to
have a consistent format and versioning in your binary files to avoid losing data, especially when dealing with player data. No one likes to lose
their game progress after an update.
TIP
Text Formats
Instead of simply storing variables directly, you can save the data as text. Godot has support for the popular JSON format (JavaScript Object
Notation), which can be used easily by other applications. It may not be suitable for save games if you don’t want the player to tamper with
them, but it can be useful for configuration storage.
You can use the to_json() and from_json() functions to convert from a dictionary to text and vice-versa. Then simply use
File.store_string() and File.get_as_text() methods to store and retrieve the JSON from the file.
Compressing and Encrypting Files
The File class has special variations of the open() method to use c
o
mpression or encryption in your files. When you use them, the file will
be automatically dealt with following the way it was opened. Note that encryption and compression needs a flush (the process that stores data
to the disk). You need to call close() on the file object to flush the data; otherwise, you may lose it.
The open_encrypted() and open_encrypted_with_pass()functions both open files with encrypted data. They can be used to open
files for writing and reading the same way as the regular open() method. The difference between open_encrypted() and
open_encrypted_with_pass() is that the first receives a binary key (as a PoolByteArray), and the other receives a string password
that will be used to decrypt the file when opened in read mode.
The same applies to the open_compressed() method. This method receives an optional argument to determine the compression mode.
You can use the COMPRESSION prefixed constants from the file class. The default method is FastLZ, which has a nice compromise between
compression ratio and speed.
TIP
Compressing and Encrypting the Same File
Godot does not offer a direct API to compress and encrypt a file at the same time, but it is possible to work around that. If your data is in an
array or in a string, you can convert it to a PoolByteArray. This kind of array has compression functions, so you can compress it and store
the result as a buffer in the encrypted file, then follow the reverse steps to get back the data.
This may be a bit convoluted, but if you really need to do it, it’s possible to abstract it away with a few functions.
NOTE
Encryption and Security
It’s important to note that even if you encrypt save games, the key to decrypt must be somewhere in your application, and dedicated hackers
could get their hands on it. Also, if the application uses the same key for all users to store save games, it won’t stop players from exchanging
save files.
The rule of thumb is to never trust the client. If your game relies on an online multiplayer experience that could be ruined by cheaters, be sure to
enforce the rules on the server, where you have control.
Dealing with Directories
Besides the File class, Godot also contains a Directory class. This one is responsible for managing folders in the file system. It can list,
delete, rename, and move files.
Listing the Files in a Folder
111111111
22222222Listing the Files in a Folder
A simple way to start is to list all the files in a certain directory. Godot has a few functions to make this behavior possible. This is a bit different
from how it’s usually handled, so let’s look at an example, shown in Listing 12.5.
LISTING12.5 Retrieving Data fromthe File
Click here to view code image
extends Node
func _ready():
# Create a new directory object
var dir = Directory.new()
var err = dir.open(“res://“)
if err != OK:
print(“An error occurred”)
return
# Start listing the directories
dir.list_dir_begin()
# Retrieve the first file or dir
var name = dir.get_next()
# Start a loop
while name != “”:
# Test if it’s a dir and print as appropriate
if dir.current_is_dir():
print(“dir : “, name)
else:
print(“file: “, name)
name = dir.get_next()
# End the listing
dir.list_dir_end()
The first step, as always, is to create a new Directory object. Try to open the resources directory, and if there’s an error, stop the function.
Then call a method to start the listing of files and directories. Start a loop, and at each iteration, call get_next() to advance in the list.
There’s a test for the current entry: if it’s a directory, prefix the print with “dir”; otherwise, use “file.” When the loop ends, call a function to stop
the listing and close the stream.
NOTE
Special Directory Entries
If you ran the code above, you may note that the first two entries are a dot (.) and a couple of dots (..). These are standard notations in file
system paths. The single dot means the current directory, and the double dot means the directory above the current one. You can use them for
relative navigation.
If you don’t need them or want to skip them when listing the files, you can pass true as the first argument of the list_dir_begin() call.
Managing Files and Directories
There are a handful of methods in the Directory class that can act as full-fledged file explorers. In fact, these functions are in use by the
engine internals in the file dialogs. Let’s walk through some possibilities with this powerful class.
Creating Directories
Two functions can create folders: make_dir() and make_dir_recursive(). The difference between the two is that the latter creates all
the intermediate directories if they do not exist, while the former just returns an error if that happens.
Deleting Files and Directories
The remove() method of the Directory class acts as a delete function for both files and directories. Note that to remove folders, they must
be empty. There’s no function to delete a directory recursively, but you can implement your own inGDScript.
Moving, Renaming, and Copying Files
The rename() method can be used for renaming and moving files. If the destination path is different from the source, the file will be moved
there. This behavior is akin to the “move” command of Unix-like platforms.
To copy files, you can use the copy() method. Its arguments are the same as the rename() function: the first is the source, and the second
is the target. Both methods overwrite the destination if it already exists.
Checking for Existence
The Directory class has two methods for checking existence in the file system: dir_exists() and file_exists(). Both work with
relative and absolute paths. Note that if you call dir_exists() with the path to a file, the function will return false, and vice-versa.
TIP
FileDialog
111111111
22222222If you need a graphical way for the user to select files and directories, you should use the FileDialog control. It has all the appropriate
functions and interface to navigate within the file system and let the user select a file. This control is very similar to the file dialogs encountered
in the Godot engine editor.
Summary
This hour covered the aspects of the file system and how Godot deals with it. You saw the special portable paths of the engine that can be
used in any platform. You learned how to create configuration files and store them to the disk, followed by general files and how to deal with
them. In the end, you saw how the Directory class can navigate and interact with the files in the user’s operating system.
Q&A
Q. Can Godot access any path in the file system?
A. Yes. However, it follows the operating system’s permissions, so the game cannot change files to which the player doesn’t have access or
permission to edit.
Q. Do the file systemfunctions apply to any OS?
A. Yes. Godot functions are truly portable. Some functions are OS-specific when there is a need (like access to the Windows drives) but in
general, you can rely on the access to work in any operating system.
Q. Does Godot deal with binary files from other applications?
A. Godot can manage any kind of file. However, if you are trying to use files from other programs, you need to implement your own parser to
analyze the file format.
Workshop
Here are a few questions to help you review the contents of this hour.
Quiz
1. Where’s the user path located?
2. True or False: ConfigFile automatically detects your game configuration and saves it.
3. Is it possible to load a file with File class if it was saved with ConfigFile?
4. True or False: Godot games can create, modify, and delete files and directories.
Answers
1. The user path resides in the common application data for the user’s operating system.
2. False. ConfigFile can be used to save the configuration, but you need to provide the information via code.
3. Yes. The File class can open any file, including those created with ConfigFile.
4. True. Games have full access to the user’s file system (if the user has permissions). These functions should be used wisely.
Exercises
Take some time to solve the following exercises. They may require some coding experience, but solving them will improve your understanding
of the file system functions.
1. Modify the directory listing to show the subdirectories. This requires a recursive function. Be careful to not fall into an infinite loop by
going into the special relative paths (“.” and “..”).
2. Abstract the configuration saving and loading to functions that receive and return a dictionary. Pass the file path as an argument.
3. Try to implement a configuration saver and loader using JSON.
4. Make a system of save slots. Each slot can be a file, and the player may be able to delete them.
5. Advanced: Implement the abstraction so that you can compress and encrypt the save game at the same time.