This commit is contained in:
SoulliesOfficial
2026-06-09 01:43:55 -04:00
parent 0fb72f5bba
commit 5fc1392747
171 changed files with 30149 additions and 22331 deletions

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: ea823e8c44a0ca842a3b0e59390994de
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,98 +0,0 @@
using System;
namespace Ichni.Editor
{
/// <summary>
/// 数值或枚举控制参数标签,挂载在 Field 或 Property 上
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class DynamicUIAttribute : Attribute
{
public string Name { get; set; }
/// <summary>
/// 属于哪个 Container (比如 "InGame Settings" 或 "Path Node")
/// </summary>
public string Group { get; set; }
/// <summary>
/// 排版格子要求。一行最大宽度分为 3 份。
/// 填 1 代表需占 1/3。填 3 代表全宽占满整行。
/// 若填 0由内置工厂根据数据类型自动识别例如 Vector3 与 Color 自动为 3bool为1 等)。
/// </summary>
public int Span { get; set; }
/// <summary>
/// 垂直占位高度。填 0 由系统智能推断(如基础件 100ColorPicker 240也可显式填写如 150f 覆盖。
/// </summary>
public float Height { get; set; }
public bool AutoUpdate { get; set; }
public bool ReadOnly { get; set; }
public DynamicUIAttribute(string name = "", string group = "Default", int span = 0, float height = 0f, bool autoUpdate = false, bool readOnly = false)
{
Name = name;
Group = group;
Span = span;
Height = height;
AutoUpdate = autoUpdate;
ReadOnly = readOnly;
}
}
/// <summary>
/// 支持 HDR 以及独立 Emission 强度或开关控制通道的拾色器。
/// 若无独立通道字段可置为 "NULL",将自动转为读取并写入至 Target Color 的 Alpha 通道中。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class DynamicUIEmissionColorAttribute : DynamicUIAttribute
{
public string EmissionEnabledName { get; set; }
public string EmissionIntensityName { get; set; }
public DynamicUIEmissionColorAttribute(string name, string enabledName = "NULL", string intensityName = "NULL", string group = "Default", int span = 3, float height = 0f)
: base(name, group, span, height)
{
EmissionEnabledName = enabledName;
EmissionIntensityName = intensityName;
}
}
/// <summary>
/// 滑块控制属性,适用于 float/int 类型字段,自动生成带范围限制的 Slider 替代 InputField。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class DynamicUISliderAttribute : DynamicUIAttribute
{
public float Min { get; set; }
public float Max { get; set; }
public bool WholeNumbers { get; set; }
public DynamicUISliderAttribute(string name = "", string group = "Default",
float min = 0f, float max = 1f, bool wholeNumbers = false,
int span = 0, float height = 0f, bool autoUpdate = false, bool readOnly = false)
: base(name, group, span, height, autoUpdate, readOnly)
{
Min = min;
Max = max;
WholeNumbers = wholeNumbers;
}
}
/// <summary>
/// 操作按钮卡片,挂载在没有任何参数的 Method() 方法上
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class DynamicUIButtonAttribute : Attribute
{
public string Name { get; set; }
public string Group { get; set; }
public DynamicUIButtonAttribute(string name = "", string group = "Default")
{
Name = name;
Group = group;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8a540054c95f48544bd6efcea6b452cc

View File

@@ -1,228 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using Ichni.RhythmGame;
using UnityEngine.Events;
namespace Ichni.Editor
{
public static class DynamicUIAutoBuilder
{
// 反射信息快取
private class FieldDef
{
public MemberInfo Member;
public DynamicUIAttribute Attr;
public Type ValueType;
}
private class MethodDef
{
public MethodInfo Method;
public DynamicUIButtonAttribute Attr;
}
/// <summary>
/// 接管任何 IBaseElement 的 UI 生成作业
/// </summary>
public static void AutoBuild(IBaseElement element, IHaveInspection inspector)
{
Type t = element.GetType();
// 扫描所有带 DynamicUI 标记的字段或属性
var dynamicFields = new List<FieldDef>();
foreach (var member in t.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
var uiAttr = member.GetCustomAttribute<DynamicUIAttribute>();
if (uiAttr != null)
{
Type vType = member is FieldInfo f ? f.FieldType : (member as PropertyInfo)?.PropertyType;
if (vType != null) dynamicFields.Add(new FieldDef { Member = member, Attr = uiAttr, ValueType = vType });
}
}
// 扫描所有带 DynamicUIButton 的方法
var dynamicMethods = new List<MethodDef>();
foreach (var method in t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
var btnAttr = method.GetCustomAttribute<DynamicUIButtonAttribute>();
if (btnAttr != null && method.GetParameters().Length == 0)
{
dynamicMethods.Add(new MethodDef { Method = method, Attr = btnAttr });
}
}
// 按照 Group 归类准备流式渲染
var allGroups = dynamicFields.Select(f => f.Attr.Group)
.Concat(dynamicMethods.Select(m => m.Attr.Group))
.Distinct().ToList();
foreach (var groupName in allGroups)
{
var container = inspector.GenerateContainer(groupName);
var fieldsInGroup = dynamicFields.Where(f => f.Attr.Group == groupName).ToList();
var methodsInGroup = dynamicMethods.Where(m => m.Attr.Group == groupName).ToList();
// ======== 智能栅格打包推演 ========
List<FieldDef> currentLineBuffer = new List<FieldDef>();
int currentSpanCount = 0; // 一行容量最高是 3
// 先排变量
foreach (var field in fieldsInGroup)
{
int span = GetEffectiveSpan(field);
span = Mathf.Clamp(span, 1, 3);
// 如果该行的槽被塞满了或者装不下了
if (currentSpanCount > 0 && currentSpanCount + span > 3)
{
FlushLine(element, inspector, container, currentLineBuffer, 3);
currentLineBuffer.Clear();
currentSpanCount = 0;
}
// 特殊兼容:由于 GridLayoutGroup 的强制统一 cellSize, 我们如果在同一行混搭不同 Span 宽度的,反而会让它变形。
// 因此若当前这行的空间要求和前一个需求跨度不一致,也必须强行断点分层!
if (currentLineBuffer.Count > 0)
{
var lastSpan = GetEffectiveSpan(currentLineBuffer[0]);
if (span != lastSpan)
{
FlushLine(element, inspector, container, currentLineBuffer, GetHighestSpanRequirement(currentLineBuffer));
currentLineBuffer.Clear();
currentSpanCount = 0;
}
}
currentLineBuffer.Add(field);
currentSpanCount += span;
}
if (currentLineBuffer.Count > 0) FlushLine(element, inspector, container, currentLineBuffer, GetHighestSpanRequirement(currentLineBuffer));
// ======== 最后把按钮并排排好 ========
if (methodsInGroup.Count > 0)
{
var btnSubcontainer = container.GenerateSubcontainer(3); // 按钮统统放在 1/3 的槽里(你可改规则)
foreach (var method in methodsInGroup)
{
MethodInfo mInfo = method.Method;
// 通过强行抓取生成 UnityAction 给工厂
UnityAction action = () => { mInfo.Invoke(element, null); element.Refresh(); };
string n = string.IsNullOrEmpty(method.Attr.Name) ? mInfo.Name : method.Attr.Name;
inspector.GenerateButton(element, btnSubcontainer, n, action);
}
}
}
}
// 核心工厂发射器
private static void FlushLine(IBaseElement element, IHaveInspection inspector, DynamicUIContainer container, List<FieldDef> fields, int lineSpanCapacity)
{
// 如果一行要放 3 份Span 都是 1 拼成的),那格子数量也是 3600/3=200一个
// 但是如果是独占的Span 都是 3格子数量反而应该是 1600/1=600一个
// 为了防止排版错开变形,我们通过 3 / span 来决定 Subcontainer 内部允许的等分元素数量。
int subcontainerDivision = Mathf.Max(1, Mathf.RoundToInt(3f / lineSpanCapacity));
float rowHeight = GetHighestHeightRequirement(fields);
var sub = container.GenerateSubcontainer(subcontainerDivision, rowHeight);
foreach (var fd in fields)
{
string title = string.IsNullOrEmpty(fd.Attr.Name) ? fd.Member.Name : fd.Attr.Name;
string pName = fd.Member.Name;
if (fd.Attr.ReadOnly)
{
var val = ReflectionHelper.GetDeepValue(element, pName);
inspector.GenerateHintText(element, sub, $"{title}: {val}");
continue;
}
// 智能类型分流
if (fd.Attr is DynamicUIEmissionColorAttribute emissionAttr)
{
// 使用之前为了消除扩展方法调不出来的尴尬而专门暴露的强转/普通调用,我们直接这里也安全使用
IHaveInspection rawInspector = inspector as IHaveInspection;
if(rawInspector != null) rawInspector.GenerateEmissionColorPicker(element, sub, title, emissionAttr.EmissionEnabledName, pName, emissionAttr.EmissionIntensityName);
}
else if (fd.ValueType == typeof(bool))
{
inspector.GenerateToggle(element, sub, title, pName);
}
else if (fd.ValueType == typeof(Vector2))
{
inspector.GenerateVector2InputField(element, sub, title, pName, fd.Attr.AutoUpdate);
}
else if (fd.ValueType == typeof(Vector3))
{
inspector.GenerateVector3InputField(element, sub, title, pName, fd.Attr.AutoUpdate);
}
else if (fd.ValueType == typeof(Color))
{
inspector.GenerateBaseColorPicker(element, sub, title, pName);
}
else if (fd.ValueType.IsEnum)
{
inspector.GenerateDropdown(element, sub, title, fd.ValueType, pName);
}
else if (fd.Attr is DynamicUISliderAttribute sliderAttr)
{
inspector.GenerateSlider(element, sub, title, pName, sliderAttr.Min, sliderAttr.Max, sliderAttr.WholeNumbers);
}
else // 容错给普通的 InputField
{
inspector.GenerateInputField(element, sub, title, pName, fd.Attr.AutoUpdate);
}
}
}
private static int SmartInferSpan(Type t)
{
if (t == typeof(bool)) return 1;
if (t == typeof(float) || t == typeof(int) || t == typeof(string)) return 1;
if (t.IsEnum) return 1;
if (t == typeof(Vector2)) return 2;
if (t == typeof(Vector3) || t == typeof(Color)) return 3; // 巨型组件通篇独占一行
return 1;
}
private static int GetHighestSpanRequirement(List<FieldDef> fields)
{
int highest = 1;
foreach (var f in fields)
{
int span = GetEffectiveSpan(f);
if (span > highest) highest = span;
}
return highest;
}
private static int GetEffectiveSpan(FieldDef fd)
{
return fd.Attr.Span > 0 ? fd.Attr.Span : SmartInferSpan(fd.ValueType);
}
private static float SmartInferHeight(FieldDef fd)
{
// EmissionColorPicker 有 Toggle/RGB滑块/Intensity输入框高度明显大于 BaseColorPicker
if (fd.Attr is DynamicUIEmissionColorAttribute) return 320f;
if (fd.ValueType == typeof(Color)) return 280f;
// 多数基础组件默认为 100
return 100f;
}
private static float GetHighestHeightRequirement(List<FieldDef> fields)
{
float highest = 100f;
foreach (var f in fields)
{
float height = f.Attr.Height > 0 ? f.Attr.Height : SmartInferHeight(f);
if (height > highest) highest = height;
}
return highest;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f0bc252acb576104aaff0846e4c1e136

View File

@@ -13,6 +13,11 @@ using Object = UnityEngine.Object;
namespace Ichni.Editor
{
/// <summary>
/// Inspector 工厂接口 —— 提供底层 UI 控件的实例化方法。
/// 外部代码应通过 <see cref="InspectorBuilder"/> 构建 Inspector
/// 而非直接调用本接口的 Generate 方法。
/// </summary>
public interface IHaveInspection
{
public RectTransform WindowRect { get; set; }
@@ -226,6 +231,7 @@ namespace Ichni.Editor
return hintText;
}
[Obsolete("GenerateParameterText 已无外部调用者,请使用 InspectorBuilder.HintText() 替代。")]
public DynamicUIParameterText GenerateParameterText(IBaseElement baseElement, DynamicUISubcontainer subcontainer,
string title, string parameterName, bool isAutoUpdate = false)
{

View File

@@ -22,6 +22,9 @@ namespace Ichni.Editor
public Dictionary<string, DynamicUISubcontainer> MarkedSubcontainers { get; set; }
public Dictionary<string, DynamicUIElement> MarkedElements { get; set; }
/// <summary> Drawer 注册表单例的便捷访问 </summary>
public DrawerRegistry DrawerRegistry => DrawerRegistry.Instance;
private void Awake()
{
WindowRect = inspectorRect;

View File

@@ -1,59 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Ichni.RhythmGame;
using UnityEngine;
namespace Ichni.Editor
{
public static class StandardInspectionElement
{
private static IHaveInspection inspector => EditorManager.instance.uiManager.inspector;
private static Inspector inspectorUI => EditorManager.instance.uiManager.inspector;
public static void GenerateForTransform(GameElement gameElement, DynamicUIContainer generateContainer = null)//关于有Transform
{
if (generateContainer is null)
{
generateContainer = inspector.GenerateContainer("Generate Elements");
}
var animationSubcontainer = generateContainer.GenerateSubcontainer(3);
var displacementButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Displacement", () =>
{
Displacement.GenerateElement("New Displacement", Guid.NewGuid(), new List<string>(), true, gameElement,
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
}); //位移
var swirlButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Swirl", () =>
{
Swirl.GenerateElement("New Swirl", Guid.NewGuid(), new List<string>(), true, gameElement,
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
}); //旋转
var scaleButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Scale", () =>
{
Scale.GenerateElement("New Scale", Guid.NewGuid(), new List<string>(), true, gameElement,
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
}); //缩放
var LookAtButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Look At",
() => LookAt.GenerateElement("New Look At", Guid.NewGuid(),
new List<string>(), true, gameElement, null, new FlexibleBool()));
var displacementTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Displacement Tracker", () =>
{
DisplacementTracker.GenerateElement("New Displacement Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
null, 0f);
});
var swirlTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Swirl Tracker", () =>
{
SwirlTracker.GenerateElement("New Swirl Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
null, 0f);
}); var ScaleTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Scale Tracker", () =>
{
ScaleTracker.GenerateElement("New Scale Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
null, 0f);
});
}
public static void GenerateForLoading()
{
inspectorUI.ClearInspector();
var container = inspector.GenerateContainer("Loading");
}
}
}

View File

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