SettingsProvider and AssetSettingsProvider API

I’m playing with it and learning from github UnityCsReference.

My use case is that I want from different assemblies build a unique SettingsProvider, is it possible?

The only case I thought that Unity does it like this was PlayerSettings (each platform has it’s own settings) but I see that it uses “partial” class approach that it’s not possible when you are on a different assembly.

Related to this, is there a way to create a *.asset file outside “Assets” folder? In Unity examples they create all settings inside “ProjectSettings” folder but if I use “AssetDatabase.CreateAsset” API it throws an error.

Sorry you can not create assets outside of the Assets folder, all the ProjectSettings assets have been set up especially in C++. Its not a generic thing users can do. You could potentially use a package to store your settings although I would not recommend it.
We have actually started to move away from storing assets in the ProjectsFolder and now store them with assets. For example, the addressable package does this.

I do not believe it is possible to combine multiple partial classes from different assemblies into one settings class.
Perhaps you could

An alternative approach could be to have an abstract settings class that inherits from ScriptableObject.
Then have the multiple assemblies create their own settings class implementations and have your master settings class find all classes that implement the abstract class, instantiate them and keep them inside of a container scriptable object asset.

The SettingsProvider are not bound to an *.asset file. They are flexible enough that you can provide your own data model. That said, if you want to save data outside of the project Assets/ folder itself you can do your own asset serialization. Here’s an example that uses JSON file to save data:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using UnityEngine;
using Random = System.Random;

namespace UnityEditor
{
    [Serializable]
    public class SettingsPropertyData
    {
        public string name;
        public string label;
        public string type;
        public string value;
        public float min;
        public float max;
    }

    [Serializable]
    public class SettingsData
    {
        public string name;
        public string label;
        public string scope;
        public SettingsPropertyData[] settings;
    }

    public class GenericSettingsProvider : SettingsProvider
    {
        private string m_LocalFilePath;
        private SettingsPropertyData[] m_Properties;

        public GenericSettingsProvider(string path, string label, string scope, SettingsPropertyData[] properties, string jsonFilePath = null)
            : base(path)
        {
            this.label = label;
            if (scope == null)
                scopes = SettingsScopes.Any;
            else if (scope.ToLower() == "project")
                scopes = SettingsScopes.Project;
            else if (scope.ToLower() == "user")
                scopes = SettingsScopes.User;
            m_Properties = properties;
            m_LocalFilePath = jsonFilePath;
        }

        public override void OnGUI(string searchContext)
        {
            using (new SettingsWindow.GUIScope())
            {
                foreach (var property in m_Properties)
                {
                    if (searchContext.Length > 0 && !SearchUtils.MatchSearchGroups(searchContext, property.label))
                        continue;

                    using (new EditorGUILayout.HorizontalScope())
                        DrawPropertyControl(property);
                }

                GUILayout.Space(10.0f);

                using (new EditorGUILayout.HorizontalScope())
                {
                    GUILayout.FlexibleSpace();
                    if (GUILayout.Button("Show in Project"))
                    {
                        var asset = AssetDatabase.LoadMainAssetAtPath(m_LocalFilePath);
                        if (asset)
                            EditorGUIUtility.PingObject(asset);
                    }
                }
            }
        }

        public override bool HasSearchInterest(string searchContext)
        {
            if (SettingsTestsUtils.HasSearchInterest(m_Properties, searchContext))
            {
                return true;
            }

            return base.HasSearchInterest(searchContext);
        }

        private string GetPropertyKey(SettingsPropertyData property)
        {
            return SettingsTestsUtils.GetPropertyKey(settingsPath, property);
        }

        private void DrawPropertyControl(SettingsPropertyData property)
        {
            switch (property.type.ToLower())
            {
                case "string": DrawStringPropertyControl(property); break;
                case "number": DrawNumberPropertyControl(property); break;
                case "bool": DrawBooleanPropertyControl(property); break;
                case "color": DrawColorPropertyControl(property); break;
                default: throw new NotSupportedException("Property type " + property.type + " not supported");
            }
        }

        private void DrawColorPropertyControl(SettingsPropertyData property)
        {
            EditorGUI.BeginChangeCheck();
            var key = GetPropertyKey(property);

            var colorComponents = SettingsTestsUtils.GetFloats(property);
            Color colorValue = new Color();
            if (colorComponents != null && colorComponents.Length == 4)
                colorValue = new Color(colorComponents[0], colorComponents[1], colorComponents[2], colorComponents[3]);

            if (IsUserSettings())
            {
                colorValue.r = EditorPrefs.GetFloat(key + ".r", colorValue.r);
                colorValue.g = EditorPrefs.GetFloat(key + ".g", colorValue.g);
                colorValue.b = EditorPrefs.GetFloat(key + ".b", colorValue.b);
                colorValue.a = EditorPrefs.GetFloat(key + ".a", colorValue.a);
            }

            Color newColor = EditorGUILayout.ColorField(property.label, colorValue);
            if (EditorGUI.EndChangeCheck())
            {
                property.value = Json.Serialize(new[] {newColor.r, newColor.g, newColor.b, newColor.a});

                if (IsUserSettings())
                {
                    EditorPrefs.SetFloat(key + ".r", newColor.r);
                    EditorPrefs.SetFloat(key + ".g", newColor.g);
                    EditorPrefs.SetFloat(key + ".b", newColor.b);
                    EditorPrefs.SetFloat(key + ".a", newColor.a);
                }
                else
                    PersistSettings();
            }
        }

        private void DrawBooleanPropertyControl(SettingsPropertyData property)
        {
            EditorGUI.BeginChangeCheck();
            var key = GetPropertyKey(property);
            bool boolValue;
            if (property.value == null || !Boolean.TryParse(property.value, out boolValue))
                boolValue = false;
            var newValue = EditorGUILayout.Toggle(property.label, IsUserSettings() ? EditorPrefs.GetBool(key, boolValue) : boolValue);
            if (EditorGUI.EndChangeCheck())
            {
                property.value = Convert.ToString(newValue, CultureInfo.InvariantCulture);

                if (IsUserSettings())
                    EditorPrefs.SetBool(key, newValue);
                else
                    PersistSettings();
            }
        }

        private void DrawNumberPropertyControl(SettingsPropertyData property)
        {
            EditorGUI.BeginChangeCheck();
            var key = GetPropertyKey(property);
            var defaultValue = property.value != null ? Convert.ToSingle(property.value) : 0;
            var floatValue = IsUserSettings() ? EditorPrefs.GetFloat(key, defaultValue) : defaultValue;
            float newValue = floatValue;

            if (property.min < property.max)
                newValue = EditorGUILayout.Slider(property.label, floatValue, property.min, property.max);
            else
                newValue = EditorGUILayout.FloatField(property.label, IsUserSettings() ? EditorPrefs.GetFloat(key, floatValue) : floatValue);
            if (EditorGUI.EndChangeCheck())
            {
                property.value = Convert.ToString(newValue, CultureInfo.InvariantCulture);

                if (IsUserSettings())
                    EditorPrefs.SetFloat(key, newValue);
                else
                    PersistSettings();
            }
        }

        private void DrawStringPropertyControl(SettingsPropertyData property)
        {
            EditorGUI.BeginChangeCheck();
            var uniquePropertyKey = GetPropertyKey(property);
            var newValue = EditorGUILayout.TextField(property.label, IsUserSettings() ? EditorPrefs.GetString(uniquePropertyKey, property.value) : property.value);
            if (EditorGUI.EndChangeCheck())
            {
                property.value = newValue;

                if (IsUserSettings())
                    EditorPrefs.SetString(uniquePropertyKey, newValue);
                else
                    PersistSettings();
            }
        }

        private void PersistSettings()
        {
            if (String.IsNullOrEmpty(m_LocalFilePath))
                throw new FileNotFoundException("Project settings data cannot be saved.");
            var dataAsJson = File.ReadAllText(m_LocalFilePath);
            var data = JsonUtility.FromJson<SettingsData>(dataAsJson);
            data.settings = m_Properties;
            File.WriteAllText(m_LocalFilePath, JsonUtility.ToJson(data, true));
        }

        public static GenericSettingsProvider LoadFromFile(string path)
        {
            var dataAsJson = File.ReadAllText(path);
            var data = JsonUtility.FromJson<SettingsData>(dataAsJson);
            return new GenericSettingsProvider(data.name, data.label, data.scope, data.settings, path);
        }

        private bool IsUserSettings()
        {
            return (scopes & SettingsScopes.User) != 0;
        }

        [SettingsProviderGroup]
        internal static SettingsProvider[] FetchGenericSettingsProviderList()
        {
            var genericSettingsFilePaths = AssetDatabase.GetAllAssetPaths().Where(path => path.EndsWith(".settings")).ToArray();
            var genericSettingsProviders = new List<SettingsProvider>(genericSettingsFilePaths.Length);

            foreach (var genericSettingsFilePath in genericSettingsFilePaths)
            {
                if (!File.Exists(genericSettingsFilePath))
                    continue;
                try
                {
                    genericSettingsProviders.Add(LoadFromFile(genericSettingsFilePath));
                }
                catch
                {
                    Debug.LogError("Failed to parse settings file " + genericSettingsFilePath);
                }
            }

            return genericSettingsProviders.ToArray();
        }
    }
}
2 Likes

I know that there is a overload where you can pass directly an Object.

I mean that Unity stores all it’s settings in ProjectSettings folder so I supposed that it’s the standard that’s why I asked how to store my serialized settings

Your example doesn’t compile. This classes doesn’t exists:

  • SettingsWindow
  • SearchUtils
  • SettingsTestsUtils
  • Json