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 using Resources.Load().

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:

Custom editor settings for Unity.

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 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:

SubSettings example for custom editor settings for Unity.

Download