修复短音符延迟
This commit is contained in:
@@ -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)
|
||||
{
|
||||
// 中段都标记为 middle,pass 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;
|
||||
|
||||
Reference in New Issue
Block a user