Table of Contents
Introduction
In this post of Godot Fundamentals, we'll be implementing saving for our User Preferences using a Custom Resource and the Resource Saver. With this we'll be able to track our settings for the music and sfx audio levels, our control scheme, and our custom control mappings for our Input Map.
Video
Custom Resource
Godot allows users to script their own Custom Resources just like they do with Objects. This allows us developers to benefit from the serialization of object properties just like the base Resource classes do.
A Custom Resource has all the benefits of an Object, RefCounted, and Resource class. This has a ton of great benefits that you can read more about on the official docs page which covers some great use cases. I've also written a quick tip article on Custom Resources that you can check out.
For our purposes we will be taking advantage of the ability of setting up our Custom Resource class for our User Preferences that will track properties for our music and sfx audio levels, what control type we want to use, and any custom control mappings we are overwritting our default Input Map.
User Preferences Resource
To get started we'll be create a new folder to contain our custom resources: res://resources
and a new script: res://resources/user_preferences.gd
. Our new script will be a class called UserPreferences
and will extend Resource
. This will export all of our properties that we want ot track.
For our audio level's we'll use the @export_range
keyword to allow us to bind the min, max, and step values; just like our sliders use.
We'll also create a function for saving our Resource
to our user's data folder as user://user_prefs.tres
. We set the file extension as .tres
which is a text based resource file, where as a .res
would be a binary resource file which is what Godot will do to all text resources when we compile our game for a build.
To use this resource throughout our code, we'll create a static accessor function that will either load an existing resource that is saved in our User's data folder, or create a new instance of our resource to be modified.
# user_preferences.gd
class_name UserPreferences extends Resource
@export_range(0, 1, .05) var bg_audio_level: float = 1.0
@export_range(0, 1, .05) var sfx_audio_level: float = 1.0
@export var input_type: Game.INPUT_SCHEMES = Game.INPUT_SCHEMES.KEYBOARD_AND_MOUSE
@export var action_events: Dictionary = {}
func save() -> void:
ResourceSaver.save(self, "user://user_prefs.tres")
static func load_or_create() -> UserPreferences:
var res: UserPreferences = load("user://user_prefs.tres") as UserPreferences
if !res:
res = UserPreferences.new()
return res
Also notice I'm using an single line to define the class_name and the extends keywords. I saw this in a few code examples and official docs. It feels like many other languages and may be my new goto styling!
Saving and Loading
Next we'll implement the saving and loading of our properties from changes that occur within our GameMenu
and the items we want to track.
Open up the GameMenu
scene and add a unique access name for our SFXSlider
, MusicSlider
, and InputTypeButton
. Then bind a reference inside the game_menu.gd
script so we have a variable reference once our node is ready. We will also add a class variable of user_prefs that is of type UserPreferences
.
# game_menu.gd
...
@onready var sfx_slider = %SFXSlider
@onready var bg_slider = %BGSlider
@onready var input_type_button = %InputTypeButton
var user_prefs: UserPreferences
From our _ready
function, we'll ensure we store a local reference to either an existing or new UserPreferences
instance by calling the static load_or_create
function on our UserPreferences
class. We'll also ensure that we set the value of our sliders and the InputTypeButton
based on what our user_prefs has defined.
# game_menu.gd
...
func _ready():
user_prefs = UserPreferences.load_or_create()
if sfx_slider:
sfx_slider.value = user_prefs.sfx_audio_level
if bg_slider:
bg_slider.value = user_prefs.bg_audio_level
if input_type_button:
input_type_button.selected = user_prefs.input_type
create_action_remap_items()
Inside our _on_*_slider_value_changed
functions we'll be adding a few lines of code to set the user_prefs values and save it using the save
function we defined on the UserPreferences
class.
# game_menu.gd
...
func _on_sfx_slider_value_changed(value):
AudioServer.set_bus_volume_db(SFX_BUS_ID, linear_to_db(value))
AudioServer.set_bus_mute(SFX_BUS_ID, value < .05)
if user_prefs:
user_prefs.sfx_audio_level = value
user_prefs.save()
...
func _on_bg_slider_value_changed(value):
AudioServer.set_bus_volume_db(MUSIC_BUS_ID, linear_to_db(value))
AudioServer.set_bus_mute(MUSIC_BUS_ID, value < .05)
if user_prefs:
user_prefs.bg_audio_level = value
user_prefs.save()
To track our input type, we'll add a bit of code to our existing _on_input_type_button_item_selected
function that saves off our changed input type:
# game_menu.gd
...
func _on_input_type_button_item_selected(index):
if index != -1:
Game.INPUT_SCHEME = index
EventBus.input_scheme_changed.emit(index)
if user_prefs:
user_prefs.input_type = index
user_prefs.save()
Lastly we'll do a bit of prep work so we can properly save our remapped actions. For us to know when an action has been remapped, we will add a signal to our RemapButton for when we set a new event for an action.
# remap_button.gd
...
signal action_remapped(action, event)
...
func _unhandled_input(event: InputEvent):
...
button_pressed = false
action_remapped.emit(action, event)
Now when we process the unhandled event and remap the action with that new event we will also emit a signal passing these values. Back in our GameMenu
's script we can connect our new signal to a _on_action_remapped
function that will be responsible for tracking the changes inside our UserPreferences
resource and saving it off. We will also refactor our create_aciton_remap_items
function to read any existing values of saved action events from the Dictionary
and set them inside the InputMap.
# game_menu.gd
...
func create_action_remap_items() -> void:
...
for index in range(action_items.size()):
...
button.action = action
if user_prefs:
if user_prefs.action_events.has(action):
var event = user_prefs.action_events[action]
InputMap.action_erase_events(action)
InputMap.action_add_event(action, event)
button.action_remapped.connect(_on_action_remapped)
settings_grid_container.add_child(button)
...
func _on_action_remapped(action: String, event: InputEvent):
if user_prefs:
user_prefs.action_events[action] = event
user_prefs.save()
If we run our game now we can see that when we make changes to our settings they will automatically be saved to our custom resource on disk and when we restart our game they will persist and automatically update the values as we set them.
Recap
We have created a Custom Resource for our User Preferences and can now track the changes we want to retain between game sessions. Each time our game is ran it will reset the state of our sliders and input configurations.
This demonstrates how simple Godot makes it for us to save state and serialize it to our disk. There are many other use cases for Custom Resources and other methods of saving state, but this is a great introduction to the topic.
As always you can start a discussion to dig deeper into the topic on our Discord Community and check out the project files on our Github project page for the source code.
Happy Coding!