修复短音符延迟

This commit is contained in:
FloatGaming
2026-01-15 02:46:42 +08:00
parent 271a576289
commit f3ece026ca
10 changed files with 666 additions and 105 deletions
+125 -30
View File
@@ -37,6 +37,11 @@ public class NoteSpawner : MonoBehaviour
[Tooltip("Interval (seconds realtime) between calibration checks.")]
public float calibrateInterval = 0.01f;
// 控制是否启用“追赶偏移量(y-offset compensation)”计算,默认关闭
[Header("Early compensation (experimental)")]
[Tooltip("When enabled, spawn positioning will compensate each segment's position based on its own activation time so newly spawned segments appear at their expected traveled position. Default: OFF.")]
public bool enableYOffsetCompensation = false;
// optional: constants for runtime clamping (kept for internal use)
private const float SpeedMultiplierMin = 0.8f;
private const float SpeedMultiplierMax = 1.25f;
@@ -72,6 +77,15 @@ public class NoteSpawner : MonoBehaviour
return;
}
// initialize JudgeManager total note count (count each NoteData as one logical note;
// long notes are counted once as a single logical note)
int total = 0;
if (beatmap.notes != null)
{
total = beatmap.notes.Length;
}
JudgeManager.Instance?.SetTotalNotes(total);
bpm = beatmap.bpm;
startTime = Time.time;
Debug.Log($"歌曲开始时间: {startTime}");
@@ -150,25 +164,52 @@ public class NoteSpawner : MonoBehaviour
return;
}
note.transform.position = spawnPoints[noteData.trackIndex].position;
Transform spawnPoint = spawnPoints[noteData.trackIndex];
// Calculate timing parameters
float smLocal = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax);
float baseTravelTime = (60f / bpm) * 4f;
float effectiveTravelTime = baseTravelTime / Mathf.Max(0.0001f, smLocal);
float noteSpeed = CalculateSpeed(effectiveTravelTime);
// Calculate hit time (realtime when note should be judged)
float rawHit = startTime + noteData.time + globalHitDelay;
float realtimeHit = Mathf.Max(0f, rawHit);
// Calculate activation time (when note should start moving)
float activationTime = realtimeHit - effectiveTravelTime;
// Compute early compensation for short notes if enabled
float noteYOffset = 0f;
if (enableYOffsetCompensation)
{
float timeSinceNoteActivation = Time.time - activationTime;
noteYOffset = Mathf.Max(0f, timeSinceNoteActivation * noteSpeed);
}
// Set initial position with offset compensation
Vector3 initialPosition = spawnPoint.position + Vector3.down * noteYOffset;
note.transform.position = initialPosition;
note.transform.rotation = Quaternion.identity;
Note noteScript = note.GetComponent<Note>();
NoteController noteController = note.GetComponent<NoteController>();
if (noteScript != null)
{
float noteSpawnTime = Time.time;
// pass realtime hit time to Note.Setup, include globalHitDelay
float rawHit = startTime + noteData.time + globalHitDelay;
float realtimeHit = Mathf.Max(0f, rawHit); // clamp to non-negative
// clamp multiplier locally and calculate travel time and speed consistent with speedMultiplier
float smLocal = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax);
float baseTravelTime = (60f / bpm) * 4f;
float effectiveTravelTime = baseTravelTime / Mathf.Max(0.0001f, smLocal);
float speed = CalculateSpeed(effectiveTravelTime);
noteScript.Setup(key, noteData.trackIndex, speed, realtimeHit, noteData.color, judgeConfig, noteData);
if (GameConfig.verboseLogs) Debug.Log($"到达时间:{noteSpawnTime + baseTravelTime}");
if (GameConfig.verboseLogs) Debug.Log($"生成短音符: color={noteData.color}, realtimeHit={realtimeHit:F3}, activationTime={activationTime:F3}, yOffset={noteYOffset:F4}");
// Setup note script with timing parameters
noteScript.Setup(key, noteData.trackIndex, noteSpeed, realtimeHit, noteData.color, judgeConfig, noteData);
// Configure controller for absolute positioning (replaces relative Translate)
if (noteController != null)
{
noteController.ConfigureAbsolutePositioning(initialPosition, realtimeHit, effectiveTravelTime, noteYOffset);
}
// Schedule calibration for short note to correct any drift
StartCoroutine(CalibrateNoteAfterSpawn(noteScript.GetComponent<NoteController>(), initialPosition));
}
else
{
@@ -221,6 +262,10 @@ public class NoteSpawner : MonoBehaviour
float effectiveTravelTimeHold = baseTravelTimeHold / Mathf.Max(0.0001f, smLocalHold);
float holdSpeed = CalculateSpeed(effectiveTravelTimeHold);
// base realtime for hits
float rawBase = startTime + noteData.time + globalHitDelay;
float baseHit = Mathf.Max(0f, rawBase);
// 生成 Start 段
GameObject startObj = notePool.GetStartNote(noteData.color);
if (startObj == null)
@@ -228,16 +273,24 @@ public class NoteSpawner : MonoBehaviour
Debug.LogError("对象池返回空 hold note start");
return;
}
startObj.transform.position = spawnPoint.position;
// compute per-segment compensation only if enabled
float startYOffset = 0f;
if (enableYOffsetCompensation)
{
float startActivation = baseHit - effectiveTravelTimeHold;
float timeSinceActivation = Time.time - startActivation;
startYOffset = Mathf.Max(0f, timeSinceActivation * holdSpeed);
}
startObj.transform.position = spawnPoint.position + Vector3.down * startYOffset;
startObj.transform.rotation = Quaternion.identity;
HoldNote holdNote = startObj.GetComponent<HoldNote>();
if (holdNote != null)
{
// pass realtime hit time (startTime + note.time) and delay 0, include globalHitDelay
float rawStartHit = startTime + noteData.time + globalHitDelay;
float startHit = Mathf.Max(0f, rawStartHit);
holdNote.Setup(holdNoteId, noteData.trackIndex, holdSpeed, startHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig, noteData);
holdNote.Setup(holdNoteId, noteData.trackIndex, holdSpeed, baseHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig, noteData);
// compute visual scale for hold segments so pieces visually connect across speedMultiplier changes
float visualScale = 0.9f * smLocalHold + 0.1f; // linear fit: f(1)=1, f(2)=1.9
@@ -264,15 +317,22 @@ public class NoteSpawner : MonoBehaviour
continue;
}
segObj.transform.position = spawnPoint.position;
float segYOffset = 0f;
if (enableYOffsetCompensation)
{
float segHit = baseHit + segmentDelay;
float segActivation = segHit - effectiveTravelTimeHold;
float timeSinceActivationSeg = Time.time - segActivation;
segYOffset = Mathf.Max(0f, timeSinceActivationSeg * holdSpeed);
}
segObj.transform.position = spawnPoint.position + Vector3.down * segYOffset;
segObj.transform.rotation = Quaternion.identity;
HoldNote holdSeg = segObj.GetComponent<HoldNote>();
if (holdSeg != null)
{
// 中段都标记为 middlepass realtime base time and segmentDelay
float rawBase = startTime + noteData.time + globalHitDelay;
float baseHit = Mathf.Max(0f, rawBase);
holdSeg.Setup(holdNoteId, noteData.trackIndex, holdSpeed, baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig, noteData);
// apply same visual scale so middle pieces visually connect
@@ -281,8 +341,9 @@ public class NoteSpawner : MonoBehaviour
holdSeg.visualSpeedMultiplier = smLocalHold;
// Immediately calibrate position and schedule additional checks to correct any offset
holdSeg.CalibratePosition(spawnPoint.position, calibrateTolerance);
StartCoroutine(CalibrateAfterSpawn(holdSeg, spawnPoint.position));
Vector3 calibSpawnPos = spawnPoint.position + Vector3.down * segYOffset;
holdSeg.CalibratePosition(calibSpawnPos, calibrateTolerance);
StartCoroutine(CalibrateAfterSpawn(holdSeg, calibSpawnPos));
}
else
{
@@ -297,7 +358,19 @@ public class NoteSpawner : MonoBehaviour
Debug.LogError("对象池返回空 hold note 片段(end)!");
return;
}
endObj.transform.position = spawnPoint.position;
float endDelay = segmentCount * actualSegmentInterval; // 不按 speedMultiplier 缩放,使用谱面长度保证尾段紧随中段
float endYOffset = 0f;
if (enableYOffsetCompensation)
{
float endHit = baseHit + endDelay;
float endActivation = endHit - effectiveTravelTimeHold;
float timeSinceActivationEnd = Time.time - endActivation;
endYOffset = Mathf.Max(0f, timeSinceActivationEnd * holdSpeed);
}
endObj.transform.position = spawnPoint.position + Vector3.down * endYOffset;
endObj.transform.rotation = Quaternion.identity;
// 为便于识别,将实例名追加后缀
@@ -308,10 +381,6 @@ public class NoteSpawner : MonoBehaviour
HoldNote holdEnd = endObj.GetComponent<HoldNote>();
if (holdEnd != null)
{
// 将 delay 设置为整个 hold 长度 (或 segmentCount * actualSegmentInterval),确保尾部在最后中段之后
float endDelay = segmentCount * actualSegmentInterval; // 不按 speedMultiplier 缩放,使用谱面长度保证尾段紧随中段
float rawBase = startTime + noteData.time + globalHitDelay;
float baseHit = Mathf.Max(0f, rawBase);
holdEnd.Setup(holdNoteId, noteData.trackIndex, holdSpeed, baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig, noteData);
// apply visual scale to end piece as well
@@ -320,8 +389,9 @@ public class NoteSpawner : MonoBehaviour
holdEnd.visualSpeedMultiplier = smLocalHold;
// schedule calibration for end as well to be safe
holdEnd.CalibratePosition(spawnPoint.position, calibrateTolerance);
StartCoroutine(CalibrateAfterSpawn(holdEnd, spawnPoint.position));
Vector3 calibEndPos = spawnPoint.position + Vector3.down * endYOffset;
holdEnd.CalibratePosition(calibEndPos, calibrateTolerance);
StartCoroutine(CalibrateAfterSpawn(holdEnd, calibEndPos));
}
else
{
@@ -340,6 +410,31 @@ public class NoteSpawner : MonoBehaviour
}
}
/// <summary>
/// 校验短音符的位置,防止其因计时偏差而位移
/// </summary>
private IEnumerator CalibrateNoteAfterSpawn(NoteController noteController, Vector3 expectedSpawnPos)
{
if (noteController == null) yield break;
GameObject noteObj = noteController.gameObject;
for (int i = 0; i < Mathf.Max(1, calibrateChecks); i++)
{
yield return new WaitForSecondsRealtime(calibrateInterval);
if (noteObj == null || !noteObj.activeSelf) yield break;
// Check if note position deviates significantly from expected spawn position
float distanceDeviation = Vector3.Distance(noteObj.transform.position, expectedSpawnPos);
if (distanceDeviation > calibrateTolerance)
{
// Snap to expected position while allowing vertical movement
Vector3 correctedPos = expectedSpawnPos;
noteObj.transform.position = correctedPos;
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Calibrated short note position, deviation was {distanceDeviation:F4}");
}
}
}
private float CalculateSpeed(float noteTravelTime)
{
return 10.75f / noteTravelTime;