Skip to content

Settings

Charles edited this page Jun 6, 2026 · 5 revisions

Introduction

Settings help make your game more accessible; however, they're often cumbersome to code on a short timeframe due in large part to the difficulty synchronizing the UI, save data, and the nodes they affect. To make this process as painless as possible, ProtoJam provides a data-driven settings framework that makes adding a new setting as simple as configuring a resource.

This framework is comprised of two components: the SettingsManager and the AbstractSetting-based settings you define. The SettingsManager is a static a class which reduces the tedium and loading/saving your settings to a pair of simple functions. AbstractSetting and its various subclasses are resources which both define the properties of a setting and act as handles to get or set its value.

ProtoJam provides the following setting types out of the box but you can create your own by extending AbstractSetting:

  • BooleanSetting - A simple on/off setting
  • RangeSetting - A floating point number constrained to a defined range and step amount
  • AudioBusVolumeRangeSetting - A variation of RangeSetting designed to store the linear volume of an audio bus

Note

This framework is intended for game settings only. All other save data should use the save game framework instead.

Using SettingsManager

The SettingsManager class is static and requires no setup to use; however, you can optionally change the location where a user's game settings are stored by modifying addons/proto_jam/settings/file_path in the project settings.

When starting the game, it's recommended to make a call to SettingsManager.load_settings() to load the user's settings. If successful, this will automatically update the value of any setting resource; otherwise, their values will be left at the defaults.

While each setting is immediately applied as it is changed, their current value will not be saved until a call to SettingsManager.save_settings() is made. This function will save the current value of all settings or return an error if they could not be saved.

Finally, a call to SettingsManager.clear_settings() may be made to immediately restore the default value of all settings. A subsequent call to SettingsManager.save_settings() is needed to save this change if desired.

Note

It is recommended to load save data from the main scene before anything else to ensure the user's volume and accessibility settings are applied as soon as possible. Furthermore, it is recommended to NOT save settings at shutdown or otherwise without the user's consent as this may be confusing.

Using AbstractSetting

AbstractSetting is the base resource which all settings extend. It provides a unique key to identify the setting in the save file, functions to get and set the value, and a value_changed signal which will be emitted any time the value changes (including when loading).

Important

No two settings may have the same key. Doing so will cause loading/saving conflicts.

Creating custom setting types

Several setting types derived from AbstractSetting are provided with ProtoJam but you may wish to create your own. Doing so is very easy and requires only four steps:

  1. Extend AbstractSetting
  2. Implement AbstractSetting.from_raw(value)
  3. Implement AbstractSetting.to_raw(value)
  4. Invoke AbstractSetting.emit_value_changed(value) as needed (see below)

Implementing to_raw and from_raw

The parameter to_raw accepts and from_raw returns define the type of the setting's value and may be any type (including classes); however, these functions must be capable of convert this type to and from a serializable (aka raw) representation respectively.

The following are the only valid return types for a to_raw implementation:

  • int
  • float
  • bool
  • null
  • String
  • StringName
  • Array (must only contain one of the above types)
  • Dictionary (must only contain the above types as keys or values)

The from_raw implementation must accept the same type to_raw returned; furthermore, it must gracefully handle null. In most cases, receiving null should result in a default value being returned.

When to call emit_value_changed

The value_changed signal will be emitted automatically when AbstractSetting.set_value(value) is called, when AbstractSetting.setting_key is changed, and when SettingsManager loads a new value for the setting; however, there are situations where you must manually call AbstractSetting.emit_value_changed() to emit the signal. The general rule is it should be called any time your setting does something outside of from_value that will cause from_value to return a different value for the same input (though you should try to avoid calling it unnecessarily).

In practice, this typically means you should call the function for two conditions:

  • When the setting is currently at its default value and the default changes
  • When the setting's constraints have changed and the current value must be adjusted to conform

Important

This should NOT be called from within from_raw itself as ProtoJam will always emit value_changed after from_raw is invoked.

Note

If the above section makes your head spin, don't worry. Take a look at the examples below or the sources for the provided setting implementations for help.

If you're still unsure, it's generally better to err on the side of caution and call emit_value_changed even when value wouldn't be different to ensure everything depending on the setting always gets updated. The worst that can happen by calling this function excessively is causing logic dependent on the value_changed signal to get invoked more than necessary.

Examples

Example 1 - Loading settings

This example demonstrates loading saved settings from the main scene.

main.gd

class_name Main
extends Node

func _ready() -> void:
	var settings_error: Error = SettingsManager.load_settings()
	if Error.OK != settings_error:
		# Failing to load settings isn't a critical error as the defaults will still exist but it should be logged for debugging
		push_error("Failed to load settings; error code %d" % settings_error)

Example 2 - Saving settings

This example demonstrates saving the current settings from a settings menu scene.

settings_menu.gd

class_name SettingsMenu
extends PanelContainer

# A reference to the button in your scene to trigger saving
@onready var _save_button: Button = %SaveButton

func _ready() -> void:
	_save_button.pressed.connect(_on_save_pressed)


func _on_save_pressed() -> void:
	var settings_error: Error = SettingsManager.save_settings()
	if Error.OK != settings_error:
		OS.alert("Unable to save settings; error code %d" % settings_error)

Example 3 - Custom setting type

This example demonstrates a custom enum setting whose value will be serialized to a string when saving.

my_enum_setting.gd

class_name MyEnumSetting
extends AbstractSetting
## A setting limited to the values of an enum.

## The enum representing the valid options for the setting.
enum MyEnum {
	OPTION_1,
	OPTION_2,
	OPTION_3,
}

## The default value of the setting.
@export var default_value: MyEnum = MyEnum.OPTION_1:
	set(value):
		# Don't do anything if the value didn't change
		if value == default_value:
			return
		
		# Get the current value before applying the new default
		var old_value: bool = get_value()
		
		# Update the default and fetch the new value
		default_value = value
		var new_value: bool = get_value()

		# Indicate the resource itself has changed
		emit_changed()
		
		# If the values before and after changing the default are different then we know it caused a change, emit a value changed signal
		if old_value != new_value:
			emit_value_changed()


## Required to extend AbstractSetting, converts the serialized value back into an enum 
func from_raw(value: Variant) -> Variant:
	# Ensure the serialized value is not null and is a valid enum string
	if null != value and value is String and MyEnum.has(value):
		return MyEnum.get(value)

	return default_value


## Required to extend AbstractSetting, converts the enum into a string for serialization
func to_raw(value: Variant) -> Variant:
	# Since value is untyped, verify it is MyEnum before attempting to string-ify it
	if value == null or value is not MyEnum:
		# Use the default if value is invalid
		value = default_value

	return MyEnum.keys()[value]

Example 4 - Custom setting control

This example demonstrates a UI slider control for a RangeSetting.

setting_slider.gd

class_name SettingSlider
extends HSlider
## A custom slider for RangeSettings.

## The setting to control.
@export var setting: RangeSetting = null:
	set(new_setting):
		# Disconnect signals from the old setting (if connected)
		if null != setting:
			if setting.value_changed.is_connected(_on_setting_value_changed):
				setting.value_changed.disconnect(_on_setting_value_changed)
			
			if setting.changed.is_connected(_on_setting_changed):
				setting.changed.disconnect(_on_setting_changed)
		
		# Update setting and connect signals to watch for changes
		setting = new_setting
		if null != setting:
			_configure_slider()
			setting.value_changed.connect(_on_setting_value_changed)
			setting.changed.connect(_on_setting_changed)


func _ready() -> void:
	value_changed.connect(_on_slider_value_changed)


func _configure_slider() -> void:
	# Configure the slider to match the setting's configuration
	min_value = setting.min_value
	step = setting.step_value
	max_value = setting.max_value
	value = setting.get_value()


func _on_slider_value_changed(value: float) -> void:
	# If the slider value changes, update the setting value
	if null != setting:
		setting.set_value(value)


func _on_setting_value_changed(setting_value: Variant) -> void:
	# If the setting value changes, update the slider value
	value = setting_value


func _on_setting_changed() -> void:
	# If the setting has changed, reconfigure the slider
	_configure_slider()

Clone this wiki locally