When creating plug-ins for Unity, the lack of simple, standardized support for project wide and per-user settings can be a bit frustrating. This article discusses an alternative to what is currently provided and includes a class that can be used to expose settings for use at runtime or in the editor with minimal effort.
EditorPrefs vs. ScriptableObjects vs. ScriptableSingleton
Unity’s
EditorPrefs API provides very basic support for storing user-specific settings in the editor but is of no help for project-wide settings. It requires additional work to read and write values and expose them to the Preferences and Project Settings dialogs. Another issue with EditorPrefs
is its lack of advanced serialization for classes, lists, or the ability to serialize references to other assets which make it less than ideal for more complicated plug-ins.
Using ScriptableObjects
for storing settings overcomes the limitations of EditorPrefs
and offers a few additional benefits such as being able to use field attributes like
[Range]
and
[Tooltip]
to customize the settings UI. And if a fully custom UI is needed, it can be implemented in a standard way by creating a
custom editor.
Unity eventually realized the usefulness of ScriptableObjects
for settings and added the ScriptableSingleton
class. Unfortunately, this only works in the editor, so it is also of no help for project-wide settings that need to be accessed at runtime. While it does allow settings files to be saved outside the Assets/
folder, it uses the InternalEditorUtility
class’s LoadSerializedFileAndForget()
and SaveSerializedFileAndForget()
methods to do so which negates some of the benefits of ScriptableObjects
. This means that all modifications to settings must call the Save()
method of ScriptableSingleton
, otherwise, changes will be lost during an assembly reload. For these reasons I opted not to use this class.
Settings Usage
There are three general types of settings that are encountered when working on a project or plugin for Unity:
- Editor User Settings: Per-user settings that are needed for the editor only. (Ex: Preferences / Scene View)
- Editor Project Settings: Project-wide settings that are needed for the editor only. (Ex: Project Settings / Version Control)
- Runtime Project Settings: Project-wide settings that runtime scripts need (read-only) access to but are configurable in the editor. (Ex: Project Settings / Physics)
While Runtime User Settings might seem like a fit here, they often have additional requirements (support for multiple players) and require different methods for serialization especially on consoles.
Settings File Locations
Ideally Unity would allow ScriptableObjects
to be stored in the ProjectSettings/
and UserSettings/
folders without the use of the InternalEditorUtility
class mentioned above. Since this is not allowed, a folder underneath Assets/
must be used for now. The following folders are used depending on the settings’ usage:
- Editor User Settings:
Assets/Settings/Editor/User/
stuff.- Important: This folder must be excluded from source control.
- Note: User settings are stored in a sub-directory with the same name as the current project folder. This allows for shallow cloning of a project (using symbolic links to the
Assets/
folder) so that multiple copies of the editor can be easily run for multiplayer game testing.
- Editor Project Settings:
Assets/Settings/Editor
- Runtime Project Settings:
Assets/Settings/Resources
- Use of the
Resources/
folder is required for runtime loading usingResources.Load()
.
- Use of the
Example
This example shows how to expose project-wide editor settings using the Settings
class and attribute:
using Hextant;
using Hextant.Editor;
using UnityEditor;
using UnityEngine;
[Settings( SettingsUsage.EditorProject, "My Plug-in Settings" )]
public sealed class MyPluginSettings : Settings<MyPluginSettings>
{
public int integerValue => _integerValue;
[SerializeField, Range( 0, 10 )] int _integerValue = 5;
public float floatValue => _floatValue;
[SerializeField, Range( 0, 100 )] float _floatValue = 25.0f;
public string stringValue => _stringValue;
[SerializeField, Tooltip( "A string value." )] string _stringValue = "Hello";
[SettingsProvider]
static SettingsProvider GetSettingsProvider() =>
instance.GetSettingsProvider();
}
Produces the following in the Edit/Project Settings dialog:
The MyPluginSettings
class simply derives from the Settings<T>
base class and uses the [Settings]
attribute to configure its usage, displayed name in the settings dialog, and optional filename (the type’s name is used if one is not specified). It is also possible to nest a Settings
instance under another in Unity’s settings dialog by specifying the name as a path using ‘/’ as a separator, e.g., [Settings( SettingsUsage.EditorProject, "Services/My Service" )]
Note: Be sure to name the derived settings class and its filename the same as required when deriving from Unity’s ScriptableObject
class.
To have the custom settings appear in the Preferences or Project Settings dialog, Unity’s [SettingsProvider]
attribute must be applied to a static method that returns the instance’s GetSettingsProvider()
result. Note that for settings used at runtime, this registration will need to be done in a separate file inside an Editor assembly (ideally Unity would have a way to register multiple settings providers at once so this could be avoided).
For plugins that manage their own settings UI, the SettingsProvider
code above can be omitted and the Settings.Set<T>()
method can be used to modify a setting which will call OnValidate()
and mark the settings asset dirty so that it will be saved:
public float floatValue
{
get => _floatValue;
set => Set( ref _floatValue, value );
}
[SerializeField] float _floatValue;
Finally, to access settings from code, the instance
property can be used:
MyPluginSettings.instance.floatValue = 33;
Validating Changes
As with other ScriptableObject
classes, the OnValidate()
method can be used to ensure values are within required ranges, etc.
protected override void OnValidate()
{
_floatValue = Math.Clamp( _floatValue, 0, 100 );
}
Note the use of the override
keyword as OnValidate()
is declared in the Settings
class so that it may easily be called by the Set<T>()
method.
Versioning
If a setting needs to be renamed, the
[FormerlySerializedAs]
attribute can be used to properly deserialize the previously saved value. However, there may be a script update that requires a more complicated change to an existing setting. This can be handled by overriding the OnValidate()
method and exposing a hidden _version
field that is used to track future versions:
protected override void OnValidate()
{
if( _version < 1 )
{
_integerValue = 7;
_version = 1;
}
}
[SerializeField, HideInInspector] int _version = 1; // Update default if needed.
Note the use of the [HideInInspector]
attribute to prevent the _version
field from being shown in the settings’ UI.
Runtime Initialization
Settings used at runtime will be loaded when the instance
property is first accessed. To force the load to happen earlier, the Settings.Initialize()
method can be called.
Keyword Search
Keyword search for settings in the Preferences and Project Settings dialogs is supported by the included ScriptableObjectSettingsProvider
which uses a helper method from the SettingsProvider
class to build a list of keywords from the settings instance.
Sub-Settings
More advanced plug-ins may wish to divide settings into additional nested classes. This can be done by having each derive from the SubSettings
class which provides a Set<T>()
method similar to the one in the Settings
class which calls the OnValidate()
method and marks the settings asset dirty. Note that derived classes will need to apply the [Serializable]
attribute or use the [SerializeReference]
attribute on fields that reference their instances.
using Hextant;
using Hextant.Editor;
using UnityEditor;
using UnityEngine;
[Settings( SettingsUsage.EditorProject, "My Project Settings" )]
public sealed class MyProjectSettings : Settings<MyProjectSettings>
{
[SettingsProvider]
static SettingsProvider GetSettingsProvider() => instance.GetSettingsProvider();
public int count { get => _count; set => Set( ref _count, value ); }
[SerializeField] int _count = 5;
// Example SubSettings class.
[System.Serializable]
public class AdvancedSettings : SubSettings
{
public string text { get => _text; set => Set( ref _text, value ); }
[SerializeField] string _text = "Some Text";
}
// The instance of the advanced settings.
public AdvancedSettings advancedSettings => _advancedSettings;
[SerializeField] AdvancedSettings _advancedSettings;
}
Produces the following results in the Project Settings dialog:
Download
- Settings.zip
- Hextant - Utilities (Git Repository & Package URL)