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. 自定义搜索框
