展前冒死重构

This commit is contained in:
2026-07-24 04:43:13 +08:00
parent d09597472b
commit 4eb70b0ac9
61 changed files with 74951 additions and 328 deletions
+70 -15
View File
@@ -35,8 +35,8 @@ public class NoteSpawner : MonoBehaviour
// Documentation text normalized.
[Tooltip("Multiplier applied to visual fall speed. Changing this will automatically adjust spawn timing so notes still arrive at their original beat times.")]
[Range(0.5f, 2f)]
public float speedMultiplier = 1f;
[Range(1f, 3f)]
public float speedMultiplier = 2f;
[Header("Calibration")]
[Tooltip("Tolerance (world units) for snapping middle segments to expected position after spawn.")]
@@ -84,8 +84,9 @@ public class NoteSpawner : MonoBehaviour
public GameObject animations;
// optional: constants for runtime clamping (kept for internal use)
private const float SpeedMultiplierMin = 0.5f;
private const float SpeedMultiplierMax = 2f;
// Player-facing speed is clamped to 1-3; FlowSpeedGlobalMultiplier makes the real range 2-6.
private const float SpeedMultiplierMin = 1f;
private const float SpeedMultiplierMax = 3f;
// 全局流速系数:与用户可调流速(speedMultiplier)相乘的独立系数,加快音符下落。
// 为什么用它而非 bpm 倍率:noteSpeed = 10.75 * sm * bpm / 240,若靠 ×bpm 提速,
// 长条音符的视觉长度由 ApplyVisualScale(sm) 决定(只随 sm 不随 bpm)、且分段数 segmentInterval=(60/bpm)/4
@@ -100,6 +101,28 @@ public class NoteSpawner : MonoBehaviour
get { return Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax) * FlowSpeedGlobalMultiplier; }
}
[Header("Note Fall Speed Source")]
[Tooltip("true=沿用谱面 BPM 决定下落速度(旧逻辑,高 BPM 歌音符更快);false=用固定参考 BPM" +
"下落速度不再随歌曲 BPM 变化,仅由流速 speedMultiplier 控制(全曲统一手感)。默认 false。")]
public bool useBpmForSpeed = false;
[Tooltip("useBpmForSpeed=false 时用作速度基准的固定参考 BPM。等价于“所有歌曲都按此 BPM 的手感下落”。" +
"建议 ≥60:地面粒子速度对 BPM 有 60 下限,低于 60 会使音符与粒子略微不同步。")]
public float referenceBpm = 120f;
// 速度/长条分段公式统一使用的“有效 BPM”。开=真实谱面 bpm;关=固定参考 bpm。
// 二者都同时喂给 noteSpeed 与 segmentInterval,故长条段间距(∝sm,bpm 相消)与地面粒子同步天然保持不变。
public float EffectiveBpm
{
get { return useBpmForSpeed ? bpm : Mathf.Max(1f, referenceBpm); }
}
[Header("Hold Note Body Fill")]
[Tooltip("true=新逻辑(默认):长条中段不拉伸(保持 prefab 原始大小),改为按流速生成更多中段密铺填满(流速越快段越多)。" +
"false=旧逻辑:中段数量固定,用 ApplyVisualScale(流速) 拉伸每段填满(流速越大越拉长)。" +
"两种方案下长条视觉长度一致、判定完全不变(中段纯视觉、end 段到达时间不变)。")]
public bool useMultiSegmentFill = true;
public NoteJudgeConfig judgeConfig; // Documentation text normalized.
private Beatmap beatmap;
private float startTime; // Absolute chart time anchor.
@@ -113,7 +136,9 @@ public class NoteSpawner : MonoBehaviour
private Dictionary<int, int> noteIndexToHoldId = new Dictionary<int, int>();
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
private const float NoteSpeedDefault = 1f;
private const string NoteSpeedDefaultVersionKey = "noteSpeedMultiplierDefaultVersion";
private const int NoteSpeedDefaultVersion = 2;
private const float NoteSpeedDefault = 2f;
private const float BaseTravelDistance = 10.75f;
private Coroutine spawnCoroutine;
@@ -146,10 +171,22 @@ public class NoteSpawner : MonoBehaviour
{
if (PlayerPrefs.HasKey(NoteSpeedPrefKey))
{
int version = PlayerPrefs.GetInt(NoteSpeedDefaultVersionKey, 0);
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
if (version < NoteSpeedDefaultVersion && Mathf.Approximately(saved, 3f))
{
saved = NoteSpeedDefault;
}
saved = Mathf.Clamp(saved, SpeedMultiplierMin, SpeedMultiplierMax);
PlayerPrefs.SetFloat(NoteSpeedPrefKey, saved);
PlayerPrefs.SetInt(NoteSpeedDefaultVersionKey, NoteSpeedDefaultVersion);
PlayerPrefs.Save();
return;
}
PlayerPrefs.SetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
PlayerPrefs.SetInt(NoteSpeedDefaultVersionKey, NoteSpeedDefaultVersion);
PlayerPrefs.Save();
}
@@ -280,10 +317,13 @@ public class NoteSpawner : MonoBehaviour
// Cache parameters that don't change within the loop
float sm = EffectiveSpeedMultiplier;
float baseTravelTime = (60f / bpm) * 4f;
// 下落速度基准:useBpmForSpeed=true 用谱面 bpm(旧逻辑)false 用固定参考 bpm(速度不随歌曲变)。
// 判定时间不受影响:到达时刻只由 note.time 决定,speed 仅改视觉下落快慢与出现时机。
float speedBpm = EffectiveBpm;
float baseTravelTime = (60f / speedBpm) * 4f;
float baseNoteTravelTime = baseTravelTime / Mathf.Max(0.0001f, sm);
float noteSpeed = CalculateSpeed(baseNoteTravelTime);
float segmentInterval = (60f / bpm) / 4f;
float segmentInterval = (60f / speedBpm) / 4f;
EnsureJudgmentLine();
float[] laneTravelTimes = BuildLaneTravelTimes(noteSpeed);
@@ -453,9 +493,24 @@ public class NoteSpawner : MonoBehaviour
return -1;
}
int segmentCount = Mathf.CeilToInt(noteData.length / segmentInterval);
// 段间隔决定"每隔多少时间放一段"。段间距(世界坐标)= noteSpeed × 段间隔,而 noteSpeed ∝ 流速 sm。
// 旧逻辑(useMultiSegmentFill=false):段间隔不随 sm 变 → 段间距 ∝ sm,段数固定,
// 靠 ApplyVisualScale(sm) 把每段拉伸 sm 倍去填满变大的段间距(流速越大越拉长)。
// 新逻辑(useMultiSegmentFill=true):段间隔 ÷ sm → 段数 ∝ sm,而段间距 = noteSpeed × (段间隔/sm) = 恒定,
// 恰好等于不拉伸(scale=1)时 prefab 的原始段高,于是用等大的原始段密铺填满(流速越大段越多)。
// 两种方案下:end 段到达时间 = baseHit + length 完全不变(段数变但 actualSegmentInterval=length/count 抵消)
// 长条整体视觉长度一致;中段纯视觉、不参与计分/连击,故判定业务逻辑完全不受影响。
float effectiveSegmentInterval = segmentInterval;
if (useMultiSegmentFill)
{
effectiveSegmentInterval = segmentInterval / Mathf.Max(0.01f, sm);
}
int segmentCount = Mathf.CeilToInt(noteData.length / effectiveSegmentInterval);
if (segmentCount < 2) segmentCount = 2; // Force at least one middle segment between start and end.
float actualSegmentInterval = segmentCount > 0 ? noteData.length / segmentCount : segmentInterval;
float actualSegmentInterval = segmentCount > 0 ? noteData.length / segmentCount : effectiveSegmentInterval;
// 新逻辑下不拉伸(每段保持 prefab 原始大小);旧逻辑下按流速 sm 拉伸每段。
float holdVisualScale = useMultiSegmentFill ? 1f : sm;
Transform spawnPoint = spawnPoints[noteData.trackIndex];
// Documentation text normalized.
@@ -501,8 +556,8 @@ public class NoteSpawner : MonoBehaviour
// pass realtime hit time (startTime + note.time) and delay 0, include globalHitDelay
holdNote.Setup(holdNoteId, noteData.trackIndex, noteSpeed, baseHit, 0f, false, scheduledEndTime, noteData.color, key, "start", travelTime, judgeConfig, noteData, isSync);
// 使用流速倍率 sm 直接进行缩放,确保长条音符在 0.5-2.0 范围内依然能完美衔接
float visualScale = sm;
// 旧逻辑=按流速 sm 拉伸;新逻辑(多段填充)=不拉伸(holdVisualScale=1)。
float visualScale = holdVisualScale;
holdNote.ApplyVisualScale(visualScale);
// inform hold note of visual speed so it can adapt judgement windows if needed
holdNote.visualSpeedMultiplier = sm;
@@ -533,8 +588,8 @@ public class NoteSpawner : MonoBehaviour
// Documentation text normalized.
holdSeg.Setup(holdNoteId, noteData.trackIndex, noteSpeed, baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", travelTime, judgeConfig, noteData, false);
// 应用与开始段一致的流速缩放
float visualScaleMid = sm;
// 与开始段一致:旧逻辑拉伸、新逻辑不拉伸(靠更多段密铺填满)。
float visualScaleMid = holdVisualScale;
holdSeg.ApplyVisualScale(visualScaleMid);
holdSeg.visualSpeedMultiplier = sm;
@@ -565,8 +620,8 @@ public class NoteSpawner : MonoBehaviour
{
holdEnd.Setup(holdNoteId, noteData.trackIndex, noteSpeed, baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", travelTime, judgeConfig, noteData, false);
// 同样应用流速缩放
float visualScaleEnd = sm;
// 与开始段一致:旧逻辑拉伸、新逻辑不拉伸。
float visualScaleEnd = holdVisualScale;
holdEnd.ApplyVisualScale(visualScaleEnd);
holdEnd.visualSpeedMultiplier = sm;