美文网首页UnityEditorunityunity3D技术分享
[Unity 3D] 将自定义配置整合到 ProjectSet

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

作者: 雨落随风 | 来源:发表于2022-10-17 22:54 被阅读0次

    在本文笔者将教大家如何将自己所写插件的全局配置绘制到 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<T> : 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<T>();
                }
                else
                {
                    Debug.LogError($"{nameof(ScriptableSingleton<T>)}: 请指定单例存档路径! ");
                }
            }
    
            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<FilePathAttribute>()
                      .FirstOrDefault(v => v != null)
                      ?.filepath;
            }
        }
        [AttributeUsage(AttributeTargets.Class)]
        public class FilePathAttribute : Attribute
        {
            internal string filepath;
            /// <summary>
            /// 单例存放路径
            /// </summary>
            /// <param name="path">相对 Project 路径</param>
            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;
    
    /// <summary>
    /// 监听编辑器状态,当编辑器重新 focus 时,重新加载实例,避免某些情景下 svn 、git 等外部修改了数据却无法同步的异常。
    /// </summary>
    [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] 将自定义配置整合到 ProjectSet

        本文链接:https://www.haomeiwen.com/subject/aebdzrtx.html