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 : IEnumerable>, ISerializationCallbackReceiver where TPair : struct, ISerializedPair { // ================= 配置与数据 ================= [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 _entries = new List(); // ================= 运行时缓存 ================= // 1. 查找表:提供 O(1) 访问 protected Dictionary _lookup = new Dictionary(); // 2. 顺序表:提供 O(n) 遍历顺序 // 我们只存 Key,因为 Value 可以去 lookup 查,节省内存 protected List _orderedKeys = new List(); // ================= 公共接口 ================= [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); } // ================= 核心:排序与重建 ================= /// /// 重建并排序。 /// /// /// 自定义排序方法。 /// 例如按 Value 降序:(a, b) => b.Value.CompareTo(a.Value) /// public void Rebuild(Comparison> 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(keyA, _lookup[keyA]); var pairB = new KeyValuePair(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> GetEnumerator() { if (_lookup.Count == 0 && _entries.Count > 0) Rebuild(); foreach (var key in _orderedKeys) { yield return new KeyValuePair(key, _lookup[key]); } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } #if UNITY_EDITOR public partial class SerializedDictionary { // 缓存重复的 Key,避免在绘制每一行时重复计算 O(N^2) private HashSet _duplicateKeysCache = new HashSet(); 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 后的参数是 RGBA,A=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(); 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()?.KeyLabel ?? string.Empty; _valueLabelCache = dictProperty.GetAttribute()?.ValueLabel ?? string.Empty; _keyWidthRatioCache = (dictProperty.ValueEntry.WeakSmartValue as SerializedDictionary)?.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}' 参数过多。仅支持无参或接收字典参数。"); } } /// /// 这个方法会被 Odin 在绘制 List 标题栏时调用 /// 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 /// /// 通用版序列化字典。 /// 不需要定义 Pair 结构体,直接使用:SerializedDictionary /// 保留了排序、Toolbar 策略和自定义标签等所有功能。 /// [Serializable] public class SerializedDictionary : SerializedDictionary.SimplePair> { [Serializable] public struct SimplePair : ISerializedPair { [HideLabel] public TKey key; [HideLabel] public TValue value; // 接口实现 public TKey Key => key; public TValue 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) }; /// /// 这是一个辅助方法,供 Odin 的 @表达式 调用 /// 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 泛型扩展方法 var titleAttr = dictProperty.GetAttribute(); if (titleAttr != null) return titleAttr.Title; // 2. 其次查找 [LabelText] var labelAttr = dictProperty.GetAttribute(); if (labelAttr != null) return labelAttr.Text; // 3. 最后使用 NiceName (变量名美化) return dictProperty.NiceName; } } // 注意:这里的泛型声明 是为了匹配目标类型 // 目标类型是:SerializedDictionary.SimplePair public class SerializedDictionarySimplePairDrawer : OdinValueDrawer.SimplePair> { protected override void DrawPropertyLayout(GUIContent label) { var property = this.Property.Parent.Parent; var dictionary = property.ValueEntry.WeakSmartValue as SerializedDictionary; float keyWidthRatio = dictionary?.keyColumnWidth ?? 0.5f; //var settings = property.GetAttribute(); 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 Key { get; } TValue Value { get; } } }