完善反射菜单

This commit is contained in:
2026-06-21 19:32:37 +08:00
parent 1d1a6b7acf
commit 61e67c5e15
31 changed files with 3209 additions and 98 deletions
@@ -0,0 +1,47 @@
using System;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 字段编组特性
/// 将多个字段标记为属于同一个 Entry,共享一个 UI 组件实例
///
/// 使用示例:
/// [XuiavrgFieldEntryGroup("field1", XuiavrgFieldTag.Label)] public string fieldLabel = "速度";
/// [XuiavrgFieldEntryGroup("field1", XuiavrgFieldTag.Context)] public float fieldValue = 0.1f;
/// [XuiavrgFieldEntryGroup("field1", XuiavrgFieldTag.Unit)] public string fieldUnit = "m/s";
///
/// 上述三个字段将合并为一个 Entry,分别绑定到 UI 组件上 Label、Context、Unit 三个子元素
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class XuiavrgFieldEntryGroupAttribute : Attribute, IXuiavrgAttribute
{
/// <summary>编组路径,以 "/" 分割层级。同一 groupPath 的字段合并到一个 Entry</summary>
public readonly string GroupPath;
/// <summary>该字段在编组 Entry 中的功能标记</summary>
public readonly XuiavrgFieldTag Tag;
/// <summary>同一 Tag 存在多个时的索引区分</summary>
public readonly int TagIndex;
/// <summary>
/// 创建字段编组标记
/// </summary>
/// <param name="groupPath">编组路径 (如 "field1" 或 "field1/child")</param>
/// <param name="tag">功能标记</param>
/// <param name="tagIndex">标记索引 (同一 Tag 存在多个时区分)</param>
public XuiavrgFieldEntryGroupAttribute(string groupPath, XuiavrgFieldTag tag, int tagIndex = 0)
{
GroupPath = groupPath;
Tag = tag;
TagIndex = tagIndex;
}
public void BindProperty(XuiavrgInspectorProperty property)
{
// 新的 Entry 体系通过 Manager.BuildEntries 直接处理此 Attribute
// 保留 IXuiavrgAttribute 兼容旧的 SchemaHydration 流程
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9de8a9119b60f0642a0fd446d791dc9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using System;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 字段排序特性
/// 控制 Entry 在界面中的显示顺序,替代原有 XuiavrgAttribute.InspectorOrder
///
/// 使用示例:
/// [XuiavrgFieldOrder(5)] public string someField;
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class XuiavrgFieldOrderAttribute : Attribute, IXuiavrgAttribute
{
/// <summary>排序值,值越小越靠前。0 表示使用默认发现顺序</summary>
public readonly int Order;
/// <summary>
/// 创建排序标记
/// </summary>
/// <param name="order">排序值</param>
public XuiavrgFieldOrderAttribute(int order)
{
Order = order;
}
public void BindProperty(XuiavrgInspectorProperty property)
{
property.inspectorOrder = Order;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d18234607f666c4ebffb17abcf3d429
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
using System;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 预制体覆盖特性
/// 覆盖对应 Entry 或普通字段生成 UI 时使用的预制体名称
///
/// 使用示例:
/// [XuiavrgFieldPrefabOverride("CustomSpeedPrefab")] public float speed;
///
/// Manager 在 GenerateUI 阶段会优先使用此名称查找 PropertyGroup
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class XuiavrgFieldPrefabOverrideAttribute : Attribute, IXuiavrgAttribute
{
/// <summary>覆盖的预制体组名称</summary>
public readonly string PrefabGroupName;
/// <summary>
/// 创建预制体覆盖标记
/// </summary>
/// <param name="prefabGroupName">
/// 目标预制体组名称,与 XuiavrgPropertyGroupScriptableObject.groupName 匹配
/// </param>
public XuiavrgFieldPrefabOverrideAttribute(string prefabGroupName)
{
PrefabGroupName = prefabGroupName;
}
public void BindProperty(XuiavrgInspectorProperty property)
{
// 新的 Entry 体系通过 Manager 直接处理此 Attribute
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46d359a456cc33249aa72c04a95987ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,627 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.Pool;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// UI 字段标记提供者接口
/// 所有 Tag 增强的 UI 组件需实现此接口,便于 Entry 在绑定阶段自动匹配
/// </summary>
public interface IXuiavrgFieldTagProvider
{
XuiavrgFieldTag EntryTag { get; }
int TagIndex { get; }
}
/// <summary>
/// 字段 Entry 接口
/// 数据对象字段与 UI 组件之间的中间缓存层
/// </summary>
public interface IFieldEntry
{
/// <summary>Entry 名称(字段名或编组路径)</summary>
string EntryName { get; }
/// <summary>Entry 完整路径(含父级路径)</summary>
string EntryPath { get; }
/// <summary>主要功能标记</summary>
XuiavrgFieldTag Tag { get; }
/// <summary>标记索引</summary>
int TagIndex { get; }
/// <summary>排序值(越小越靠前)</summary>
int FieldOrder { get; set; }
/// <summary>是否为编组 Entry(包含子 Entry)</summary>
bool IsGroupEntry { get; }
/// <summary>父 Entry</summary>
IFieldEntry Parent { get; }
/// <summary>子 Entry 列表(只读)</summary>
IReadOnlyList<IFieldEntry> Children { get; }
/// <summary>生成的 UI 实例引用</summary>
GameObject UIInstance { get; }
/// <summary>绑定的预制体组</summary>
XuiavrgPropertyGroupScriptableObject BoundPrefabGroup { get; set; }
/// <summary>覆盖的预制体组名称</summary>
string PrefabOverrideName { get; set; }
// === 生命周期 ===
/// <summary>Entry 创建时调用</summary>
void OnEntryCreate();
/// <summary>绑定 UI 和数据时调用</summary>
void OnEntryBind();
/// <summary>释放时调用(归还池前)</summary>
void OnEntryRelease();
/// <summary>销毁时调用</summary>
void OnEntryDestroy();
// === 数据访问 ===
/// <summary>获取指定成员的数据值</summary>
object GetDataValue(string memberName);
/// <summary>设置指定成员的数据值</summary>
void SetDataValue(string memberName, object value);
/// <summary>获取 Entry 下所有绑定成员的名称</summary>
IEnumerable<string> GetMemberNames();
// === UI 绑定 ===
/// <summary>
/// 绑定 UI 组件
/// </summary>
/// <param name="uiInstance">UI 预制体实例</param>
/// <param name="componentCache">
/// 组件缓存列表。传入 null 时会自动收集 uiInstance 上所有 Component。
/// 父 Entry 可将已收集的缓存传递给子 Entry 以提升性能
/// </param>
void BindUIComponent(GameObject uiInstance, List<Component> componentCache);
}
/// <summary>
/// 字段临时 Entry 实现类
/// 作为数据对象字段和 UI 组件之间的中间缓存层
///
/// 支持:
/// - 多字段合并为一个 Entry(通过 XuiavrgFieldEntryGroupAttribute)
/// - 嵌套 Entry(父 Entry 包含子 Entry)
/// - 对象池复用(通过静态 ObjectPool)
/// - MVVM 双向绑定的生命周期
/// </summary>
public class XuiFieldTempEntry : IFieldEntry
{
#region 对象池
internal static ObjectPool<XuiFieldTempEntry> Pool { get; private set; }
static XuiFieldTempEntry()
{
Pool = new ObjectPool<XuiFieldTempEntry>(
createFunc: () => new XuiFieldTempEntry(),
actionOnGet: entry => entry.OnEntryCreate(),
actionOnRelease: entry => entry.OnEntryRelease(),
actionOnDestroy: entry => entry.OnEntryDestroy(),
collectionCheck: false,
defaultCapacity: 32,
maxSize: 512
);
}
/// <summary>从池中获取 Entry 实例</summary>
public static XuiFieldTempEntry Get() => Pool.Get();
/// <summary>将 Entry 归还池中</summary>
public static void Release(XuiFieldTempEntry entry) => Pool.Release(entry);
#endregion
#region 内部绑定数据结构
/// <summary>
/// 成员反射绑定信息
/// </summary>
internal class MemberBinding
{
/// <summary>绑定的数据目标对象</summary>
public object valueTarget;
/// <summary>是否为属性(false 为字段)</summary>
public bool isProperty;
/// <summary>属性信息</summary>
public PropertyInfo propertyInfo;
/// <summary>字段信息</summary>
public FieldInfo fieldInfo;
/// <summary>功能标记</summary>
public XuiavrgFieldTag tag;
/// <summary>标记索引</summary>
public int tagIndex;
/// <summary>
/// 从绑定目标获取值
/// </summary>
public object GetValue()
{
if (valueTarget == null) return null;
return isProperty
? propertyInfo.GetValue(valueTarget)
: fieldInfo.GetValue(valueTarget);
}
/// <summary>
/// 设置值到绑定目标
/// </summary>
public void SetValue(object value)
{
if (valueTarget == null) return;
if (isProperty)
{
if (propertyInfo.CanWrite)
propertyInfo.SetValue(valueTarget, value);
}
else
{
fieldInfo.SetValue(valueTarget, value);
}
}
}
#endregion
#region 字段
private string _entryName;
private string _entryPath;
private IFieldEntry _parent;
private List<XuiFieldTempEntry> _children = new List<XuiFieldTempEntry>();
private Dictionary<string, MemberBinding> _bindings = new Dictionary<string, MemberBinding>();
private GameObject _uiInstance;
private XuiavrgPropertyGroupScriptableObject _boundPrefabGroup;
private string _prefabOverrideName;
private int _fieldOrder;
#endregion
#region IFieldEntry 属性实现
public string EntryName => _entryName;
public string EntryPath
{
get
{
if (!string.IsNullOrEmpty(_entryPath)) return _entryPath;
if (_parent != null)
{
string parentPath = _parent.EntryPath;
return string.IsNullOrEmpty(parentPath)
? _entryName
: parentPath + "/" + _entryName;
}
return _entryName;
}
}
public XuiavrgFieldTag Tag { get; private set; }
public int TagIndex { get; private set; }
public int FieldOrder
{
get => _fieldOrder;
set => _fieldOrder = value;
}
public bool IsGroupEntry => _children.Count > 0;
public IFieldEntry Parent => _parent;
public IReadOnlyList<IFieldEntry> Children => _children;
public GameObject UIInstance => _uiInstance;
public XuiavrgPropertyGroupScriptableObject BoundPrefabGroup
{
get => _boundPrefabGroup;
set => _boundPrefabGroup = value;
}
public string PrefabOverrideName
{
get => _prefabOverrideName;
set => _prefabOverrideName = value;
}
#endregion
#region 初始化方法
/// <summary>
/// 设置 Entry 基本属性
/// </summary>
public void Initialize(string entryName, XuiavrgFieldTag tag = XuiavrgFieldTag.None, int tagIndex = 0)
{
_entryName = entryName;
_entryPath = null;
Tag = tag;
TagIndex = tagIndex;
_fieldOrder = 0;
}
/// <summary>
/// 设置父 Entry(用于嵌套)
/// </summary>
public void SetParent(IFieldEntry parent)
{
_parent = parent;
_entryPath = null; // 重置路径,下次访问重新计算
}
/// <summary>
/// 添加子 Entry
/// </summary>
public void AddChild(XuiFieldTempEntry child)
{
if (child == null) return;
child.SetParent(this);
_children.Add(child);
}
/// <summary>
/// 注册成员绑定
/// </summary>
/// <param name="memberName">成员名称</param>
/// <param name="memberInfo">反射信息</param>
/// <param name="target">数据目标对象</param>
/// <param name="tag">功能标记</param>
/// <param name="tagIndex">标记索引</param>
public void RegisterBinding(string memberName, MemberInfo memberInfo, object target,
XuiavrgFieldTag tag = XuiavrgFieldTag.None, int tagIndex = 0)
{
MemberBinding binding = new MemberBinding
{
valueTarget = target,
tag = tag,
tagIndex = tagIndex
};
switch (memberInfo)
{
case PropertyInfo propInfo:
binding.isProperty = true;
binding.propertyInfo = propInfo;
break;
case FieldInfo fieldInfo:
binding.isProperty = false;
binding.fieldInfo = fieldInfo;
break;
}
_bindings[memberName] = binding;
// 如果是第一个注册的绑定,设置主 Tag
if (Tag == XuiavrgFieldTag.None && tag != XuiavrgFieldTag.None)
{
Tag = tag;
TagIndex = tagIndex;
}
}
#endregion
#region 数据访问
public object GetDataValue(string memberName)
{
if (_bindings.TryGetValue(memberName, out var binding))
return binding.GetValue();
// 查找子 Entry
foreach (var child in _children)
{
var value = child.GetDataValue(memberName);
if (value != null) return value;
}
return null;
}
public void SetDataValue(string memberName, object value)
{
if (_bindings.TryGetValue(memberName, out var binding))
{
binding.SetValue(value);
return;
}
// 查找子 Entry
foreach (var child in _children)
{
if (child.HasBinding(memberName))
{
child.SetDataValue(memberName, value);
return;
}
}
}
public IEnumerable<string> GetMemberNames() => _bindings.Keys;
/// <summary>
/// 检查是否包含指定成员绑定
/// </summary>
public bool HasBinding(string memberName) => _bindings.ContainsKey(memberName);
/// <summary>
/// 获取指定成员的标记
/// </summary>
public XuiavrgFieldTag GetMemberTag(string memberName)
{
if (_bindings.TryGetValue(memberName, out var binding))
return binding.tag;
return XuiavrgFieldTag.None;
}
#endregion
#region UI 绑定
public void BindUIComponent(GameObject uiInstance, List<Component> componentCache)
{
_uiInstance = uiInstance;
if (_uiInstance == null) return;
if (componentCache == null)
{
componentCache = new List<Component>();
_uiInstance.GetComponentsInChildren(true, componentCache);
}
BindUIComponentWithCache(_uiInstance, componentCache);
// 子 Entry 复用组件缓存
foreach (var child in _children)
{
child.BindUIComponent(_uiInstance, componentCache);
}
}
private void BindUIComponentWithCache(GameObject uiInstance, List<Component> componentCache)
{
// 遍历组件缓存,匹配 tag
var locators = new List<XuiavrgLocator>();
foreach (var comp in componentCache)
{
if (comp is XuiavrgLocator locator)
{
locators.Add(locator);
}
if (comp is IXuiavrgFieldTagProvider tagProvider)
{
// 匹配 Tag 和 TagIndex
if (tagProvider.EntryTag == this.Tag &&
(this.TagIndex == 0 || tagProvider.TagIndex == this.TagIndex))
{
// 找到匹配的 UI 组件,建立绑定
BuildComponentBinding(comp);
}
}
}
// 如果没有 IXuiavrgFieldTagProvider,尝试通过 Locator 匹配
if (locators.Count > 0)
{
BuildLocatorBindings(locators);
}
}
/// <summary>
/// 通过标签提供者建立组件绑定
/// </summary>
private void BuildComponentBinding(Component comp)
{
// 根据组件类型和标记类型进行数据同步
switch (comp)
{
case UnityEngine.UI.Text uiText when Tag == XuiavrgFieldTag.Label || Tag == XuiavrgFieldTag.Output:
uiText.text = GetFirstBindingValue()?.ToString() ?? "";
break;
case UnityEngine.UI.Button _ when Tag == XuiavrgFieldTag.Button:
// 按钮绑定在 BindUIEvents 中处理
break;
case UnityEngine.UI.Toggle uiToggle when Tag == XuiavrgFieldTag.Toggle:
if (GetFirstBindingValue() is bool boolVal)
uiToggle.isOn = boolVal;
break;
case UnityEngine.UI.Slider uiSlider when Tag == XuiavrgFieldTag.Slider:
if (GetFirstBindingValue() is float floatVal)
uiSlider.value = floatVal;
break;
case UnityEngine.UI.InputField uiInput when Tag == XuiavrgFieldTag.Input:
uiInput.text = GetFirstBindingValue()?.ToString() ?? "";
break;
case UnityEngine.UI.Dropdown uiDropdown when Tag == XuiavrgFieldTag.Dropdown:
if (GetFirstBindingValue() is int intVal)
uiDropdown.value = intVal;
break;
}
}
/// <summary>
/// 通过 Locator 建立绑定
/// </summary>
private void BuildLocatorBindings(List<XuiavrgLocator> locators)
{
foreach (var locator in locators)
{
foreach (var locProp in locator.Properties)
{
// 按 propertyName 匹配成员名
if (_bindings.TryGetValue(locProp.propertyName, out var binding))
{
// 从数据端同步到 UI
var dataValue = binding.GetValue();
locProp.SetValue(dataValue);
}
}
}
}
/// <summary>
/// 获取第一个绑定的值
/// </summary>
private object GetFirstBindingValue()
{
foreach (var kvp in _bindings)
{
var val = kvp.Value.GetValue();
if (val != null) return val;
}
return null;
}
#endregion
#region 事件绑定
/// <summary>
/// 绑定 UI 交互事件(双向绑定)
/// </summary>
public void BindUIEvents()
{
if (_uiInstance == null) return;
var allComponents = new List<Component>();
_uiInstance.GetComponentsInChildren(true, allComponents);
foreach (var comp in allComponents)
{
if (comp is IXuiavrgFieldTagProvider tagProvider)
{
if (tagProvider.EntryTag != this.Tag) continue;
BindComponentEvents(comp);
}
}
// 子 Entry 事件绑定
foreach (var child in _children)
{
child.BindUIEvents();
}
}
private void BindComponentEvents(Component comp)
{
switch (comp)
{
case UnityEngine.UI.Button uiButton:
uiButton.onClick.AddListener(() =>
{
SetFirstBindingValue(true);
});
break;
case UnityEngine.UI.Toggle uiToggle:
uiToggle.onValueChanged.AddListener(val =>
{
SetFirstBindingValue(val);
});
break;
case UnityEngine.UI.Slider uiSlider:
uiSlider.onValueChanged.AddListener(val =>
{
SetFirstBindingValue(val);
});
break;
case UnityEngine.UI.InputField uiInput:
uiInput.onEndEdit.AddListener(val =>
{
SetFirstBindingValue(val);
});
break;
case UnityEngine.UI.Dropdown uiDropdown:
uiDropdown.onValueChanged.AddListener(val =>
{
SetFirstBindingValue(val);
});
break;
}
}
private void SetFirstBindingValue(object value)
{
foreach (var kvp in _bindings)
{
kvp.Value.SetValue(value);
break;
}
}
#endregion
#region 生命周期
public void OnEntryCreate()
{
_entryName = null;
_entryPath = null;
_parent = null;
_fieldOrder = 0;
_prefabOverrideName = null;
}
public void OnEntryBind()
{
BindUIEvents();
}
public void OnEntryRelease()
{
_entryName = null;
_entryPath = null;
_parent = null;
_uiInstance = null;
_boundPrefabGroup = null;
_prefabOverrideName = null;
Tag = XuiavrgFieldTag.None;
TagIndex = 0;
_fieldOrder = 0;
// 清理绑定引用
_bindings.Clear();
// 递归释放子 Entry
foreach (var child in _children)
{
Release(child);
}
_children.Clear();
}
public void OnEntryDestroy()
{
_bindings.Clear();
_children.Clear();
_uiInstance = null;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f5cdd134ffafe74faa7c37b7b2f9919
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 测试用枚举 - 用于下拉选择测试
/// </summary>
public enum XuiavrgTestQuality
{
Low,
Medium,
High,
Epic,
Legendary
}
/// <summary>
/// 反射界面生成器测试数据类
/// 涵盖新版 Entry 体系的所有特性:编组、排序、预制体覆盖、多种字段类型
///
/// 使用方式:
/// 1. 在 Hierarchy 右键 -> Xeric UI -> Create Reflection UI Example 创建示例节点
/// 2. 创建 XuiavrgPropertyGroupScriptableObject 并设置 groupName 和 supportedTags
/// 3. 将编组对应的预制体拖入
/// 4. 调用 manager.BuildEntries(new XuiavrgExampleTestData()) 构建 Entry
/// 5. 调用 manager.GenerateUI() 生成界面
/// 或直接使用兼容方法: manager.SchemaHydration(new XuiavrgExampleTestData())
/// </summary>
[Serializable]
public class XuiavrgExampleTestData
{
// ==================== 基础数值类型 ====================
[XuiavrgFieldOrder(1)]
[XuiavrgNumberCheck(0, 100)]
public int intValue = 42;
[XuiavrgFieldOrder(2)]
[XuiavrgNumberCheck(0f, 999f)]
public float floatValue = 3.14f;
[XuiavrgFieldOrder(3)]
public double doubleValue = 123.456;
[XuiavrgFieldOrder(4)]
[XuiavrgNumberCheck(0, 999999)]
public long longValue = 100000;
// ==================== 多字段编组示例 ====================
// 以下三个字段合并为一个 Entry,共享一个 UI 组件实例
// UI 组件上需要有 Label、Context、Unit 对应的 IXuiavrgFieldTagProvider
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Label)]
[XuiavrgFieldOrder(5)]
public string speedLabel = "速度";
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Context)]
[XuiavrgNumberCheck(-999f, 999f)]
public float speedValue = 0.1f;
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Unit)]
public string speedUnit = "m/s";
// 第二个编组示例 - 健康值
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Label)]
[XuiavrgFieldOrder(6)]
public string healthLabel = "生命值";
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Context)]
[XuiavrgNumberCheck(0, 9999)]
public float healthValue = 100f;
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Unit)]
public string healthUnit = "HP";
// ==================== 字符串类型 ====================
[XuiavrgFieldOrder(7)]
[XuiavrgRequired]
[XuiavrgStringCheck(minLength: 2, maxLength: 16)]
public string playerName = "DefaultName";
[XuiavrgFieldOrder(8)]
[XuiavrgNotEmpty]
[XuiavrgStringCheck(onlyEmail: false)]
public string emailAddress = "example@mail.com";
[XuiavrgFieldOrder(9)]
[XuiavrgFieldPrefabOverride("MultilineTextArea")]
[XuiavrgStringCheck(maxLength: 500)]
public string biography = "这是一段个人简介...";
// ==================== 布尔类型 ====================
[XuiavrgFieldOrder(10)]
public bool enableFeature = true;
[XuiavrgFieldOrder(11)]
public bool advancedMode = false;
// ==================== 枚举类型 ====================
[XuiavrgFieldOrder(12)]
public XuiavrgTestQuality quality = XuiavrgTestQuality.High;
// ==================== 隐藏属性 ====================
[XuiavrgHide(true)]
public string hiddenField = "此字段被隐藏,不会生成 Entry";
// ==================== 不同尺寸 ====================
[XuiavrgFieldOrder(13)]
[XuiavrgAttribute("小号", "size_small", null, size: XuiavrgSize.Small)]
public string smallSizeField = "小尺寸";
[XuiavrgFieldOrder(14)]
[XuiavrgAttribute("大号", "size_large", null, size: XuiavrgSize.Large)]
public string largeSizeField = "大尺寸";
// ==================== 集合类型(兼容旧标记) ====================
[XuiavrgFieldOrder(15)]
[XuiavrgCollectionCheck(1, 10, true)]
public List<string> tags = new List<string> { "Unity", "C#", "UI" };
// ==================== 按钮触发器 ====================
[XuiavrgFieldOrder(16)]
public bool submitTrigger = false;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 211cf8030617ad143ba1f4f68f544658
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,51 @@
using System;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 字段 UI 功能标记枚举
/// 用于在数据对象字段和 UI 组件之间建立功能映射
/// 每个标记代表 UI 组件上的一种功能元素
/// </summary>
[Flags]
public enum XuiavrgFieldTag
{
None = 0,
/// <summary>标题文本 / Label</summary>
Label = 1 << 0,
/// <summary>内容值 / 数值上下文</summary>
Context = 1 << 1,
/// <summary>单位文本</summary>
Unit = 1 << 2,
/// <summary>布尔开关 / Toggle</summary>
Toggle = 1 << 3,
/// <summary>按钮触发器</summary>
Button = 1 << 4,
/// <summary>滑块 / 进度条</summary>
Slider = 1 << 5,
/// <summary>下拉选择</summary>
Dropdown = 1 << 6,
/// <summary>输入值 / InputField</summary>
Input = 1 << 7,
/// <summary>单选项 / Radio</summary>
Select = 1 << 8,
/// <summary>符号 / 图标</summary>
Sign = 1 << 9,
/// <summary>编辑区域 / 多行文本</summary>
Edit = 1 << 10,
/// <summary>输出区域 / 只读展示</summary>
Output = 1 << 11,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e3eac1306ca1a5145b8a2cae218062f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,13 +1,10 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Deconstruction.Type.Peculiarity;
#if ODIN_INSPECTOR
using Sirenix.OdinInspector;
#endif
using UnityEngine;
using UnityEngine.Pool;
using Object = UnityEngine.Object;
namespace XericUI.ReflectionUIGenerator
{
@@ -15,105 +12,403 @@ namespace XericUI.ReflectionUIGenerator
/// 节点下生成反射属性管理器
/// </summary>
/// <remarks>
/// 管理器需要挂载在unity节点中,管理器中指定的属性承载节点将用于生成属性内容。
/// 管理器需要挂载在 Unity 节点中,管理器中指定的属性承载节点将用于生成属性内容。
///
/// 为了准确生成界面属性,首先需要准备界面属性预制体。
/// 在包含界面交互元素的预制体跟目录上挂载 <see cref="XuiavrgLocator"/> 脚本,其将作为组件入口,管理所有的交互组件,必须在脚本中添加所有允许交互的元素。
/// (注:如果你没有odin插件,那么组件样式可能会与设计的不一致,请务必确保每个数组中只包含一个组件)
///
/// 然后在工程中创建 <see cref="XuiavrPropertyScriptableObject"/> 对象,指定对应的数据类型,将刚刚创建的界面预制体拖入,这样就完成了两者的绑定。
/// 为了方便管理,再创建一个 <see cref="XuiavrgPropertyGroupScriptableObject"/> 对象,它可以成为一个组,快速管理和切换所需的数据集合。
///
/// 接下来,在管理器中填入使用的 <see cref="XuiavrgPropertyGroupScriptableObject"/> 对象,然后调用 SchemaHydration 方法传入负载,界面顷刻生成。
/// 新的生命周期流程:
/// 1. BuildEntries(object payload) — 传入数据对象,构建 Entry 列表
/// 2. OnPostBuildEntries(entries) — 虚方法,可重写添加额外 Entry
/// 3. 自动按 FieldOrder 排序
/// 4. GenerateUI() — 根据 Entry 列表生成 UI 实例并绑定
///
/// 对象池说明:
/// - Entry 通过 XuiFieldTempEntry.Pool 静态池管理
/// - UI 预制体通过内部 PropertyGroupPools 字典管理,以 PropertyGroupScriptableObject 为键
/// </remarks>
public class XuiavrgInspectorManager : MonoBehaviour
{
public static Func<XuiavrgInspectorProperty> CreatInspectorProperty = () => new XuiavrgInspectorProperty();
public static Action<XuiavrgInspectorProperty> GetInspectorProperty = a => {};
public static Action<XuiavrgInspectorProperty> ReleaseInspectorProperty = a => a.Clear();
public static Action<XuiavrgInspectorProperty> DestoryInspectorProperty = a => {};
public static ObjectPool<XuiavrgInspectorProperty> InspectorPropertyPool
= new ObjectPool<XuiavrgInspectorProperty>(CreatInspectorProperty, GetInspectorProperty, ReleaseInspectorProperty, DestoryInspectorProperty);
#region 对象池
#if ODIN_INSPECTOR
[LabelText("预制体组")]
#endif
public XuiavrgPropertyGroupScriptableObject propertyUIPrefabsGroup;
#if ODIN_INSPECTOR
[LabelText("反射属性预览")]
#endif
public List<XuiavrgInspectorProperty> allProperty = new List<XuiavrgInspectorProperty>();
/// <summary>
/// 装填数据负载
/// UI 预制体对象池,以 PropertyGroupScriptableObject 为键
/// </summary>
private Dictionary<XuiavrgPropertyGroupScriptableObject, ObjectPool<GameObject>> _propertyGroupPools
= new Dictionary<XuiavrgPropertyGroupScriptableObject, ObjectPool<GameObject>>();
/// <summary>
/// 从指定 PropertyGroup 的预制体池获取 UI 实例
/// </summary>
public GameObject PropertyGroupPoolGet(XuiavrgPropertyGroupScriptableObject group)
{
if (group == null) return null;
if (!_propertyGroupPools.TryGetValue(group, out var pool))
{
var firstPrefab = group.FirstOrDefault()?.prefab;
pool = new ObjectPool<GameObject>(
createFunc: () =>
firstPrefab != null
? Object.Instantiate(firstPrefab, transform)
: new GameObject("PropertyItem", typeof(RectTransform)),
actionOnGet: go => go.SetActive(true),
actionOnRelease: go => go.SetActive(false),
actionOnDestroy: go =>
{
if (go) Destroy(go);
},
collectionCheck: false,
defaultCapacity: 4,
maxSize: 64
);
_propertyGroupPools[group] = pool;
}
return pool.Get();
}
/// <summary>
/// 将 UI 实例归还到指定 PropertyGroup 的预制体池
/// </summary>
public void PropertyGroupPoolRelease(XuiavrgPropertyGroupScriptableObject group, GameObject instance)
{
if (group == null || instance == null) return;
if (_propertyGroupPools.TryGetValue(group, out var pool))
{
pool.Release(instance);
}
else
{
if (instance) Destroy(instance);
}
}
/// <summary>
/// 清空所有预制体对象池
/// </summary>
public void ClearAllPropertyGroupPools()
{
foreach (var pool in _propertyGroupPools.Values)
{
pool.Clear();
}
_propertyGroupPools.Clear();
}
#endregion
#region 字段
/// <summary>
/// 当前活跃的 Entry 列表
/// </summary>
public List<IFieldEntry> CurrentEntries { get; private set; } = new List<IFieldEntry>();
/// <summary>
/// 当前传入的数据负载
/// </summary>
public object CurrentPayload { get; private set; }
#endregion
#region 核心生命周期
/// <summary>
/// 传入对象实例,构建 Entry 列表
/// </summary>
/// <param name="payload">数据端对象</param>
#if ODIN_INSPECTOR
[Button]
#endif
public void BuildEntries(object payload)
{
// 清空之前的 Entry
ClearCurrentEntries();
CurrentPayload = payload;
if (payload == null)
{
Debug.LogError("[Xuiavrg] 未指定有效负载,无法构建 Entry");
return;
}
var type = payload.GetType();
var memberInfos = type.GetRuntimeMemberInfo().ToList();
// 用于合并编组 Entry 的临时字典
var groupEntries = new Dictionary<string, XuiFieldTempEntry>();
foreach (var memberInfo in memberInfos)
{
// 检查隐藏标记
var hideAttr = memberInfo.GetCustomAttribute<XuiavrgHideAttribute>();
if (hideAttr != null && hideAttr.Hide)
continue;
// 检查编组标记
var groupAttr = memberInfo.GetCustomAttribute<XuiavrgFieldEntryGroupAttribute>();
if (groupAttr != null)
{
// 编组字段:合并到同名 Entry
if (!groupEntries.TryGetValue(groupAttr.GroupPath, out var groupEntry))
{
groupEntry = XuiFieldTempEntry.Get();
groupEntry.Initialize(groupAttr.GroupPath);
groupEntries[groupAttr.GroupPath] = groupEntry;
CurrentEntries.Add(groupEntry);
}
groupEntry.RegisterBinding(
memberInfo.Name, memberInfo, payload,
groupAttr.Tag, groupAttr.TagIndex);
}
else
{
// 普通字段:创建独立 Entry
var entry = XuiFieldTempEntry.Get();
entry.Initialize(memberInfo.Name);
entry.RegisterBinding(memberInfo.Name, memberInfo, payload);
// 检查预制体覆盖
var overrideAttr = memberInfo.GetCustomAttribute<XuiavrgFieldPrefabOverrideAttribute>();
if (overrideAttr != null)
{
entry.PrefabOverrideName = overrideAttr.PrefabGroupName;
}
CurrentEntries.Add(entry);
}
// 应用排序标记
var orderAttr = memberInfo.GetCustomAttribute<XuiavrgFieldOrderAttribute>();
if (orderAttr != null)
{
// 找到该成员对应的 Entry(可能是编组 Entry)
if (groupAttr != null && groupEntries.TryGetValue(groupAttr.GroupPath, out var gEntry))
{
if (gEntry.FieldOrder == 0 || orderAttr.Order < gEntry.FieldOrder)
gEntry.FieldOrder = orderAttr.Order;
}
else
{
// 普通字段的排序应用到其 Entry
var normalEntry = CurrentEntries.LastOrDefault();
if (normalEntry != null)
normalEntry.FieldOrder = orderAttr.Order;
}
}
// 应用旧的 IXuiavrgAttribute(兼容)
ApplyLegacyAttributes(memberInfo, payload, groupAttr, groupEntries);
}
// 调用后期回调
OnPostBuildEntries(CurrentEntries);
// 排序
SortEntries();
}
/// <summary>
/// 应用旧的 IXuiavrgAttribute 体系中的属性(兼容保留)
/// </summary>
private void ApplyLegacyAttributes(
MemberInfo memberInfo, object payload,
XuiavrgFieldEntryGroupAttribute groupAttr,
Dictionary<string, XuiFieldTempEntry> groupEntries)
{
// 处理旧的 XuiavrgAttribute(设置 labelName, inspectorOrder 等)
var oldAttr = memberInfo.GetCustomAttribute<XuiavrgAttribute>();
if (oldAttr != null)
{
// 创建临时的 InspectorProperty 来接收绑定
var tempProp = new XuiavrgInspectorProperty();
tempProp.RegsiterDataValue(memberInfo, payload);
oldAttr.BindProperty(tempProp);
// 将相关属性同步到 Entry
IFieldEntry targetEntry;
if (groupAttr != null && groupEntries.TryGetValue(groupAttr.GroupPath, out var gEntry))
targetEntry = gEntry;
else
targetEntry = CurrentEntries.LastOrDefault(e => !e.IsGroupEntry || e == null) ?? CurrentEntries.LastOrDefault();
if (targetEntry != null)
{
if (tempProp.inspectorOrder != 0 && targetEntry.FieldOrder == 0)
targetEntry.FieldOrder = tempProp.inspectorOrder;
targetEntry.PrefabOverrideName ??= tempProp.uiPropertyName;
}
}
// 处理其他旧的验证 Attribute
foreach (var attr in memberInfo.GetCustomAttributes().OfType<IXuiavrgAttribute>())
{
if (attr is XuiavrgAttribute) continue; // 已处理
if (attr is XuiavrgHideAttribute) continue; // 已处理
if (attr is XuiavrgFieldEntryGroupAttribute) continue; // 已处理
if (attr is XuiavrgFieldOrderAttribute) continue; // 已处理
if (attr is XuiavrgFieldPrefabOverrideAttribute) continue; // 已处理
// 其他旧 Attribute(Required, StringCheck, NumberCheck 等)
var tempProp = new XuiavrgInspectorProperty();
tempProp.RegsiterDataValue(memberInfo, payload);
attr.BindProperty(tempProp);
// 这些验证信息目前由 InspectorProperty 保留,后续可迁移到 Entry
}
}
/// <summary>
/// 生成后期回调(可重写)
/// 在此方法中可以手动添加额外的 Entry(如 Preview 结构、不在数据对象中的内容)
/// </summary>
/// <param name="entries">当前 Entry 列表</param>
protected virtual void OnPostBuildEntries(List<IFieldEntry> entries)
{
// 默认无操作,子类可重写
}
/// <summary>
/// 对 Entry 列表按 FieldOrder 排序
/// </summary>
private void SortEntries()
{
CurrentEntries.Sort((a, b) =>
{
int orderCompare = a.FieldOrder.CompareTo(b.FieldOrder);
if (orderCompare != 0) return orderCompare;
return string.Compare(a.EntryName, b.EntryName, StringComparison.Ordinal);
});
}
/// <summary>
/// 根据 Entry 列表生成 UI
/// </summary>
public void GenerateUI()
{
if (CurrentEntries == null || CurrentEntries.Count == 0)
{
Debug.LogWarning("[Xuiavrg] 没有可生成的 Entry,请先调用 BuildEntries");
return;
}
foreach (var entry in CurrentEntries)
{
if (entry is not XuiFieldTempEntry tempEntry) continue;
// 查找匹配的 PropertyGroup
var group = ResolvePropertyGroup(entry);
if (group == null)
{
Debug.LogWarning($"[Xuiavrg] 未找到 Entry '{entry.EntryName}' 的匹配预制体组,跳过");
continue;
}
entry.BoundPrefabGroup = group;
// 从对象池获取 UI 实例
var uiInstance = PropertyGroupPoolGet(group);
if (uiInstance == null) continue;
// 设置父节点和命名
uiInstance.name = entry.EntryName;
uiInstance.transform.SetParent(transform, false);
// 绑定 UI 组件(首次绑定不传缓存,由 Entry 内部收集)
entry.BindUIComponent(uiInstance, null);
// 绑定事件
entry.OnEntryBind();
}
}
/// <summary>
/// 解析 Entry 对应的 PropertyGroup
/// 优先使用 PrefabOverrideName,否则按 Tag 匹配
/// </summary>
private XuiavrgPropertyGroupScriptableObject ResolvePropertyGroup(IFieldEntry entry)
{
// 1. 优先检查 PrefabOverrideName
if (!string.IsNullOrEmpty(entry.PrefabOverrideName))
{
var overrideGroup = XuiavrgPropertyGroupScriptableObject.FindGroupByName(entry.PrefabOverrideName);
if (overrideGroup != null) return overrideGroup;
}
// 2. 按 Tag 查找匹配的组
if (entry.Tag != XuiavrgFieldTag.None)
{
var tags = new List<XuiavrgFieldTag> { entry.Tag };
// 收集子 Entry 的 Tag(如果是编组 Entry)
if (entry.IsGroupEntry)
{
foreach (var child in entry.Children)
{
if (child.Tag != XuiavrgFieldTag.None && !tags.Contains(child.Tag))
tags.Add(child.Tag);
}
}
var matchedGroup = XuiavrgPropertyGroupScriptableObject.FindGroupByTags(tags.ToArray());
if (matchedGroup != null) return matchedGroup;
}
// 3. 回退到通用匹配
return XuiavrgPropertyGroupScriptableObject.FindGroupByName("Default");
}
/// <summary>
/// 清空当前界面和 Entry 列表
/// </summary>
public void ClearAll()
{
ClearCurrentEntries();
ClearAllPropertyGroupPools();
CurrentPayload = null;
}
/// <summary>
/// 清空当前 Entry 列表(归还到池中)
/// </summary>
private void ClearCurrentEntries()
{
foreach (var entry in CurrentEntries)
{
// 释放 UI 实例
if (entry.UIInstance != null && entry.BoundPrefabGroup != null)
{
PropertyGroupPoolRelease(entry.BoundPrefabGroup, entry.UIInstance);
}
// 归还 Entry 到池
if (entry is XuiFieldTempEntry tempEntry)
{
XuiFieldTempEntry.Release(tempEntry);
}
}
CurrentEntries.Clear();
}
#endregion
#region 生命周期
private void OnDestroy()
{
ClearAll();
}
#endregion
#region 兼容旧 API
/// <summary>
/// [已弃用] 请使用 BuildEntries + GenerateUI 替代
/// </summary>
[Obsolete("Use BuildEntries + GenerateUI instead", false)]
public void SchemaHydration(object payload)
{
// 清空环境和界面
allProperty.Clear();
InspectorPropertyPool.Clear();
if (!propertyUIPrefabsGroup)
{
Debug.LogError("未指定编组,无法生成界面");
return;
}
if (payload != null)
{
Debug.LogError("未指定有效负载,无法生成界面");
return;
}
// 从反射构建
var type = payload.GetType();
SchemaHydration(allProperty, type, payload);
BuildEntries(payload);
GenerateUI();
}
/// <summary>
/// 状态数据负载
/// </summary>
/// <param name="properties">反射属性结合</param>
/// <param name="payload">负载实例</param>
/// <typeparam name="T">进行检查的负载类型</typeparam>
private static void SchemaHydration<T>(List<XuiavrgInspectorProperty> properties, object payload)
=> SchemaHydration(properties, typeof(T), payload);
/// <summary>
/// 装填数据负载
/// </summary>
/// <param name="properties">反射属性集合</param>
/// <param name="payloadType">进行检查的负载类型</param>
/// <param name="payload">负载实例</param>
private static void SchemaHydration(List<XuiavrgInspectorProperty> properties, Type payloadType, object payload)
{
bool needSort = false;
foreach (var memberInfo in payloadType.GetRuntimeMemberInfo())
{
var mw = InspectorPropertyPool.Get();
// 绑定负载
mw.RegsiterDataValue(memberInfo, payload);
// 字段属性上的所有特性都进行标记
foreach (var memberAttribute in memberInfo.GetCustomAttributes()
.OfType<IXuiavrgAttribute>())
{
memberAttribute.BindProperty(mw);
needSort |= mw.inspectorOrder != 0; // 如果有成员使用了order
}
properties.Add(mw);
}
// 排序
if (needSort)
properties.Sort((property1, property2) => property1.inspectorOrder.CompareTo(property2.inspectorOrder));
}
#endregion
}
}
}
@@ -1,19 +1,82 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 界面属性编组
/// 提供预制体与 UI 标记的绑定,支持全局静态访问
/// </summary>
[Serializable]
[CreateAssetMenu(fileName = "Xeric UI avr Property group", menuName = "Xeric Library/UI Action Vessel/Reflection UI Gen/UI avr Property Group", order = 10)]
public class XuiavrgPropertyGroupScriptableObject : ScriptableObject, IList<XuiavrPropertyScriptableObject>
{
#region 全局注册表
/// <summary>
/// 全局预制体组列表
/// 所有 PropertyGroupScriptableObject 在 OnEnable 时自动注册,OnDisable 时自动注销
/// </summary>
internal static List<XuiavrgPropertyGroupScriptableObject> GlobalPrefabList
= new List<XuiavrgPropertyGroupScriptableObject>();
/// <summary>
/// 根据组名查找预制体组
/// </summary>
public static XuiavrgPropertyGroupScriptableObject FindGroupByName(string groupName)
{
return GlobalPrefabList.FirstOrDefault(g => g.groupName == groupName);
}
/// <summary>
/// 查找包含指定标记的预制体组
/// </summary>
public static List<XuiavrgPropertyGroupScriptableObject> FindGroupsByTag(XuiavrgFieldTag tag)
{
return GlobalPrefabList.Where(g => g.supportedTags.Contains(tag)).ToList();
}
/// <summary>
/// 查找同时包含所有指定标记的预制体组
/// </summary>
public static XuiavrgPropertyGroupScriptableObject FindGroupByTags(params XuiavrgFieldTag[] tags)
{
return GlobalPrefabList.FirstOrDefault(g =>
tags.All(t => g.supportedTags.Contains(t)));
}
#endregion
#region 字段
/// <summary>预制体组名称(用于 XuiavrgFieldPrefabOverride 匹配)</summary>
public string groupName;
/// <summary>此组支持的功能标记集合</summary>
public List<XuiavrgFieldTag> supportedTags = new List<XuiavrgFieldTag>();
public List<XuiavrPropertyScriptableObject> propertys = new List<XuiavrPropertyScriptableObject>();
#endregion
#region 生命周期
void OnEnable()
{
if (!GlobalPrefabList.Contains(this))
GlobalPrefabList.Add(this);
}
void OnDisable()
{
GlobalPrefabList.Remove(this);
}
#endregion
public IEnumerator<XuiavrPropertyScriptableObject> GetEnumerator()
{
return propertys.GetEnumerator();