改进属性动画和note生成逻辑

Signed-off-by: TRADER_FOER <lhf190@outlook.com>
This commit is contained in:
2026-06-20 15:58:40 +08:00
parent 93b085f3cd
commit 2146fdf881
15 changed files with 18981 additions and 18603 deletions

View File

@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Ichni.Editor;
using UnityEngine;
@@ -16,13 +19,49 @@ namespace Ichni.RhythmGame
public override void SetUpInspector()
{
base.SetUpInspector();
IHaveInspection insp = EditorManager.instance.uiManager.inspector;
Inspector inspector = EditorManager.instance.uiManager.inspector;
IHaveInspection insp = inspector;
// ── 组件下拉菜单 ──
List<string> componentOptions = new List<string>();
if (animatedObject != null)
{
var components = animatedObject.gameObject.GetComponents<Component>();
foreach (var c in components)
{
string fullName = c.GetType().FullName;
if (!componentOptions.Contains(fullName))
componentOptions.Add(fullName);
}
componentOptions.Sort();
}
// ── 属性/字段下拉菜单(基于当前 componentName ──
List<string> propertyOptions = new List<string>();
if (!string.IsNullOrEmpty(componentName))
{
Type compType = ResolveComponentType(componentName);
if (compType != null)
{
foreach (var prop in compType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (prop.CanWrite && prop.PropertyType == typeof(Color))
propertyOptions.Add(prop.Name);
}
foreach (var field in compType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (field.FieldType == typeof(Color) && !propertyOptions.Contains(field.Name))
propertyOptions.Add(field.Name);
}
propertyOptions.Sort();
}
}
InspectorBuilder.For(this)
.Section("Property Animation Color")
.InputField(nameof(componentName), "Component Name")
.OnChanged(() => AfterInitialize())
.InputField(nameof(propertyName), "Property Name")
.Dropdown(nameof(componentName), componentOptions, "Component")
.OnChanged(() => { AfterInitialize(); inspector.SetInspector(this); })
.Dropdown(nameof(propertyName), propertyOptions, "Property")
.OnChanged(() => AfterInitialize())
.Button("GraphicEditor", () => insp.GenerateGraphicalFlexibleFloatWindow(this, "Property Color",
new FlexibleFloat[] { propertyValueR, propertyValueG, propertyValueB, propertyValueA },
@@ -33,5 +72,15 @@ namespace Ichni.RhythmGame
.Button("Color A", () => insp.GenerateCompositeParameterWindow(this, "Color A", nameof(propertyValueA)).SetAsFlexibleFloat())
.Build();
}
private static Type ResolveComponentType(string typeName)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type type = assembly.GetType(typeName);
if (type != null) return type;
}
return null;
}
}
}

View File

@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Ichni.Editor;
using UnityEngine;
@@ -15,18 +18,64 @@ namespace Ichni.RhythmGame
public override void SetUpInspector()
{
base.SetUpInspector();
IHaveInspection insp = EditorManager.instance.uiManager.inspector;
Inspector inspector = EditorManager.instance.uiManager.inspector;
IHaveInspection insp = inspector;
// ── 组件下拉菜单 ──
List<string> componentOptions = new List<string>();
if (animatedObject != null)
{
var components = animatedObject.gameObject.GetComponents<Component>();
foreach (var c in components)
{
string fullName = c.GetType().FullName;
if (!componentOptions.Contains(fullName))
componentOptions.Add(fullName);
}
componentOptions.Sort();
}
// ── 属性/字段下拉菜单(基于当前 componentName ──
List<string> propertyOptions = new List<string>();
if (!string.IsNullOrEmpty(componentName))
{
Type compType = ResolveComponentType(componentName);
if (compType != null)
{
foreach (var prop in compType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (prop.CanWrite && prop.PropertyType == typeof(float))
propertyOptions.Add(prop.Name);
}
foreach (var field in compType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (field.FieldType == typeof(float) && !propertyOptions.Contains(field.Name))
propertyOptions.Add(field.Name);
}
propertyOptions.Sort();
}
}
InspectorBuilder.For(this)
.Section("Property Animation Float")
.InputField(nameof(componentName), "Component Name")
.OnChanged(() => AfterInitialize())
.InputField(nameof(propertyName), "Property Name")
.Dropdown(nameof(componentName), componentOptions, "Component")
.OnChanged(() => { AfterInitialize(); inspector.SetInspector(this); })
.Dropdown(nameof(propertyName), propertyOptions, "Property")
.OnChanged(() => AfterInitialize())
.Button("GraphicEditor", () => insp.GenerateGraphicalFlexibleFloatWindow(this, "Property Float",
new FlexibleFloat[] { propertyValue }, new string[] { "Value" }))
.Button("Float Value", () => insp.GenerateCompositeParameterWindow(this, "Float Value", nameof(propertyValue)).SetAsFlexibleFloat())
.Build();
}
private static Type ResolveComponentType(string typeName)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type type = assembly.GetType(typeName);
if (type != null) return type;
}
return null;
}
}
}

View File

@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Ichni.Editor;
using UnityEngine;
@@ -15,13 +18,49 @@ namespace Ichni.RhythmGame
public override void SetUpInspector()
{
base.SetUpInspector();
IHaveInspection insp = EditorManager.instance.uiManager.inspector;
Inspector inspector = EditorManager.instance.uiManager.inspector;
IHaveInspection insp = inspector;
// ── 组件下拉菜单 ──
List<string> componentOptions = new List<string>();
if (animatedObject != null)
{
var components = animatedObject.gameObject.GetComponents<Component>();
foreach (var c in components)
{
string fullName = c.GetType().FullName;
if (!componentOptions.Contains(fullName))
componentOptions.Add(fullName);
}
componentOptions.Sort();
}
// ── 属性/字段下拉菜单(基于当前 componentName ──
List<string> propertyOptions = new List<string>();
if (!string.IsNullOrEmpty(componentName))
{
Type compType = ResolveComponentType(componentName);
if (compType != null)
{
foreach (var prop in compType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (prop.CanWrite && prop.PropertyType == typeof(Vector3))
propertyOptions.Add(prop.Name);
}
foreach (var field in compType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (field.FieldType == typeof(Vector3) && !propertyOptions.Contains(field.Name))
propertyOptions.Add(field.Name);
}
propertyOptions.Sort();
}
}
InspectorBuilder.For(this)
.Section("Property Animation Vector3")
.InputField(nameof(componentName), "Component Name")
.OnChanged(() => AfterInitialize())
.InputField(nameof(propertyName), "Property Name")
.Dropdown(nameof(componentName), componentOptions, "Component")
.OnChanged(() => { AfterInitialize(); inspector.SetInspector(this); })
.Dropdown(nameof(propertyName), propertyOptions, "Property")
.OnChanged(() => AfterInitialize())
.Button("GraphicEditor", () => insp.GenerateGraphicalFlexibleFloatWindow(this, "Property Vector3",
new FlexibleFloat[] { propertyValueX, propertyValueY, propertyValueZ }, new string[] { "X", "Y", "Z" }))
@@ -30,5 +69,15 @@ namespace Ichni.RhythmGame
.Button("Vector Z", () => insp.GenerateCompositeParameterWindow(this, "Vector Z", nameof(propertyValueZ)).SetAsFlexibleFloat())
.Build();
}
private static Type ResolveComponentType(string typeName)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type type = assembly.GetType(typeName);
if (type != null) return type;
}
return null;
}
}
}

View File

@@ -17,19 +17,19 @@ namespace Ichni.RhythmGame
private Component targetComponent;
private FieldInfo targetField;
private PropertyInfo targetProperty;
// 高性能赋值委托
private Action<Color> colorSetterDelegate;
#endregion
#region [] Lifecycle & Factory
public static PropertyAnimationColor GenerateElement(string elementName, Guid id, List<string> tags, bool isFirstGenerated,
GameElement animatedObject, string componentName, string propertyName,
GameElement animatedObject, string componentName, string propertyName,
FlexibleFloat propertyValueR, FlexibleFloat propertyValueG, FlexibleFloat propertyValueB, FlexibleFloat propertyValueA)
{
PropertyAnimationColor animation = Instantiate(EditorManager.instance.basePrefabs.emptyObject)
.AddComponent<PropertyAnimationColor>();
animation.Initialize(elementName, id, tags, isFirstGenerated, animatedObject);
animation.animatedObject = animatedObject;
@@ -64,10 +64,10 @@ namespace Ichni.RhythmGame
{
Debug.LogWarning($"[PropertyAnimationColor] Cannot find Component '{componentName}' strictly on '{animatedObject.name}'.");
}
base.AfterInitialize();
}
private Type GetTypeFromAllAssemblies(string typeName)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
@@ -125,7 +125,7 @@ namespace Ichni.RhythmGame
if (colorSetterDelegate != null)
{
colorSetterDelegate(targetColor);
colorSetterDelegate(targetColor);
}
else if (targetProperty != null)
{
@@ -135,20 +135,24 @@ namespace Ichni.RhythmGame
{
targetField.SetValue(targetComponent, targetColor);
}
if (animatedObject is IHaveDirtyMarkSubmodule dirtyTarget)
{
dirtyTarget.dirtyMarkSubmodule.MarkDirty(propertyName);
}
else
{
animatedObject.Refresh();
}
}
public override void ApplyTimeOffset(float offset)
{
base.ApplyTimeOffset(offset);
void ApplyOffset(FlexibleFloat ff)
{
if (ff == null || ff.animations == null) return;
foreach(var a in ff.animations) { a.startTime += offset; a.endTime += offset; }
foreach (var a in ff.animations) { a.startTime += offset; a.endTime += offset; }
}
ApplyOffset(propertyValueR);
ApplyOffset(propertyValueG);

View File

@@ -17,7 +17,7 @@ namespace Ichni.RhythmGame
private Component targetComponent;
private FieldInfo targetField;
private PropertyInfo targetProperty;
// 我们尝试通过原生的 Action 来降低反射 SetValue 带来的性能损耗装箱与GC
private Action<float> floatSetterDelegate;
#endregion
@@ -28,7 +28,7 @@ namespace Ichni.RhythmGame
{
PropertyAnimationFloat animation = Instantiate(EditorManager.instance.basePrefabs.emptyObject)
.AddComponent<PropertyAnimationFloat>();
animation.Initialize(elementName, id, tags, isFirstGenerated, animatedObject);
animation.animatedObject = animatedObject;
@@ -60,10 +60,10 @@ namespace Ichni.RhythmGame
{
Debug.LogWarning($"[PropertyAnimationFloat] Cannot find Component '{componentName}' strictly on '{animatedObject.name}'.");
}
base.AfterInitialize();
}
private Type GetTypeFromAllAssemblies(string typeName)
{
// 对于跨程序集的搜索
@@ -125,7 +125,7 @@ namespace Ichni.RhythmGame
if (floatSetterDelegate != null)
{
floatSetterDelegate(value);
floatSetterDelegate(value);
}
else if (targetProperty != null)
{
@@ -135,19 +135,23 @@ namespace Ichni.RhythmGame
{
targetField.SetValue(targetComponent, value);
}
if (animatedObject is IHaveDirtyMarkSubmodule dirtyTarget)
{
dirtyTarget.dirtyMarkSubmodule?.MarkDirty(propertyName);
}
else
{
animatedObject.Refresh();
}
}
public override void ApplyTimeOffset(float offset)
{
base.ApplyTimeOffset(offset);
if (propertyValue != null && propertyValue.animations != null)
{
foreach(var a in propertyValue.animations)
foreach (var a in propertyValue.animations)
{
a.startTime += offset;
a.endTime += offset;

View File

@@ -17,19 +17,19 @@ namespace Ichni.RhythmGame
private Component targetComponent;
private FieldInfo targetField;
private PropertyInfo targetProperty;
// 高性能赋值委托
private Action<Vector3> vectorSetterDelegate;
#endregion
#region [] Lifecycle & Factory
public static PropertyAnimationVector3 GenerateElement(string elementName, Guid id, List<string> tags, bool isFirstGenerated,
GameElement animatedObject, string componentName, string propertyName,
GameElement animatedObject, string componentName, string propertyName,
FlexibleFloat propertyValueX, FlexibleFloat propertyValueY, FlexibleFloat propertyValueZ)
{
PropertyAnimationVector3 animation = Instantiate(EditorManager.instance.basePrefabs.emptyObject)
.AddComponent<PropertyAnimationVector3>();
animation.Initialize(elementName, id, tags, isFirstGenerated, animatedObject);
animation.animatedObject = animatedObject;
@@ -63,10 +63,10 @@ namespace Ichni.RhythmGame
{
Debug.LogWarning($"[PropertyAnimationVector3] Cannot find Component '{componentName}' strictly on '{animatedObject.name}'.");
}
base.AfterInitialize();
}
private Type GetTypeFromAllAssemblies(string typeName)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
@@ -121,7 +121,7 @@ namespace Ichni.RhythmGame
if (vectorSetterDelegate != null)
{
vectorSetterDelegate(targetVector);
vectorSetterDelegate(targetVector);
}
else if (targetProperty != null)
{
@@ -131,20 +131,24 @@ namespace Ichni.RhythmGame
{
targetField.SetValue(targetComponent, targetVector);
}
if (animatedObject is IHaveDirtyMarkSubmodule dirtyTarget)
{
dirtyTarget.dirtyMarkSubmodule.MarkDirty(propertyName);
}
else
{
animatedObject.Refresh();
}
}
public override void ApplyTimeOffset(float offset)
{
base.ApplyTimeOffset(offset);
void ApplyOffset(FlexibleFloat ff)
{
if (ff == null || ff.animations == null) return;
foreach(var a in ff.animations) { a.startTime += offset; a.endTime += offset; }
foreach (var a in ff.animations) { a.startTime += offset; a.endTime += offset; }
}
ApplyOffset(propertyValueX);
ApplyOffset(propertyValueY);

View File

@@ -56,6 +56,12 @@ namespace Ichni.RhythmGame
public override void AfterInitialize()
{
base.AfterInitialize();
// 修复 A1当 noteVisual 为空时useNotePrefab=false 或预制体加载失败),
// 自动从当前已加载的主题包中查找匹配的 NoteVisual 并生成。
// 这确保即使没有预制体系统Note 依然有视觉表现。
EnsureNoteVisual();
generateEffects = GetEffectListSafe("Generate");
generalJudgeEffects = GetEffectListSafe("GeneralJudge");
perfectEffects = GetEffectListSafe("Perfect");
@@ -67,6 +73,35 @@ namespace Ichni.RhythmGame
AddinNoteManager();
}
/// <summary>
/// 当 noteVisual 为空时,从已加载的主题包中自动查找并生成匹配的默认 NoteVisual。
/// 修复 useNotePrefab=false 或预制体加载失败导致 Note 无视觉表现的问题。
/// </summary>
private void EnsureNoteVisual()
{
if (noteVisual != null) return;
if (!isOnTrack || track == null) return;
if (ThemeBundleManager.instance.loadedThemeBundleList.Count == 0) return;
foreach (var bundle in ThemeBundleManager.instance.loadedThemeBundleList)
{
foreach (var go in bundle.assetList_GameObject)
{
NoteVisualBase visual = go.GetComponent<NoteVisualBase>();
if (visual == null) continue;
// Tap/Stay/Flick 使用非 Hold 视觉Hold 使用 Hold 视觉
if (this is Tap or Stay or Flick && visual is NoteVisualBaseHold) continue;
if (this is Hold && visual is not NoteVisualBaseHold) continue;
// 通过工厂方法创建 NoteVisualisFirstGenerated=true 确保效果注册和 AfterInitialize
SubstantialObject.GenerateElement("NoteVisual", Guid.NewGuid(),
new List<string>(), true, bundle.themeBundleName, go.name, this);
return; // 使用查找到的第一个匹配视觉
}
}
}
public override void SetDefaultSubmodules()
{
timeDurationSubmodule ??= new TimeDurationSubmodule(this);

View File

@@ -48,11 +48,21 @@ namespace Ichni.RhythmGame
/// <summary>
/// NoteVisual 初始化完毕后,通知父 Note 重新缓存效果列表。
/// 解决手动生成 NoteVisual 时,父 Note 的 generateEffects 等缓存为 null 导致特效不被驱动的问题。
/// 同时在此延迟创建 effectSubmodule当 BM/预制体数据缺少效果子模块时),
/// 避免在 GenerateElement 工厂中预创建导致与后续 Submodule_BM 恢复冲突的重复断言。
/// </summary>
public override void AfterInitialize()
{
base.AfterInitialize();
// 延迟初始化:在 BM 加载管线全部执行完毕后,
// 如果 effectSubmodule 仍为 null缺少 BM 数据),则使用空预设创建,
// 确保后续 GetEffectListSafe 返回空列表而非 null。
if (effectSubmodule == null)
{
effectSubmodule = new EffectSubmodule(this, EffectSubmodule.EffectSubmodulePreset.Note);
}
if (note != null)
{
note.RefreshNoteVisualCaches();

View File

@@ -140,6 +140,7 @@ namespace Ichni.RhythmGame
public override void Refresh()
{
base.Refresh();
SetShape();
ParticleSystemRenderer particleSystemRenderer = particle.GetComponent<ParticleSystemRenderer>();
particleSystemRenderer.material.SetColor("_BaseColor", colorSubmodule.currentBaseColor);
if (colorSubmodule.emissionEnabled)