谱面改进

This commit is contained in:
SoulliesOfficial
2026-04-09 11:03:18 -04:00
parent 9af26bb435
commit 3a63641a2c
106 changed files with 1994 additions and 336 deletions

View File

@@ -12,8 +12,13 @@ namespace Ichni.RhythmGame
#region [] Exposed Fields & References
private TransformSubmodule targetTransformSubmodule;
public FlexibleFloat positionX, positionY, positionZ;
public static HashSet<Displacement> ActiveDisplacements = new HashSet<Displacement>();
#endregion
protected void OnEnable() { ActiveDisplacements.Add(this); }
protected void OnDisable() { ActiveDisplacements.Remove(this); }
#region [] Lifecycle & Factory
public static Displacement GenerateElement(string elementName, Guid id,
List<string> tags, bool isFirstGenerated, GameElement animatedObject,
@@ -45,21 +50,65 @@ namespace Ichni.RhythmGame
positionY.UpdateFlexibleFloat(songTime);
positionZ.UpdateFlexibleFloat(songTime);
if (forceUpdate || positionX.returnType is FlexibleReturnType.MiddleExecuting ||
positionY.returnType is FlexibleReturnType.MiddleExecuting ||
positionZ.returnType is FlexibleReturnType.MiddleExecuting)
bool isMiddleExecuting = positionX.returnType is FlexibleReturnType.MiddleExecuting ||
positionY.returnType is FlexibleReturnType.MiddleExecuting ||
positionZ.returnType is FlexibleReturnType.MiddleExecuting;
bool isSwitching = positionX.isSwitchingReturnType || positionY.isSwitchingReturnType || positionZ.isSwitchingReturnType;
if (forceUpdate || isMiddleExecuting)
{
if(!forceUpdate) animationReturnType = FlexibleReturnType.MiddleExecuting;
Vector3 currentPosition = new Vector3(positionX.value, positionY.value, positionZ.value);
targetTransformSubmodule.positionOffset += currentPosition;
targetTransformSubmodule.positionDirtyMark = true;
// 首帧合并退让保护
bool shouldSkipFirstFrame = false;
if (!forceUpdate && isSwitching)
{
foreach (var d in ActiveDisplacements)
{
if (d != this && d.targetTransformSubmodule == this.targetTransformSubmodule)
{
if (d.positionX.isSwitchingReturnType || d.positionY.isSwitchingReturnType || d.positionZ.isSwitchingReturnType)
{
shouldSkipFirstFrame = true;
break;
}
}
}
}
if (!shouldSkipFirstFrame)
{
Vector3 currentPosition = new Vector3(positionX.value, positionY.value, positionZ.value);
targetTransformSubmodule.positionOffset += currentPosition;
targetTransformSubmodule.positionDirtyMark = true;
}
}
else if (positionX.isSwitchingReturnType || positionY.isSwitchingReturnType || positionZ.isSwitchingReturnType)
else if (isSwitching)
{
//animationReturnType = FlexibleReturnType.MiddleExecuting;
//Vector3 currentPosition = new Vector3(positionX.value, positionY.value, positionZ.value);
//targetTransformSubmodule.positionOffset += currentPosition;
//targetTransformSubmodule.positionDirtyMark = true;
// 最终帧补偿机制
bool isAnyOtherExecuting = false;
foreach (var d in ActiveDisplacements)
{
if (d != this && d.targetTransformSubmodule == this.targetTransformSubmodule)
{
if (d.positionX.returnType is FlexibleReturnType.MiddleExecuting ||
d.positionY.returnType is FlexibleReturnType.MiddleExecuting ||
d.positionZ.returnType is FlexibleReturnType.MiddleExecuting)
{
isAnyOtherExecuting = true;
break;
}
}
}
if (!isAnyOtherExecuting)
{
animationReturnType = FlexibleReturnType.MiddleExecuting;
Vector3 currentPosition = new Vector3(positionX.value, positionY.value, positionZ.value);
targetTransformSubmodule.positionOffset += currentPosition;
targetTransformSubmodule.positionDirtyMark = true;
}
}
else
{

View File

@@ -11,8 +11,14 @@ namespace Ichni.RhythmGame
#region [] Exposed Fields & References
public TransformSubmodule targetTransformSubmodule;
public FlexibleFloat scaleX, scaleY, scaleZ;
// 静态跟踪全局激活的Scale组件为了方便同物体的跨动画状态感应
public static HashSet<Scale> ActiveScales = new HashSet<Scale>();
#endregion
protected void OnEnable() { ActiveScales.Add(this); }
protected void OnDisable() { ActiveScales.Remove(this); }
#region [] Lifecycle & Factory
public static Scale GenerateElement(string elementName, Guid id,
List<string> tags, bool isFirstGenerated, GameElement animatedObject,
@@ -49,21 +55,65 @@ namespace Ichni.RhythmGame
scaleY.UpdateFlexibleFloat(songTime);
scaleZ.UpdateFlexibleFloat(songTime);
if (forceUpdate || scaleX.returnType is FlexibleReturnType.MiddleExecuting ||
scaleY.returnType is FlexibleReturnType.MiddleExecuting ||
scaleZ.returnType is FlexibleReturnType.MiddleExecuting)
bool isMiddleExecuting = scaleX.returnType is FlexibleReturnType.MiddleExecuting ||
scaleY.returnType is FlexibleReturnType.MiddleExecuting ||
scaleZ.returnType is FlexibleReturnType.MiddleExecuting;
bool isSwitching = scaleX.isSwitchingReturnType || scaleY.isSwitchingReturnType || scaleZ.isSwitchingReturnType;
if (forceUpdate || isMiddleExecuting)
{
if(!forceUpdate) animationReturnType = FlexibleReturnType.MiddleExecuting;
Vector3 currentScale = new Vector3(scaleX.value, scaleY.value, scaleZ.value);
targetTransformSubmodule.scaleOffset += currentScale;
targetTransformSubmodule.scaleDirtyMark = true;
// 检查是否是刚开始的第一帧,且有其它同类动画正在收尾(避免两帧叠加导致 ScaleX2
bool shouldSkipFirstFrame = false;
if (!forceUpdate && isSwitching)
{
foreach (var s in ActiveScales)
{
if (s != this && s.targetTransformSubmodule == this.targetTransformSubmodule)
{
if (s.scaleX.isSwitchingReturnType || s.scaleY.isSwitchingReturnType || s.scaleZ.isSwitchingReturnType)
{
shouldSkipFirstFrame = true;
break;
}
}
}
}
if (!shouldSkipFirstFrame)
{
Vector3 currentScale = new Vector3(scaleX.value, scaleY.value, scaleZ.value);
targetTransformSubmodule.scaleOffset += currentScale;
targetTransformSubmodule.scaleDirtyMark = true;
}
}
else if (scaleX.isSwitchingReturnType || scaleY.isSwitchingReturnType || scaleZ.isSwitchingReturnType)
else if (isSwitching)
{
//animationReturnType = FlexibleReturnType.MiddleExecuting;
//Vector3 currentScale = new Vector3(scaleX.value, scaleY.value, scaleZ.value);
//targetTransformSubmodule.scaleOffset += currentScale;
//targetTransformSubmodule.scaleDirtyMark = true;
// 【收尾保护】如果这个动画是附着物体的“最后的动画”的结束,额外更新彻底将其设为终定值
bool isAnyOtherExecuting = false;
foreach (var s in ActiveScales)
{
if (s != this && s.targetTransformSubmodule == this.targetTransformSubmodule)
{
if (s.scaleX.returnType is FlexibleReturnType.MiddleExecuting ||
s.scaleY.returnType is FlexibleReturnType.MiddleExecuting ||
s.scaleZ.returnType is FlexibleReturnType.MiddleExecuting)
{
isAnyOtherExecuting = true;
break;
}
}
}
if (!isAnyOtherExecuting)
{
animationReturnType = FlexibleReturnType.MiddleExecuting; // 使系统认为有有效活动
Vector3 currentScale = new Vector3(scaleX.value, scaleY.value, scaleZ.value);
targetTransformSubmodule.scaleOffset += currentScale;
targetTransformSubmodule.scaleDirtyMark = true;
}
}
else
{

View File

@@ -12,8 +12,13 @@ namespace Ichni.RhythmGame
#region [] Exposed Fields & References
private TransformSubmodule targetTransformSubmodule;
public FlexibleFloat eulerAngleX, eulerAngleY, eulerAngleZ;
public static HashSet<Swirl> ActiveSwirls = new HashSet<Swirl>();
#endregion
protected void OnEnable() { ActiveSwirls.Add(this); }
protected void OnDisable() { ActiveSwirls.Remove(this); }
#region [] Lifecycle & Factory
public static Swirl GenerateElement(string elementName, Guid id,
List<string> tags, bool isFirstGenerated, GameElement animatedObject,
@@ -43,22 +48,65 @@ namespace Ichni.RhythmGame
eulerAngleY.UpdateFlexibleFloat(songTime);
eulerAngleZ.UpdateFlexibleFloat(songTime);
if (forceUpdate || eulerAngleX.returnType is FlexibleReturnType.MiddleExecuting ||
eulerAngleY.returnType is FlexibleReturnType.MiddleExecuting ||
eulerAngleZ.returnType is FlexibleReturnType.MiddleExecuting)
bool isMiddleExecuting = eulerAngleX.returnType is FlexibleReturnType.MiddleExecuting ||
eulerAngleY.returnType is FlexibleReturnType.MiddleExecuting ||
eulerAngleZ.returnType is FlexibleReturnType.MiddleExecuting;
bool isSwitching = eulerAngleX.isSwitchingReturnType || eulerAngleY.isSwitchingReturnType || eulerAngleZ.isSwitchingReturnType;
if (forceUpdate || isMiddleExecuting)
{
if(!forceUpdate) animationReturnType = FlexibleReturnType.MiddleExecuting;
Vector3 currentEulerAngles = new Vector3(eulerAngleX.value, eulerAngleY.value, eulerAngleZ.value);
targetTransformSubmodule.eulerAnglesOffset += currentEulerAngles;
targetTransformSubmodule.eulerAnglesDirtyMark = true;
// 首帧合并退让保护
bool shouldSkipFirstFrame = false;
if (!forceUpdate && isSwitching)
{
foreach (var s in ActiveSwirls)
{
if (s != this && s.targetTransformSubmodule == this.targetTransformSubmodule)
{
if (s.eulerAngleX.isSwitchingReturnType || s.eulerAngleY.isSwitchingReturnType || s.eulerAngleZ.isSwitchingReturnType)
{
shouldSkipFirstFrame = true;
break;
}
}
}
}
if (!shouldSkipFirstFrame)
{
Vector3 currentEulerAngles = new Vector3(eulerAngleX.value, eulerAngleY.value, eulerAngleZ.value);
targetTransformSubmodule.eulerAnglesOffset += currentEulerAngles;
targetTransformSubmodule.eulerAnglesDirtyMark = true;
}
}
else if (eulerAngleX.isSwitchingReturnType || eulerAngleY.isSwitchingReturnType || eulerAngleZ.isSwitchingReturnType)
else if (isSwitching)
{
//animationReturnType = FlexibleReturnType.MiddleExecuting;
//Vector3 currentEulerAngles = new Vector3(eulerAngleX.value, eulerAngleY.value, eulerAngleZ.value);
//targetTransformSubmodule.eulerAnglesOffset += currentEulerAngles;
//Debug.Break();
//targetTransformSubmodule.eulerAnglesDirtyMark = true;
// 最终帧补偿机制
bool isAnyOtherExecuting = false;
foreach (var s in ActiveSwirls)
{
if (s != this && s.targetTransformSubmodule == this.targetTransformSubmodule)
{
if (s.eulerAngleX.returnType is FlexibleReturnType.MiddleExecuting ||
s.eulerAngleY.returnType is FlexibleReturnType.MiddleExecuting ||
s.eulerAngleZ.returnType is FlexibleReturnType.MiddleExecuting)
{
isAnyOtherExecuting = true;
break;
}
}
}
if (!isAnyOtherExecuting)
{
animationReturnType = FlexibleReturnType.MiddleExecuting;
Vector3 currentEulerAngles = new Vector3(eulerAngleX.value, eulerAngleY.value, eulerAngleZ.value);
targetTransformSubmodule.eulerAnglesOffset += currentEulerAngles;
targetTransformSubmodule.eulerAnglesDirtyMark = true;
}
}
else
{

View File

@@ -1,40 +1,92 @@
using Ichni.RhythmGame.Beatmap;
using Lean.Pool;
using UnityEngine;
namespace Ichni.RhythmGame
{
public class HighPassFilterEffect : EffectBase
/// <summary>
/// 高通滤波效果Wwise RTPC 驱动版)。
/// 在 Wwise 中需要创建名为 "Song_Highpass" 的 RTPC范围 0~100
/// 并将其连接到 Music Bus 的 High-Pass Filter 参数Built-in Parameter
///
/// BM 中存储的 peak 是 Unity AudioHighPassFilter.cutoffFrequencyHz
/// 运行时使用对数公式转换为 Wwise RTPC0~100保持与 Unity 编辑器中一致的感知效果。
/// 转换公式rtpc = (ln(currentFreq) - ln(minFreq)) / (ln(maxFreq) - ln(minFreq)) * 100
///
/// 示例3500 Hz → RTPC ≈ 76与编辑器中 Unity HPF 3500Hz 感知等效)
/// </summary>
public partial class HighPassFilterEffect : EffectBase
{
#region [] Effect Parameters
// peakBM 中存储的目标截止频率Hz语义与 Unity AudioHighPassFilter.cutoffFrequency 一致
public float peak;
public bool useEQ;
public AnimationCurve intensityCurve;
private const string RtpcName = "Song_Highpass";
// 人耳可听范围边界
private const float MinFrequency = 20f; // 对应 RTPC = 0无滤波
private const float MaxFrequency = 20000f; // 对应 RTPC = 100完全截止
// 幂次指数:调节频率到 RTPC 的曲线弯曲程度
// 1.0 = 纯线性(效果太弱)
// 0.5 = 接近对数(效果太强)
// 0.65 = 推荐起点,可根据实际测试微调(向 1.0 调弱,向 0.5 调强)
private const float PowerExponent = 0.25f;
#endregion
#region [] Initialization
public HighPassFilterEffect(float effectTime, float peak, AnimationCurve intensityCurve)
public HighPassFilterEffect(float effectTime, float peak, bool useEQ, AnimationCurve intensityCurve)
: base(effectTime)
{
this.effectTime = 0;
this.effectTime = effectTime;
this.peak = peak;
this.useEQ = useEQ;
this.intensityCurve = intensityCurve;
}
#endregion
#region [] Effect Pattern Overrides
public override void Adjust()
#region [] Hz Wwise RTPC
/// <summary>
/// 将频率值Hz通过幂次曲线映射到 Wwise HPF RTPC0~100
/// 10 Hz → 0无滤波22000 Hz → 100完全截止
/// 调整 PowerExponent 可改变曲线弯曲程度:越小越像对数(强),越接近 1 越线性(弱)。
/// </summary>
private static float FrequencyToRtpc(float frequencyHz)
{
/*MMF_Player effect = LeanPool.Spawn(GameManager.Instance.basePrefabs.highPassFilterEffect).GetComponent<MMF_Player>();
effect.GetFeedbackOfType<MMF_AudioFilterHighPass>().effectTime = effectTime;
effect.GetFeedbackOfType<MMF_AudioFilterHighPass>().RemapHighPassZero = 10;
effect.GetFeedbackOfType<MMF_AudioFilterHighPass>().RemapHighPassOne = peak;
effect.GetFeedbackOfType<MMF_AudioFilterHighPass>().ShakeHighPass = intensityCurve;
effect.PlayFeedbacks();
LeanPool.Despawn(effect.gameObject, effectTime);*/
float clampedFreq = Mathf.Clamp(frequencyHz, MinFrequency, MaxFrequency);
// 先归一化到 0~1线性
float normalized = (clampedFreq - MinFrequency) / (MaxFrequency - MinFrequency);
// 幂次变换compressed = normalized^exponent使曲线在低频段更灵敏
float shaped = Mathf.Pow(normalized, PowerExponent);
return Mathf.Clamp(shaped * 100f, 0f, 100f);
}
#endregion
#region [] Effect Pattern Overrides
public override void PreExecute() { }
public override void Execute()
{
// 在 10Hz无效果与 peak Hz最强截止之间基于曲线插值
float intensity = intensityCurve != null ? intensityCurve.Evaluate(effectProgressPercent) : 0f;
float currentFreq = Mathf.Lerp(MinFrequency, peak, intensity);
// 对数映射到 Wwise RTPC
float rtpcValue = FrequencyToRtpc(currentFreq);
GameManager.Instance.songPlayer.HighPassFilter.SetValue(GameManager.Instance.songPlayer.gameObject, rtpcValue);
}
public override void Adjust() { ResetEffect(); }
public override void Recover() { ResetEffect(); }
public override void Disrupt() { ResetEffect(); }
private void ResetEffect()
{
// RTPC = 0 → 10Hz → 无滤波
GameManager.Instance.songPlayer.HighPassFilter.SetValue(GameManager.Instance.songPlayer.gameObject, 0f);
Debug.Log("HighPassFilterEffect reset: RTPC set to 0 (10Hz, no filter)");
}
#endregion
}
}

View File

@@ -1,42 +1,89 @@
using System.Collections;
using System.Collections.Generic;
using Ichni.RhythmGame.Beatmap;
using Lean.Pool;
using UnityEngine;
namespace Ichni.RhythmGame
{
public class LowPassFilterEffect : EffectBase
/// <summary>
/// 低通滤波效果Wwise RTPC 驱动版)。
/// 在 Wwise 中需要创建名为 "Song_Lowpass" 的 RTPC范围 0~100
/// 并将其连接到 Music Bus 的 Low-Pass Filter 参数Built-in Parameter
///
/// BM 中存储的 bottom 是 Unity AudioLowPassFilter.cutoffFrequencyHz
/// 运行时使用对数公式转换为 Wwise RTPC0~100保持与 Unity 编辑器中一致的感知效果。
/// 转换公式rtpc = (ln(maxFreq) - ln(currentFreq)) / (ln(maxFreq) - ln(minFreq)) * 100
/// </summary>
public partial class LowPassFilterEffect : EffectBase
{
#region [] Effect Parameters
// bottomBM 中存储的目标截止频率Hz语义与 Unity AudioLowPassFilter.cutoffFrequency 一致
public float bottom;
public bool useEQ;
public AnimationCurve intensityCurve;
private const string RtpcName = "Song_Lowpass";
// 人耳可听范围边界
private const float MinFrequency = 20f; // 对应 RTPC = 100完全截止
private const float MaxFrequency = 20000f; // 对应 RTPC = 0无滤波
// 幂次指数:调节频率到 RTPC 的曲线弯曲程度
// 1.0 = 纯线性(效果太弱)
// 0.5 = 接近对数(效果太强)
// 0.65 = 推荐起点,可根据实际测试微调(向 1.0 调弱,向 0.5 调强)
private const float PowerExponent = 0.25f;
#endregion
#region [] Initialization
public LowPassFilterEffect(float effectTime, float bottom, AnimationCurve intensityCurve)
public LowPassFilterEffect(float effectTime, float bottom, bool useEQ, AnimationCurve intensityCurve)
: base(effectTime)
{
this.effectTime = 0;
this.effectTime = effectTime;
this.bottom = bottom;
this.useEQ = useEQ;
this.intensityCurve = intensityCurve;
}
#endregion
#region [] Effect Pattern Overrides
public override void Adjust()
#region [] Hz Wwise RTPC
/// <summary>
/// 将频率值Hz通过幂次曲线映射到 Wwise LPF RTPC0~100
/// 22000 Hz → 0无滤波趋近 10 Hz → 100完全截止
/// 调整 PowerExponent 可改变曲线弯曲程度:越小越像对数(强),越接近 1 越线性(弱)。
/// </summary>
private static float FrequencyToRtpc(float frequencyHz)
{
/*MMF_Player effect = LeanPool.Spawn(GameManager.Instance.basePrefabs.lowPassFilterEffect).GetComponent<MMF_Player>();
effect.GetFeedbackOfType<MMF_AudioFilterLowPass>().effectTime = effectTime;
effect.GetFeedbackOfType<MMF_AudioFilterLowPass>().RemapLowPassZero = 22000;
effect.GetFeedbackOfType<MMF_AudioFilterLowPass>().RemapLowPassOne = bottom;
effect.GetFeedbackOfType<MMF_AudioFilterLowPass>().ShakeLowPass = intensityCurve;
effect.PlayFeedbacks();
LeanPool.Despawn(effect.gameObject, effectTime);*/
float clampedFreq = Mathf.Clamp(frequencyHz, MinFrequency, MaxFrequency);
// 先归一化:频率大 = 房内线性值大(无滤波方向)
float normalized = (clampedFreq - MinFrequency) / (MaxFrequency - MinFrequency);
// 幂次变换,然后反转(频率越低 = RTPC 越高 = 截止越强)
float shaped = Mathf.Pow(normalized, PowerExponent);
return Mathf.Clamp((1f - shaped) * 100f, 0f, 100f);
}
#endregion
#region [] Effect Pattern Overrides
public override void PreExecute() { }
public override void Execute()
{
// 在 22000Hz无效果与 bottom Hz最强截止之间基于曲线插值
float intensity = intensityCurve != null ? intensityCurve.Evaluate(effectProgressPercent) : 0f;
float currentFreq = Mathf.Lerp(MaxFrequency, bottom, intensity);
// 对数映射到 Wwise RTPC
float rtpcValue = FrequencyToRtpc(currentFreq);
GameManager.Instance.songPlayer.LowPassFilter.SetValue(GameManager.Instance.songPlayer.gameObject, rtpcValue);
}
public override void Adjust() { ResetEffect(); }
public override void Recover() { ResetEffect(); }
public override void Disrupt() { ResetEffect(); }
private void ResetEffect()
{
// RTPC = 0 → 22000Hz → 无滤波
GameManager.Instance.songPlayer.LowPassFilter.SetValue(GameManager.Instance.songPlayer.gameObject, 0f);
}
#endregion
}
}