Unity 编辑器之 SettingsProvider、Hierarchy、Project、SearchField

1. SettingProvider

1. 概念

Preference 是 user 级别的,不进 git,存储方式可以用 EditorPref;ProjectSetting 是项目级别的,进 git,通过创建 ScriptableObject 来生成 .asset 文件,供团队共用。

2. 与 .asset 的关系

3. 示例

分为两种形式:

一种是通过手动创建 ScriptableObject,把路径设置为 ProjectSettings/custom_plugin.asset,然后直接通过 File.ReadAllText 读取,使用 JsonUtility 类写入。此时 asset 文件里面是 JSON 格式,不是 YAML 格式。

另一种是通过 Unity 封装的方法自动创建 ScriptableObject,asset 文件里面是 YAML 格式(推荐)。

3.1 第一种
cs 复制代码
// Assets/Scripts/MySDKSettings.cs
using UnityEngine;

[System.Serializable]
public class MySDKSettings : ScriptableObject
{
    public const string k_Path = "ProjectSettings/MySDKSettings.asset";

    public bool enableDebug = false;
    public string endpoint = "https://prod.example.com";
    public int maxRetry = 3;

    // ===== 运行时访问 =====
    private static MySDKSettings s_Instance;
    public static MySDKSettings Instance
    {
        get
        {
            if (s_Instance == null)
                s_Instance = LoadOrCreate();
            return s_Instance;
        }
    }

#if UNITY_EDITOR
    public static MySDKSettings LoadOrCreate()
    {
        MySDKSettings settings;

        if (System.IO.File.Exists(k_Path))
        {
            settings = CreateInstance<MySDKSettings>();
            var json = System.IO.File.ReadAllText(k_Path);
            JsonUtility.FromJsonOverwrite(json, settings);
        }
        else
        {
            settings = CreateInstance<MySDKSettings>();
            Save(settings);
        }

        return settings;
    }

    public static void Save(MySDKSettings settings)
    {
        var json = JsonUtility.ToJson(settings, true);
        System.IO.File.WriteAllText(k_Path, json);
        UnityEditor.AssetDatabase.Refresh();
    }
#else
    // 运行时:不允许访问
    public static MySDKSettings LoadOrCreate()
    {
        throw new System.NotSupportedException("MySDKSettings cannot be loaded at runtime from ProjectSettings");
    }
#endif
}
cs 复制代码
// Assets/Editor/MySDKSettingsProvider.cs
using UnityEditor;
using UnityEngine;

[SettingsProvider]
static SettingsProvider CreateMySDKSettingsProvider()
{
    return new SettingsProvider("Project/My SDK", SettingsScope.Project)
    {
        label = "My SDK",
        guiHandler = (searchContext) =>
        {
            var settings = MySDKSettings.Instance;
            var so = new SerializedObject(settings);

            using (var scope = new EditorGUI.ChangeCheckScope())
            {
                EditorGUILayout.PropertyField(so.FindProperty("enableDebug"));
                EditorGUILayout.PropertyField(so.FindProperty("endpoint"));
                EditorGUILayout.PropertyField(so.FindProperty("maxRetry"));

                if (scope.changed)
                {
                    so.ApplyModifiedProperties();
                    MySDKSettings.Save(settings);
                }
            }

            EditorGUILayout.HelpBox("配置保存在 ProjectSettings/MySDKSettings.asset", MessageType.Info);
        },

        keywords = new System.Collections.Generic.HashSet<string>(new[] { "MySDK", "Debug", "Endpoint" })
    };
}
3.2 第二种
cs 复制代码
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.UIElements;

[FilePath("ProjectSettings/MyCustomPlugin.asset", FilePathAttribute.Location.ProjectFolder)]
public class MyScriptableObject : ScriptableSingleton<MyScriptableObject>
{
    public string testString;
    public int testInt;

    // save 方法是 protected,必须重新封装一下
    public void SaveChange(bool saveAsText)
    {
        Save(saveAsText); // 必须手动 Save,才会写盘
    }
}

public class SettingProviderSample : SettingsProvider
{
    private SerializedObject serializeObj;
    private SerializedProperty serializeFieldInt;
    private SerializedProperty serializeFieldString;

    [SettingsProvider]
    static SettingsProvider CreateMyProjectSettings()
    {
        var provider = new SettingProviderSample("Project/My Plugin", SettingsScope.Project)
        {
            label = "My Plugin", // 左侧列表显示的名字
            keywords = new[] { "A", "Custom", "Ray" }
        };
        return provider;
    }

    private SettingProviderSample(string path, SettingsScope scopes, IEnumerable<string> keywords = null) : base(path,
        scopes, keywords)
    {
    }

    public override void OnActivate(string searchContext, VisualElement rootElement)
    {
        base.OnActivate(searchContext, rootElement);
        serializeObj = new SerializedObject(MyScriptableObject.instance);
        serializeFieldInt = serializeObj.FindProperty("testInt");
        serializeFieldString = serializeObj.FindProperty("testString");

        EditorGUILayout.PropertyField(serializeObj.FindProperty("testString"));
    }

    public override bool HasSearchInterest(string searchContext)
    {
        return base.HasSearchInterest(searchContext);
        return searchContext.Contains("A");
    }

    public override void OnGUI(string searchContext)
    {
        base.OnGUI(searchContext);

        serializeObj.UpdateIfRequiredOrScript();

        EditorGUI.BeginChangeCheck();

        EditorGUILayout.BeginFadeGroup((string.IsNullOrEmpty(searchContext) || !searchContext.Contains("testInt"))
            ? 1
            : 0);
        EditorGUILayout.PropertyField(serializeFieldString);
        EditorGUILayout.EndFadeGroup();
        EditorGUILayout.PropertyField(serializeFieldInt);


        if (EditorGUI.EndChangeCheck())
        {
            serializeObj.ApplyModifiedProperties();
            MyScriptableObject.instance.SaveChange(true);
        }
    }
}

2. SearchContext

HasSearchInterest 的优先级大于 keywords,同时存在时,以 HasSearchInterest 为准。

3. EditorApplication.hierarchyWindowItemOnGUI

cs 复制代码
[InitializeOnLoad]
public class HierarchyIcon
{
    private static Texture2D myIcon;
    static HierarchyIcon()
    {
        myIcon = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Samples/Sprite/icon_1.png");
        EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
    }
    
    static void OnHierarchyGUI(int instanceID, Rect selectionRect)
    {
        var go = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
        if (go != null && go.GetComponent<PathTransform>() != null)
        {
            Rect r = new Rect(selectionRect);
            r.x = 120;
            r.size = new Vector2(20, 20);
            GUI.DrawTexture(r, myIcon);
        }
    }
}

4. EditorApplication.projectWindowItemOnGUI

cs 复制代码
[InitializeOnLoad]
public class ProjectIcon
{
    private static Texture2D myIcon;
    static ProjectIcon()
    {
        myIcon = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Samples/Sprite/icon_2.png");
        EditorApplication.projectWindowItemOnGUI += OnHierarchyGUI;
    }
    
    static void OnHierarchyGUI(string guid, Rect selectionRect)
    {
        var go = AssetDatabase.GUIDToAssetPath(guid);
        if (go != null && go.Contains("PathTransform"))
        {
            Rect r = new Rect(selectionRect);
            r.x = 150;
            r.size = new Vector2(20, 20);
            GUI.DrawTexture(r, myIcon);
        }
    }
}

5. PropertyDrawer

6. DecoratorDrawer

7. 自定义搜索框

相关推荐
吴声子夜歌2 小时前
Java面试——Spark原理及应用(二)
java·面试·spark
吴声子夜歌2 小时前
Java面试——Spark原理及应用(一)
java·面试·spark
慧一居士3 小时前
ThreadLocal 功能及使用场景介绍,使用详解
java
zh73143 小时前
typephp的编译核心phpx原理解析
android·java·开发语言
2401_894915535 小时前
GEO 优化源码全解析:从搜索引擎到 AI 引擎的底层改写逻辑
java·服务器·前端·数据库·人工智能·分布式·搜索引擎
Java牛马9 小时前
SpringBoot Starter 依赖相关总结
java·spring·springboot·starter·自动装配·依赖
吴声子夜歌9 小时前
Java面试——Spring Cloud原理及应用(二)
java·spring cloud·面试
yj爆裂鼓手9 小时前
spine绑定骨骼运动工具&spine纹理压缩自动设置工具
游戏·unity
东小西10 小时前
【SAA实战】第 1 篇:ReactAgent 入门——先撸个"会调工具的助手"跑起来
java·人工智能·spring