[Unity 3D] 将自定义配置整合到 ProjectSettings

在本文笔者将教大家如何将自己所写插件的全局配置绘制到 ProjectSettings , 同时将配置文件存放在 ProjectSettings 目录下。

前言

HybridCLR 配置项均为编辑器下生效,这种配置文件放置在项目中就会对原有项目有侵入,但是放在 ProjectSettings 文件夹中就会很完美,这作用域拿捏的死死的;同时,将 HybridCLR Settings 绘制到 ProjectSettings 面板,更显优雅。在此背景下,我提了 PR,随便作此文以记之,希望能够帮助到需要的朋友。

Settings For HybridCLR

实现

  1. 通过继承:SettingsProvider 重载 OnTitleBarGUI 函数绘制标题栏右侧三个按钮,他们是:文档、Presets、Reset。
  2. 通过继承:SettingsProvider 重载 OnGUI 函数绘制设置面板主体,调用的核心 API 如下。
m_SerializedObject.Update(); // 更新序列化 数据文件
EditorGUI.BeginChangeCheck(); // 开始检查面板修改
EditorGUILayout.PropertyField(m_Enable); // 使用默认字段风格绘制字段
...
此处省略同质代码若干
...
if (EditorGUI.EndChangeCheck()) // 结束面板修改的检查
{
    m_SerializedObject.ApplyModifiedProperties();  // 应用修改了的属性值
    HybridCLRSettings.Instance.Save(); //存储单例数据到 ProjectSettings 文件夹
}
  1. 通过 ScriptableObject 单例实现配置数据的唯一性、实现数据存储到 ProjectSettings ,其中InternalEditorUtility.LoadSerializedFileAndForget(filePath) 函数实现了 ScriptableObject 资产的 Assets 文件夹外的加载。
    InternalEditorUtility.SaveToSerializedFileAndForget(obj, filePath, saveAsText); 函数实现了 ScriptableObject 资产的 Assets 文件夹外的保存。
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

namespace HybridCLR.Editor
{
    public class ScriptableSingleton : ScriptableObject where T : ScriptableObject
    {
        private static T s_Instance;
        public static T Instance
        {
            get
            {
                if (!s_Instance)
                {
                    LoadOrCreate();
                }
                return s_Instance;
            }
        }
        public static void LoadOrCreate()
        {
            string filePath = GetFilePath();
            if (!string.IsNullOrEmpty(filePath))
            {
                var arr = InternalEditorUtility.LoadSerializedFileAndForget(filePath);
                s_Instance = arr.Length > 0 ? arr[0] as T : s_Instance??CreateInstance();
            }
            else
            {
                Debug.LogError($"{nameof(ScriptableSingleton)}: 请指定单例存档路径! ");
            }
        }

        public void Save(bool saveAsText = true)       
        {
            if (!s_Instance)
            {
                Debug.LogError("Cannot save ScriptableSingleton: no instance!");
                return;
            }

            string filePath = GetFilePath();
            if (!string.IsNullOrEmpty(filePath))
            {
                string directoryName = Path.GetDirectoryName(filePath);
                if (!Directory.Exists(directoryName))
                {
                    Directory.CreateDirectory(directoryName);
                }
                UnityEngine.Object[] obj = new T[1] { s_Instance };
                InternalEditorUtility.SaveToSerializedFileAndForget(obj, filePath, saveAsText);
            }
        }
        protected static string GetFilePath()
        {
            return typeof(T).GetCustomAttributes(inherit: true)
                  .Cast()
                  .FirstOrDefault(v => v != null)
                  ?.filepath;
        }
    }
    [AttributeUsage(AttributeTargets.Class)]
    public class FilePathAttribute : Attribute
    {
        internal string filepath;
        /// 
        /// 单例存放路径
        /// 
        /// 相对 Project 路径
        public FilePathAttribute(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentException("Invalid relative path (it is empty)");
            }
            if (path[0] == '/')
            {
                path = path.Substring(1);
            }
            filepath = path;
        }
    }
}
  1. 通过继承 PresetSelectorReceiver 实现配置的 Preset(配置预设)。
using UnityEditor;
using UnityEditor.Presets;
using UnityEngine;

namespace HybridCLR.Editor
{
    public class SettingsPresetReceiver : PresetSelectorReceiver
    {
        private Object m_Target;
        private Preset m_InitialValue;
        private SettingsProvider m_Provider;
        internal void Init(Object target, SettingsProvider provider)
        {
            m_Target = target;
            m_InitialValue = new Preset(target);
            m_Provider = provider;
        }
        public override void OnSelectionChanged(Preset selection)
        {
            if (selection != null)
            {
                Undo.RecordObject(m_Target, "Apply Preset " + selection.name);
                selection.ApplyTo(m_Target);
            }
            else
            {
                Undo.RecordObject(m_Target, "Cancel Preset");
                m_InitialValue.ApplyTo(m_Target);
            }
            m_Provider.Repaint();
        }
        public override void OnSelectionClosed(Preset selection)
        {
            OnSelectionChanged(selection);
            Object.DestroyImmediate(this);
        }
    }
}
  1. 为了保证外部对配置的修改生效,监测 InternalEditorUtility.isApplicationActive 属性,在编辑器重新 focus 时重新加载单例并通过监听 OnEditorFocused 重绘 ProjectSettings 面板。如果想要精准一些,可以获取文件属性,有修改则重新加载。
using HybridCLR.Editor;
using System;
using UnityEditor;
using UnityEditorInternal;

/// 
/// 监听编辑器状态,当编辑器重新 focus 时,重新加载实例,避免某些情景下 svn 、git 等外部修改了数据却无法同步的异常。
/// 
[InitializeOnLoad]
public static class EditorStatusWatcher
{
    public static Action OnEditorFocused;
    static bool isFocused;
    static EditorStatusWatcher() => EditorApplication.update += Update;
    static void Update()
    {
        if (isFocused != InternalEditorUtility.isApplicationActive)
        {
            isFocused = InternalEditorUtility.isApplicationActive;
            if (isFocused)
            {
                HybridCLRSettings.LoadOrCreate();
                OnEditorFocused?.Invoke();
            }
        }
    }
}

结语

有尝试过使用 Unity 自己的 ScriptableSingleton ,但最终不得不放弃,原因如下:

  1. 因为其 hideflag 为 dontsave ,所以绘制到 ProjectSettings 的数据无法编辑(在 HybridCLRSettingsProviderOnActivate 中插入 HybridCLRSettings.Instance.hideFlags &= ~HideFlags.NotEditable; 可解决不能编辑的问题)。
    2.,此单例基类的构造函数的实现与 Presets 功能不兼容,会报错。

版权所有,转载请注明出处

你可能感兴趣的:([Unity 3D] 将自定义配置整合到 ProjectSettings)