This commit is contained in:
SoulliesOfficial
2025-10-23 00:49:44 -04:00
parent 9b1b5ca93f
commit 61a397dd4c
9846 changed files with 2618439 additions and 793547 deletions

View File

@@ -0,0 +1,70 @@
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
namespace SLSFramework.General
{
[System.Serializable]
public class InterfaceHolder<T> where T : class
{
[SerializeField] private MonoBehaviour value;
public T Value
{
get
{
if (value == null)
{
Debug.LogError("value is null");
return null;
}
T castValue = value as T;
if (castValue == null)
{
Debug.LogError($"value cannot be cast to {typeof(T)}. It is of type {value.GetType()}");
}
return castValue;
}
}
public InterfaceHolder(MonoBehaviour value)
{
this.value = value;
}
}
#if UNITY_EDITOR && !ODIN_INSPECTOR
[CustomPropertyDrawer(typeof(InterfaceHolder<>))]
public class InterfaceHolderDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
SerializedProperty valueProperty = property.FindPropertyRelative("value");
EditorGUI.BeginChangeCheck();
MonoBehaviour newValue = (MonoBehaviour)EditorGUI
.ObjectField(position, label, valueProperty.objectReferenceValue,
typeof(MonoBehaviour), true);
if (EditorGUI.EndChangeCheck())
{
if (newValue == null || newValue.GetComponent(fieldInfo.FieldType.GenericTypeArguments[0]) != null)
{
valueProperty.objectReferenceValue = newValue;
}
else
{
Debug.LogWarning($"Assigned object must implement interface {fieldInfo.FieldType.GenericTypeArguments[0].Name}");
}
}
EditorGUI.EndProperty();
}
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4351450027391954e85fea86db758c08
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,97 @@
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
namespace SLSFramework.General
{
[System.Serializable]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{
[SerializeField] private List<SerializedDictionaryKVPProps<TKey, TValue>> dictionaryList = new();
[SerializeField] private float dividerPosProp = 0.5f;
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
foreach (var kvp in this)
{
if (dictionaryList.FirstOrDefault(value => this.Comparer.Equals(value.Key, kvp.Key))
is SerializedDictionaryKVPProps<TKey, TValue> serializedKVP)
{
serializedKVP.Value = kvp.Value;
}
else
{
dictionaryList.Add(kvp);
}
}
dictionaryList.RemoveAll(value => ContainsKey(value.Key) == false);
for (int i = 0; i < dictionaryList.Count; i++)
{
dictionaryList[i].index = i;
}
}
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
Clear();
dictionaryList.RemoveAll(r => r.Key == null);
foreach (var serializedKVP in dictionaryList)
{
if (!(serializedKVP.isKeyDuplicated = ContainsKey(serializedKVP.Key)))
{
Add(serializedKVP.Key, serializedKVP.Value);
}
}
}
public new TValue this[TKey key]
{
get
{
#if UNITY_EDITOR
if (ContainsKey(key))
{
var duplicateKeysWithCount = dictionaryList.GroupBy(item => item.Key)
.Where(group => group.Count() > 1)
.Select(group => new { Key = group.Key, Count = group.Count() });
foreach (var duplicatedKey in duplicateKeysWithCount)
{
Debug.LogError($"Key '{duplicatedKey.Key}' is duplicated {duplicatedKey.Count} times in the dictionary.");
}
return base[key];
}
else
{
Debug.LogError($"Key '{key}' not found in dictionary.");
return default(TValue);
}
#else
return base[key];
#endif
}
}
[System.Serializable]
public class SerializedDictionaryKVPProps<TypeKey, TypeValue>
{
public TypeKey Key;
public TypeValue Value;
public int index;
public bool isKeyDuplicated;
public SerializedDictionaryKVPProps(TypeKey key, TypeValue value) { this.Key = key; this.Value = value; }
public static implicit operator SerializedDictionaryKVPProps<TypeKey, TypeValue>(KeyValuePair<TypeKey, TypeValue> kvp)
=> new SerializedDictionaryKVPProps<TypeKey, TypeValue>(kvp.Key, kvp.Value);
public static implicit operator KeyValuePair<TypeKey, TypeValue>(SerializedDictionaryKVPProps<TypeKey, TypeValue> kvp)
=> new KeyValuePair<TypeKey, TypeValue>(kvp.Key, kvp.Value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 08185d6eb814648ce9cdfca048e1611b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,433 @@
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
namespace SLSFramework.General
{
[CustomPropertyDrawer(typeof(SerializableDictionary<,>), true)]
public class SerializableDictionaryDrawer : PropertyDrawer
{
public override void OnGUI(Rect rect, SerializedProperty prop, GUIContent label)
{
var indentedRect = EditorGUI.IndentedRect(rect);
void Head()
{
var headerRect = indentedRect;
headerRect.height = EditorGUIUtility.singleLineHeight;
void ExpandablePanel()
{
var fullHeaderRect = new Rect(headerRect);
fullHeaderRect.x -= 17;
fullHeaderRect.width += 34;
if (Event.current != null && fullHeaderRect.Contains(Event.current.mousePosition))
{
Color transparentGrey = new Color(0.4f, 0.4f, 0.4f, 0.4f);
EditorGUI.DrawRect(fullHeaderRect, transparentGrey);
}
GUI.color = Color.clear;
if (GUI.Button(new Rect(fullHeaderRect.x, fullHeaderRect.y, fullHeaderRect.width - 40,
fullHeaderRect.height), ""))
{
prop.isExpanded = !prop.isExpanded;
}
GUI.color = Color.white;
var triangleRect = rect;
triangleRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.Foldout(triangleRect, prop.isExpanded, "");
}
void DisplayName()
{
GUI.color = Color.white;
#if UNITY_2022_1_OR_NEWER
var labelRect = headerRect;
GUI.Label(labelRect, prop.displayName);
#else
GUI.Label(headerRect, prop.displayName);
#endif
GUI.color = Color.white;
GUI.skin.label.fontSize = 12;
GUI.skin.label.fontStyle = FontStyle.Normal;
GUI.skin.label.alignment = TextAnchor.MiddleLeft;
}
void DuplicatedKeysWarning()
{
if (Event.current != null && Event.current.type != EventType.Repaint)
{
return;
}
var hasRepeated = false;
var repeatedKeys = new List<string>();
for (int i = 0; i < dictionaryList.arraySize; i++)
{
SerializedProperty isKeyRepeatedProperty = dictionaryList.GetArrayElementAtIndex(i)
.FindPropertyRelative("isKeyDuplicated");
if (isKeyRepeatedProperty.boolValue)
{
hasRepeated = true;
SerializedProperty keyProperty = dictionaryList.GetArrayElementAtIndex(i).FindPropertyRelative("Key");
string keyString = GetSerializedPropertyValueAsString(keyProperty);
repeatedKeys.Add(keyString);
}
}
if (!hasRepeated)
{
return;
}
float with = GUI.skin.label.CalcSize(new GUIContent(prop.displayName)).x;
headerRect.x += with + 35f;
var warningRect = headerRect;
Rect warningRectIcon = new Rect(headerRect.x - 18, headerRect.y, headerRect.width, headerRect.height);
GUI.color = Color.white;
GUI.Label(warningRectIcon, EditorGUIUtility.IconContent("console.erroricon"));
GUI.color = new Color(1.0f, 0.443f, 0.443f);
GUI.skin.label.fontStyle = FontStyle.Bold;
GUI.Label(warningRect, "Duplicated keys: " + string.Join(", ", repeatedKeys));
GUI.color = Color.white;
GUI.skin.label.fontStyle = FontStyle.Normal;
}
string GetSerializedPropertyValueAsString(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.intValue.ToString();
case SerializedPropertyType.Boolean:
return property.boolValue.ToString();
case SerializedPropertyType.Float:
return property.floatValue.ToString();
case SerializedPropertyType.String:
return property.stringValue;
default:
return "(Unsupported Type)";
}
}
ExpandablePanel();
DisplayName();
DuplicatedKeysWarning();
}
void List()
{
if (!prop.isExpanded)
{
return;
}
SetupList(prop);
float newHeight = indentedRect.height - EditorGUIUtility.singleLineHeight - 3;
indentedRect.y += indentedRect.height - newHeight;
indentedRect.height = newHeight;
reorderableList.DoList(indentedRect);
}
SetupProps(prop);
Head();
List();
}
public override float GetPropertyHeight(SerializedProperty prop, GUIContent label)
{
SetupProps(prop);
var height = EditorGUIUtility.singleLineHeight;
if (prop.isExpanded)
{
SetupList(prop);
height += reorderableList.GetHeight() + 5;
}
return height;
}
private float GetListElementHeight(int index)
{
if (index >= dictionaryList.arraySize) return 0;
var kvpProp = dictionaryList.GetArrayElementAtIndex(index);
var keyProp = kvpProp.FindPropertyRelative("Key");
var valueProp = kvpProp.FindPropertyRelative("Value");
float keyHeight = EditorGUI.GetPropertyHeight(keyProp, true);
float valueHeight;
if (IsSingleLine(valueProp))
{
// 如果Value是单行高度就是它自身的高度
valueHeight = EditorGUI.GetPropertyHeight(valueProp, true);
}
else
{
// 如果Value是复杂类型基础高度是标题行的高度
valueHeight = EditorGUIUtility.singleLineHeight;
// 如果它被展开了,需要加上所有子属性的高度
if (valueProp.isExpanded)
{
foreach (var child in GetChildren(valueProp))
{
valueHeight += EditorGUI.GetPropertyHeight(child, true) + EditorGUIUtility.standardVerticalSpacing;
}
}
}
// 返回Key和Value中较高者的高度并增加一点垂直间距
return Mathf.Max(keyHeight, valueHeight) + EditorGUIUtility.standardVerticalSpacing;
}
void DrawListElement(Rect rect, int index, bool isActive, bool isFocused)
{
if (index >= dictionaryList.arraySize) return;
var kvpProp = dictionaryList.GetArrayElementAtIndex(index);
var keyProp = kvpProp.FindPropertyRelative("Key");
var valueProp = kvpProp.FindPropertyRelative("Value");
// 为整个元素添加一点垂直内边距
rect.y += 2;
// --- 区域计算 ---
var dividerWidh = 6f;
var dividerPosition = (dividerPosProp != null) ? dividerPosProp.floatValue : 0.3f;
var fullRect = rect;
fullRect.width -= 1;
var keyRect = fullRect;
keyRect.width *= dividerPosition;
keyRect.width -= dividerWidh / 2;
var valueRect = fullRect;
valueRect.x += fullRect.width * dividerPosition;
valueRect.width *= (1 - dividerPosition);
valueRect.width -= dividerWidh / 2;
// --- 绘制Key (保持不变) ---
// 确保Key的高度是单行避免它被拉伸
keyRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(keyRect, keyProp, GUIContent.none, true);
// --- 核心修改自定义绘制Value列 ---
if (IsSingleLine(valueProp))
{
// 如果Value是单行则正常绘制
valueRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(valueRect, valueProp, GUIContent.none, true);
}
else
{
// 如果Value是复杂类型则自定义绘制
var headerRect = new Rect(valueRect.x + 12, valueRect.y, valueRect.width, EditorGUIUtility.singleLineHeight);
// --- 问题1修复分离绘制三角箭头和标题增加间距 ---
// 1. 先只绘制三角箭头
valueProp.isExpanded = EditorGUI.Foldout(headerRect, valueProp.isExpanded, GUIContent.none, true);
// 2. 在箭头右侧留出空间后,再绘制标题
var titleRect = new Rect(headerRect.x + 3, headerRect.y, headerRect.width - 15, headerRect.height);
EditorGUI.LabelField(titleRect, valueProp.type, EditorStyles.boldLabel);
// 如果展开了,则在下方绘制所有子属性
if (valueProp.isExpanded)
{
// --- 问题2修复正确的循环绘制逻辑 ---
var contentRect = new Rect(valueRect.x,
valueRect.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing, valueRect.width,
valueRect.height);
EditorGUI.indentLevel++;
foreach (var child in GetChildren(valueProp))
{
// 1. 获取子属性自身的高度
var childHeight = EditorGUI.GetPropertyHeight(child, true);
// 2. 为子属性创建一个精确的矩形区域
var childRect = new Rect(contentRect.x, contentRect.y, contentRect.width, childHeight);
// 3. 在这个精确的区域内绘制子属性
//EditorGUI.PropertyField(childRect, child, true);
// 4. 保留属性名称,拉长子属性的输入框
if (child.hasVisibleChildren == false)
{
EditorGUI.LabelField(new Rect(childRect.x, childRect.y, EditorGUIUtility.labelWidth, childHeight), child.displayName);
Rect valueFieldRect = new Rect(childRect.x + EditorGUIUtility.labelWidth / 2,
childRect.y, childRect.width - EditorGUIUtility.labelWidth / 2, childHeight);
EditorGUI.PropertyField(valueFieldRect, child, GUIContent.none);
}
else
{
EditorGUI.PropertyField(childRect, child, true);
}
// 5. 将下一个绘制的Y坐标向下移动为下一个属性留出空间
contentRect.y += childHeight + EditorGUIUtility.standardVerticalSpacing;
}
EditorGUI.indentLevel--;
}
}
// --- 绘制和处理分割线 (保持不变) ---
Divider(rect, fullRect, dividerPosition, dividerWidh);
}
void Divider(Rect originalRect, Rect fullRect, float dividerPosition, float dividerWidth)
{
Rect dividerRect = fullRect;
dividerRect.x += fullRect.width * dividerPosition - dividerWidth / 2;
dividerRect.width = dividerWidth;
EditorGUIUtility.AddCursorRect(dividerRect, MouseCursor.ResizeHorizontal);
if (Event.current != null && dividerRect.Contains(Event.current.mousePosition))
{
if (Event.current.type == EventType.MouseDown)
{
//isDividerDragged = true;
}
else if (Event.current.type == EventType.MouseUp
|| Event.current.type == EventType.MouseMove
|| Event.current.type == EventType.MouseLeaveWindow)
{
isDividerDragged = false;
}
}
if (isDividerDragged && Event.current != null && Event.current.type == EventType.MouseDrag)
{
dividerPosProp.floatValue = Mathf.Clamp(dividerPosProp.floatValue + Event.current.delta.x / originalRect.width, .2f, .8f);
}
}
private void ShowDictIsEmptyMessage(Rect rect)
{
GUI.Label(rect, "Empty");
}
private IEnumerable<SerializedProperty> GetChildren(SerializedProperty prop)
{
prop = prop.Copy();
var endProp = prop.GetEndProperty();
prop.NextVisible(true);
while (!SerializedProperty.EqualContents(prop, endProp))
{
yield return prop;
if (!prop.NextVisible(false))
break;
}
}
private IEnumerable<SerializedProperty> GetChildren(SerializedProperty prop, bool enterVisibleGrandchildren)
{
prop = prop.Copy();
var startPath = prop.propertyPath;
var enterVisibleChildren = true;
while (prop.NextVisible(enterVisibleChildren) && prop.propertyPath.StartsWith(startPath))
{
yield return prop;
enterVisibleChildren = enterVisibleGrandchildren;
}
}
private bool IsSingleLine(SerializedProperty prop)
{
return prop.propertyType != SerializedPropertyType.Generic || prop.hasVisibleChildren == false;
}
private void SetupList(SerializedProperty prop)
{
if (reorderableList != null)
{
return;
}
SetupProps(prop);
this.reorderableList = new ReorderableList(dictionaryList.serializedObject, dictionaryList, true, false, true, true);
this.reorderableList.drawElementCallback = DrawListElement;
this.reorderableList.elementHeightCallback = GetListElementHeight;
this.reorderableList.drawNoneElementCallback = ShowDictIsEmptyMessage;
}
private ReorderableList reorderableList;
private bool isDividerDragged;
public void SetupProps(SerializedProperty prop)
{
if (this.property != null)
{
return;
}
this.property = prop;
this.dictionaryList = prop.FindPropertyRelative("dictionaryList");
this.dividerPosProp = prop.FindPropertyRelative("dividerPosProp");
// 尝试获取字段上的 KeyWidthAttribute
// 如果找到了该属性,则设置 dividerPosProp 的值
if (fieldInfo.GetCustomAttributes(typeof(KeyWidthAttribute), true).FirstOrDefault() is KeyWidthAttribute keyWidthAttribute)
{
this.dividerPosProp.floatValue = keyWidthAttribute.WidthPercentage;
}
else
{
this.dividerPosProp.floatValue = 0.5f; // 默认值
}
}
private SerializedProperty property;
private SerializedProperty dictionaryList;
private SerializedProperty dividerPosProp;
}
}
#endif
namespace SLSFramework.General
{
/// <summary>
/// 用于指定 SerializableDictionary 抽屉中 Key 区域的宽度占比。
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field)]
public class KeyWidthAttribute : PropertyAttribute
{
public readonly float WidthPercentage;
/// <summary>
/// 设置 Key 区域的宽度占比。
/// </summary>
/// <param name="widthPercentage">一个0.1到0.9之间的浮点数代表Key区域占总宽度的百分比。</param>
public KeyWidthAttribute(float widthPercentage)
{
// 将值限制在一个合理的范围内避免UI错乱
WidthPercentage = Mathf.Clamp(widthPercentage, 0.1f, 0.9f);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af44f85b3a51e40cb8b1285fb308b2a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
using UnityEngine;
namespace SLSFramework.General
{
[System.Serializable]
public class UnityObjectWrapper<T> where T : class
{
[SerializeField] private Object value;
public T Value => value as T;
public UnityObjectWrapper(Object value)
{
this.value = value;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36996041f1dde6b46942025e4519df17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: