@@ -0,0 +1,553 @@
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 后的参数是 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 < 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 ; }
}
}