Game Dev Artisan Logo
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.

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!

Want to support our work?Support us via Ko-fi