Files
Continentis/Assets/Scripts/SLSUtilities/SerializedDictionary/SerializedDictionary.cs

553 lines
22 KiB
C#
Raw Normal View History

2026-03-20 11:56:50 -04:00
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
#if UNITY_EDITOR
using System.Reflection;
using Sirenix.Utilities.Editor;
using Sirenix.OdinInspector.Editor;
using UnityEditor;
#endif
namespace SLSUtilities.General
{
[Serializable]
[HideLabel]
[LabelWidth(80)]
[InlineProperty]
public partial class SerializedDictionary<TKey, TValue, TPair> :
IEnumerable<KeyValuePair<TKey, TValue>>, ISerializationCallbackReceiver
where TPair : struct, ISerializedPair<TKey, TValue>
{
// ================= 配置与数据 =================
[SerializeField, HideInInspector]
private DuplicateKeyStrategy _duplicateStrategy = DuplicateKeyStrategy.LogError;
[LabelText("@SerializedDictionaryHelper.GetDictionaryTitle($property)")]
[SerializeField]
[InfoBox("检测到重复的 Key重复项已被标记为红色运行时将根据策略被忽略或报错。", InfoMessageType.Error, VisibleIf = "HasDuplicates")]
[ListDrawerSettings(ShowIndexLabels = false, OnTitleBarGUI = "DrawToolbar", OnBeginListElementGUI = "DrawListElementBackground")]
[Searchable]
[PropertyOrder(1)]
protected List<TPair> _entries = new List<TPair>();
// ================= 运行时缓存 =================
// 1. 查找表:提供 O(1) 访问
protected Dictionary<TKey, TValue> _lookup = new Dictionary<TKey, TValue>();
// 2. 顺序表:提供 O(n) 遍历顺序
// 我们只存 Key因为 Value 可以去 lookup 查,节省内存
protected List<TKey> _orderedKeys = new List<TKey>();
// ================= 公共接口 =================
[SerializeField, HideInInspector]
public float keyColumnWidth = 0.5f;
public int Count => _lookup.Count;
public TValue this[TKey key]
{
get
{
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild(); // 懒加载保护
return _lookup[key];
}
set
{
if (_lookup.Count == 0) Rebuild();
// 如果是新 Key需要加入顺序表
if (!_lookup.ContainsKey(key))
{
_orderedKeys.Add(key);
}
_lookup[key] = value;
}
}
public bool ContainsKey(TKey key)
{
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
return _lookup.ContainsKey(key);
}
public bool TryGetValue(TKey key, out TValue value)
{
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
return _lookup.TryGetValue(key, out value);
}
public void Add(TKey key, TValue value)
{
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
if (_lookup.ContainsKey(key)) throw new ArgumentException($"[SerializedDictionary] Key 已经存在: {key}");
TPair temp = new TPair();
temp.Key = key;
temp.Value = value;
_entries.Add(temp);
_orderedKeys.Add(key);
_lookup.Add(key, value);
}
public void Clear()
{
_entries.Clear();
_lookup.Clear();
_orderedKeys.Clear();
}
public bool Remove(TKey key)
{
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
if (_lookup.Remove(key))
{
_orderedKeys.Remove(key);
for (int i = 0; i < _entries.Count; i++)
{
if (EqualityComparer<TKey>.Default.Equals(_entries[i].Key, key))
{
_entries.RemoveAt(i);
break;
}
}
return true;
}
return false;
}
public Dictionary<TKey, TValue> ToDictionary()
{
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
return new Dictionary<TKey, TValue>(_lookup);
}
public ICollection<TKey> Keys
{
get
{
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
return _orderedKeys;
}
}
// ================= 核心:排序与重建 =================
/// <summary>
/// 重建并排序。
/// </summary>
/// <param name="sortMethod">
/// 自定义排序方法。
/// 例如按 Value 降序:(a, b) => b.Value.CompareTo(a.Value)
/// </param>
public void Rebuild(Comparison<KeyValuePair<TKey, TValue>> sortMethod = null)
{
_lookup.Clear();
_orderedKeys.Clear();
// 1. 先构建基础数据
foreach (var entry in _entries)
{
TKey key = entry.Key;
// 过滤空 Key
if (key == null || (key is string s && string.IsNullOrEmpty(s))) continue;
if (_lookup.ContainsKey(key))
{
HandleDuplicate(key, entry.Value);
}
else
{
_lookup.Add(key, entry.Value);
_orderedKeys.Add(key);
}
}
// 2. 如果提供了排序方法,对 _orderedKeys 进行排序
if (sortMethod != null)
{
// List.Sort 默认不支持 KeyValuePair 的 Comparison我们需要转接一下
_orderedKeys.Sort((keyA, keyB) =>
{
var pairA = new KeyValuePair<TKey, TValue>(keyA, _lookup[keyA]);
var pairB = new KeyValuePair<TKey, TValue>(keyB, _lookup[keyB]);
return sortMethod(pairA, pairB);
});
}
}
private void HandleDuplicate(TKey key, TValue newValue)
{
switch (_duplicateStrategy)
{
case DuplicateKeyStrategy.LogError:
Debug.LogError($"[SerializedDictionary] 重复 Key: '{key}'。");
break;
case DuplicateKeyStrategy.Overwrite:
_lookup[key] = newValue;
break;
// Ignore: do nothing
}
}
// ================= 序列化回调 =================
public void OnBeforeSerialize() { }
public void OnAfterDeserialize()
{
// 编辑器下不自动构建,为了性能。
// 运行时通常在 Awake/OnEnable 显式调用 Rebuild(sorter)。
_lookup.Clear();
_orderedKeys.Clear();
}
// ================= 迭代器 (核心魔法) =================
// 这里我们不遍历 Dictionary而是遍历 _orderedKeys
// 这样就能保证 foreach 的顺序是我们排好序的顺序
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
if (_lookup.Count == 0 && _entries.Count > 0) Rebuild();
foreach (var key in _orderedKeys)
{
yield return new KeyValuePair<TKey, TValue>(key, _lookup[key]);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
#if UNITY_EDITOR
public partial class SerializedDictionary<TKey, TValue, TPair>
{
// 缓存重复的 Key避免在绘制每一行时重复计算 O(N^2)
private HashSet<TKey> _duplicateKeysCache = new HashSet<TKey>();
private bool HasDuplicates => _duplicateKeysCache.Count > 0;
[OnInspectorGUI]
private void UpdateDuplicateCache()
{
// 简单高效的统计:找到所有出现次数 > 1 的 Key
// 这里使用 LINQ GroupBy对于几百个元素的配置表性能完全没问题
// 如果列表极大(上千条),可能需要优化,但通常配置表不会这么大
_duplicateKeysCache.Clear();
var duplicates = _entries.Select(e => e.Key)
.Where(k => k != null).GroupBy(k => k)
.Where(g => g.Count() > 1).Select(g => g.Key);
foreach (var key in duplicates)
{
_duplicateKeysCache.Add(key);
}
}
// ★ 核心魔法:绘制背景 ★
// 这个方法会被 List Drawer 在绘制每个元素前调用
private void DrawListElementBackground(int index)
{
if (index < 0 || index >= _entries.Count) return;
TKey key = _entries[index].Key;
// 如果这个 Key 是重复的
if (key != null && _duplicateKeysCache.Contains(key))
{
// 获取当前行的高度和矩形区域
Rect rect = GUIHelper.GetCurrentLayoutRect();
// 绘制红色背景 (Color 后的参数是 RGBAA=0.3f 让它半透明不遮挡文字)
SirenixEditorGUI.DrawSolidRect(rect, new Color(1f, 0.2f, 0.2f, 0.3f));
}
}
private string _keyLabelCache = "";
private string _valueLabelCache = "";
private float _keyWidthRatioCache = 0.5f;
protected virtual void DrawHeader()
{
}
// 2. 新增虚方法,作为 Toolbar 的主入口
protected virtual void DrawToolbar(InspectorProperty listProperty)
{
// 1. 绘制去重策略按钮
DrawDuplicateStrategySelector();
// 2. ★ 核心逻辑:绘制自定义 Attribute 按钮 ★
// listProperty.Parent 就是持有这个字典的字段 (例如 characterAttributes)
var dictProperty = listProperty.Parent;
if (dictProperty == null) return;
// 获取该字段上所有的 [DictionaryToolbarAction]
// 需要引用 System.Linq
var actions = dictProperty.Attributes.OfType<ToolbarButtonAttribute>();
foreach (var action in actions)
{
if (SirenixEditorGUI.ToolbarButton(action.Icon))
{
InvokeAction(action, dictProperty);
}
Rect btnRect = GUILayoutUtility.GetLastRect();
if (!string.IsNullOrEmpty(action.Tooltip))
{
GUI.Label(btnRect, new GUIContent("", action.Tooltip));
}
}
if (SirenixEditorGUI.ToolbarButton(SdfIconType.ArrowLeftRight)) // 换了个更贴切的图标
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("10%"), Mathf.Approximately(keyColumnWidth, 0.1f), () => keyColumnWidth = 0.1f);
menu.AddItem(new GUIContent("30%"), Mathf.Approximately(keyColumnWidth, 0.3f), () => keyColumnWidth = 0.3f);
menu.AddItem(new GUIContent("50%"), Mathf.Approximately(keyColumnWidth, 0.5f), () => keyColumnWidth = 0.5f);
menu.AddItem(new GUIContent("70%"), Mathf.Approximately(keyColumnWidth, 0.7f), () => keyColumnWidth = 0.7f);
menu.AddItem(new GUIContent("90%"), Mathf.Approximately(keyColumnWidth, 0.9f), () => keyColumnWidth = 0.9f);
menu.ShowAsContext();
}
_keyLabelCache = dictProperty.GetAttribute<SerializedDictionarySettingsAttribute>()?.KeyLabel ?? string.Empty;
_valueLabelCache = dictProperty.GetAttribute<SerializedDictionarySettingsAttribute>()?.ValueLabel ?? string.Empty;
_keyWidthRatioCache = (dictProperty.ValueEntry.WeakSmartValue as SerializedDictionary<TKey, TValue>)?.keyColumnWidth ?? 0.5f;
DrawTitle(listProperty);
}
protected void DrawTitle(InspectorProperty listProperty)
{
if (!listProperty.State.Expanded || _entries.Count == 0) return;
if (string.IsNullOrEmpty(_keyLabelCache) && string.IsNullOrEmpty(_valueLabelCache)) return;
GUILayout.EndHorizontal();
float keyWidthRatio = _keyWidthRatioCache;
float totalWidth = EditorGUIUtility.currentViewWidth - 80f + 50f;
float keyPixelWidth = totalWidth * keyWidthRatio;
float valuePixelWidth = totalWidth - keyPixelWidth;
// 4. 绘制背景条 (使用深色背景,模拟 Toolbar 的延伸)
// 预留 22px 高度
Rect headerRect = GUILayoutUtility.GetRect(totalWidth, 20f);
Rect insideRect = new Rect(headerRect.x + 1, headerRect.y, headerRect.width - 2, headerRect.height);
SirenixEditorGUI.DrawSolidRect(insideRect, Color.gray8);
SirenixEditorGUI.DrawSolidRect(headerRect, SirenixGUIStyles.BorderColor);
// 5. 绘制文字 (使用 BeginIndentedHorizontal 确保和 List 内容对齐)
SirenixEditorGUI.BeginIndentedHorizontal();
{
// 计算 Label 区域
Rect contentRect = new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height);
Rect keyRect = new Rect(contentRect.x, contentRect.y, keyPixelWidth, contentRect.height);
Rect valueRect = new Rect(keyRect.xMax, contentRect.y, valuePixelWidth, contentRect.height);
// Draw Key
if (!string.IsNullOrEmpty(_keyLabelCache))
{
GUI.Label(keyRect, _keyLabelCache, SirenixGUIStyles.CenteredWhiteMiniLabel);
}
// Draw Splitter
SirenixEditorGUI.DrawSolidRect(new Rect(keyRect.xMax, keyRect.y, 1, keyRect.height), SirenixGUIStyles.BorderColor);
// Draw Value
if (!string.IsNullOrEmpty(_valueLabelCache))
{
GUI.Label(valueRect, _valueLabelCache, SirenixGUIStyles.CenteredWhiteMiniLabel);
}
}
SirenixEditorGUI.EndIndentedHorizontal();
// =======================================================
// ★ 恢复布局 ★
// =======================================================
// 我们必须重新开启一个 Horizontal 布局。
// 否则 Odin 在 DrawToolbar 结束后尝试关闭布局时会报错(因为我们已经手动关闭了)。
GUILayout.BeginHorizontal();
}
private void InvokeAction(ToolbarButtonAttribute action, InspectorProperty dictProperty)
{
string methodName = action.MethodName;
// 获取包含该方法的对象实例 (例如 EditorBaseCollection 的实例)
object targetObject = dictProperty.ParentValues[0];
Type targetType = targetObject.GetType();
// 反射查找方法
MethodInfo method = targetType.GetMethod(methodName,
BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (method == null)
{
Debug.LogError($"[DictionaryToolbarAction] 在 {targetType.Name} 中找不到方法 '{methodName}'");
return;
}
// 执行方法 (支持传参)
var parameters = method.GetParameters();
if (parameters.Length == 0)
{
method.Invoke(targetObject, null);
}
else if (parameters.Length == 1)
{
// 如果方法有一个参数,尝试传入字典本身
// ValueEntry.WeakSmartValue 是字典的实例
method.Invoke(targetObject, new object[] { dictProperty.ValueEntry.WeakSmartValue });
}
else
{
Debug.LogError($"[DictionaryToolbarAction] 方法 '{methodName}' 参数过多。仅支持无参或接收字典参数。");
}
}
/// <summary>
/// 这个方法会被 Odin 在绘制 List 标题栏时调用
/// </summary>
private void DrawDuplicateStrategySelector()
{
if (SirenixEditorGUI.ToolbarButton(SdfIconType.Gear))
{
// 当按钮被点击时,创建一个通用菜单 (GenericMenu)
GenericMenu menu = new GenericMenu();
// 遍历枚举的所有选项,添加到菜单中
foreach (DuplicateKeyStrategy strategy in Enum.GetValues(typeof(DuplicateKeyStrategy)))
{
// 添加菜单项:当用户点击某一项时,执行 Lambda 表达式修改值
menu.AddItem(new GUIContent(strategy.ToString()), _duplicateStrategy == strategy, () =>
{
_duplicateStrategy = strategy;
// 强制标记脏数据,确保 Unity 保存修改
// 注意:在 ScriptableObject 中可能不需要显式 SetDirty但在某些嵌套结构中是必要的
});
}
// 在鼠标位置显示菜单
menu.ShowAsContext();
}
}
}
#endif
/// <summary>
/// 通用版序列化字典。
/// 不需要定义 Pair 结构体直接使用SerializedDictionary<string, float>
/// 保留了排序、Toolbar 策略和自定义标签等所有功能。
/// </summary>
[Serializable]
public class SerializedDictionary<TKey, TValue> : SerializedDictionary<TKey, TValue, SerializedDictionary<TKey, TValue>.SimplePair>
{
[Serializable]
public struct SimplePair : ISerializedPair<TKey, TValue>
{
[HideLabel]
public TKey key;
[HideLabel]
public TValue value;
// 接口实现
public TKey Key { get => key; set => key = value; }
public TValue Value { get => value; set => this.value = value; }
}
}
#if UNITY_EDITOR
public static class SerializedDictionaryHelper
{
public static object GetDictionaryFromKey(InspectorProperty keyProperty)
{
if (keyProperty == null) return null;
var dictProperty = keyProperty.Parent?.Parent?.Parent?.Parent;
if (dictProperty == null) return null;
return dictProperty.ValueEntry.WeakSmartValue;
}
public static readonly GUIStyle NoMargin = new GUIStyle
{
margin = new RectOffset(0, 0, 0, 0),
padding = new RectOffset(0, 0, 0, 0),
overflow = new RectOffset(0, 0, 0, 0)
};
/// <summary>
/// 这是一个辅助方法,供 Odin 的 @表达式 调用
/// </summary>
public static string GetDictionaryTitle(InspectorProperty listProperty)
{
// listProperty 是 _entries 列表
// listProperty.Parent 是 SerializedDictionary 实例本身的 Property
var dictProperty = listProperty.Parent;
if (dictProperty == null) return "Dictionary";
// 1. 优先查找 [DictionaryTitle]
// 在 C# 代码里,我们可以随意使用 GetAttribute<T> 泛型扩展方法
var titleAttr = dictProperty.GetAttribute<DictionaryTitleAttribute>();
if (titleAttr != null) return titleAttr.Title;
// 2. 其次查找 [LabelText]
var labelAttr = dictProperty.GetAttribute<LabelTextAttribute>();
if (labelAttr != null) return labelAttr.Text;
// 3. 最后使用 NiceName (变量名美化)
return dictProperty.NiceName;
}
}
// 注意:这里的泛型声明 <TKey, TValue> 是为了匹配目标类型
// 目标类型是SerializedDictionary<TKey, TValue>.SimplePair
public class SerializedDictionarySimplePairDrawer<TKey, TValue> : OdinValueDrawer<SerializedDictionary<TKey, TValue>.SimplePair>
{
protected override void DrawPropertyLayout(GUIContent label)
{
var property = this.Property.Parent.Parent;
var dictionary = property.ValueEntry.WeakSmartValue as SerializedDictionary<TKey, TValue>;
float keyWidthRatio = dictionary?.keyColumnWidth ?? 0.5f;
//var settings = property.GetAttribute<SerializedDictionarySettingsAttribute>();
float totalWidth = EditorGUIUtility.currentViewWidth - 80f;
float keyPixelWidth = totalWidth * keyWidthRatio;
SirenixEditorGUI.BeginHorizontalPropertyLayout(label);
{
var keyProp = this.Property.Children["key"];
var valueProp = this.Property.Children["value"];
GUILayout.BeginVertical(SerializedDictionaryHelper.NoMargin, GUILayout.Width(keyPixelWidth));
keyProp.Draw();
GUILayout.EndVertical();
//绘制分割线
Rect dividerRect = new Rect(GUILayoutUtility.GetLastRect().xMax, GUILayoutUtility.GetLastRect().y, 1, GUILayoutUtility.GetLastRect().height);
SirenixEditorGUI.DrawSolidRect(dividerRect, SirenixGUIStyles.BorderColor);
GUILayout.BeginVertical(SerializedDictionaryHelper.NoMargin);
valueProp.Draw();
GUILayout.EndVertical();
}
SirenixEditorGUI.EndHorizontalPropertyLayout();
}
}
#endif
public enum DuplicateKeyStrategy { LogError, Ignore, Overwrite }
public interface ISerializedPair<TKey, TValue>
{
TKey Key { get; set; }
TValue Value { get; set; }
}
}