When making a game in Godot, the values of node properties are most often specific to that individual node, and duplication of a property value among multiple nodes is more a coincidence or design decision than an actual shared value. Still, we will occasionally run into scenarios where we must share a value between multiple nodes. If these nodes have a strong parent-child relationship, this is easily handled by floating these properties up into the parent, where they're then easily obtained and shared between all the child nodes.

Project settings in Godot are primarily for giving the engine the information it needs, to setup and run exactly as you intend it. But they also exist for values where there is no clear-cut relationship between all the possible nodes that may need the value. We of course are not limited to pre-defined project settings; Godot is more than happy to let you define custom entries.

Unfortunately there is a hang-up with nodes that require custom project settings--they tend not to function if the settings aren't defined. This isn't an issue for a node that is purpose-built and will never be reused outside the current project. Add the new settings using the editor, and tell the node where to look for them.

Then comes along that pesky modularity; we develop a node that implements a feature we want, and it turns out to be a feature we want in multiple projects. Turns out it's a feature other people may also want in their projects. Now you're stuck distributing said feature with a note attached, telling the end-user exactly what they need to do to add the correct project settings, to make the feature function.

This may not be so bad if project settings had simple and distinct names, but as it turns out, their name is more of a path into the project settings. When even default settings have names like:

debug/gdscript/warnings/unassigned_variable_op_assign

...it becomes pretty obvious why we wouldn't want to keep manually duplicating effort to add the custom settings we require. As programmers, we should be thinking smart, not hard. Why aren't we making the node itself do the heavy lifting for us?

So as you could probably glean from the ProjectSettings docs, ProjectSettings is largely a glorified dictionary that tracks meta-data about each setting. This makes adding a new setting pretty straightforward:

  1. Check if the setting already exists. If it does, we're done and can bail out here.
  2. If it doesn't, add it.
  3. Add our meta-data about the setting, to make it easier for users to modify in the editor.
  4. Give it a default initial value that users can always revert to.
  5. Save our project settings to lock in our changes.

Now it just so happens that ProjectSettings exposes an individual method for each step. It's just a matter of chaining them into a method of our own.

func add_custom_project_setting(name: String, default_value, type: int, hint: int = PROPERTY_HINT_NONE, hint_string: String = "") -> void:
	
	if ProjectSettings.has_setting(name): return
	
	var setting_info: Dictionary = {
		"name": name, 
		"type": type, 
		"hint": hint, 
		"hint_string": hint_string
	}
	
	ProjectSettings.set_setting(name, default_value)
	ProjectSettings.add_property_info(setting_info)
	ProjectSettings.set_initial_value(name, default_value)

Although it's pretty straightforward, our arguments/meta-info could use some explanation.

name is pretty obviously the name of our setting. The thing to remember though is that like the default settings, this is actually a path into the settings. The root is the category our setting will appear under. Next is the sub-category, followed by the group in the sub-category, and so-on, until finally the actual name of the setting.


Like property names on nodes, the editor stylizes them. The actual names are generally all lowercase, with underscores replacing spaces, and the editor displays them in a more visually friendly way.


In the example above, Enabled is actually display/window/per_pixel_transparency/enabled. We can see the heirarchy displayed within the project settings window.

default_value is self-explanatory.

type is the type of variable the setting is; explicitly, it's a value of the enum Variant.Type defined in @GlobalScope

hint is a value of the enum PropertyHint defined in @GlobalScope. It's purpose is to give the editor context as to the use of our new setting.

hint_string is actually a formatting string that the editor uses for even more context to hint. For instance, if hint is PROPERTY_HINT_RANGE, then hint_string is defined as "min,max" or "min,max,step", which will let the editor know the valid value range for our setting.

We now have an easy method for creating our new settings, but now we need to automate it. This can be done in a myriad of ways, which will largely be left as an exercise to the reader, but possibly the simplest form is as such:

tool
extends Node

func _init() -> void:
	
    # Example settings
	add_custom_project_setting("display/window/size/max_size", Vector2(0, 0), TYPE_VECTOR2)
	add_custom_project_setting("display/window/size/min_size", Vector2(0, 0), TYPE_VECTOR2)
	
	var error: int = ProjectSettings.save()
	if error: push_error("Encountered error %d when saving project settings." % error)

You'll notice that it's here that we actually save our changes to the project settings. When saving, you need to account for possible errors. This is because saving the project settings, actually writes out to the setting file, in both the editor and in an actual build. In this example, we don't do much in the way of error handling--we simply push an error to alert the user and continue on.