Files
2026-07-30 23:15:58 +08:00

186 lines
6.7 KiB
C#

using UnityEngine;
public static class GameplayClock
{
private static bool initialized;
private static bool paused;
private static double chartStartDspTime;
private static double pauseStartDspTime;
private static double accumulatedPauseSeconds;
private static float chartStartSongTime;
private static double scheduledMusicStartDsp; // anchor: when music actually starts playing
// Seconds the chart clock leads the music clip position by (== globalDelaySeconds + userAudioOffset).
// Captured when music is scheduled; used to preserve the songTime = musicTime + offset relationship
// when re-anchoring to actual music progress after a pause.
private static double musicToChartOffsetSeconds;
// Per-frame cache of NowSongTime. AudioSettings.dspTime only advances on the audio
// thread (~once per audio buffer), so it is effectively constant within a rendered
// frame. Many notes/segments read NowSongTime every frame; caching by frame turns
// hundreds of native dspTime reads per frame into one. Invalidated when the frame
// changes or clock state mutates (Reset/StartChart/Pause/Resume).
private static int nowSongTimeFrame = -1;
private static float nowSongTimeCached;
public static bool IsInitialized => initialized;
public static bool IsPaused => paused;
public static float ChartStartSongTime => chartStartSongTime;
public static double ChartStartDspTime => chartStartDspTime;
public static double ScheduledMusicStartDsp => scheduledMusicStartDsp;
public static void Reset()
{
initialized = false;
paused = false;
chartStartDspTime = 0d;
pauseStartDspTime = 0d;
accumulatedPauseSeconds = 0d;
chartStartSongTime = 0f;
scheduledMusicStartDsp = 0d;
musicToChartOffsetSeconds = 0d;
nowSongTimeFrame = -1;
}
public static void StartChart(float initialSongTimeSeconds = 0f)
{
initialized = true;
paused = false;
chartStartDspTime = AudioSettings.dspTime;
pauseStartDspTime = 0d;
accumulatedPauseSeconds = 0d;
chartStartSongTime = Mathf.Max(0f, initialSongTimeSeconds);
scheduledMusicStartDsp = 0d; // will be set by SetMusicAnchor when music plays
musicToChartOffsetSeconds = 0d;
nowSongTimeFrame = -1;
}
/// <summary>
/// Record the scheduled music start time for reference.
/// Call this immediately after PlayScheduled or Play.
/// NOTE: Does NOT re-anchor chartStartDspTime — the chart clock stays pinned to gameplay start (StartChart),
/// and music simply plays offset into the future. Re-anchoring would shift all note timing by globalDelaySeconds.
/// </summary>
public static void SetMusicAnchor(double musicStartDspTime)
{
if (!initialized) return;
scheduledMusicStartDsp = musicStartDspTime;
// Do NOT re-anchor chartStartDspTime here — note spawn/hit times were computed from the original anchor.
// Re-anchoring would create a systematic offset equal to globalDelaySeconds.
// Capture the constant lead of chart song-time over music-clip time: songTime(D) - musicTime(D),
// where musicTime(D) = D - musicStartDspTime. This lets ReanchorToMusicTime preserve the relationship.
musicToChartOffsetSeconds = chartStartSongTime - chartStartDspTime - accumulatedPauseSeconds + musicStartDspTime;
nowSongTimeFrame = -1;
}
public static void Pause()
{
if (!initialized || paused)
{
return;
}
paused = true;
pauseStartDspTime = AudioSettings.dspTime;
nowSongTimeFrame = -1;
}
public static void Resume()
{
if (!initialized || !paused)
{
return;
}
accumulatedPauseSeconds += AudioSettings.dspTime - pauseStartDspTime;
pauseStartDspTime = 0d;
paused = false;
nowSongTimeFrame = -1;
}
/// <summary>
/// Re-anchor the clock to the actual music progress on resume.
/// Call this after Resume() if musicSource.time is available, to correct any drift.
/// </summary>
public static void ReanchorToMusicTime(float actualMusicTime)
{
if (!initialized || paused) return;
// Reverse-calculate chartStartDspTime so that song-time now equals the music clip position
// PLUS the constant lead (globalDelaySeconds/audio offset) captured at SetMusicAnchor.
// Without adding musicToChartOffsetSeconds, resuming would force songTime == musicTime and
// reintroduce the exact backward offset the SetMusicAnchor fix removed.
double now = AudioSettings.dspTime;
double targetSongTime = actualMusicTime + musicToChartOffsetSeconds;
chartStartDspTime = now - accumulatedPauseSeconds - (targetSongTime - chartStartSongTime);
nowSongTimeFrame = -1;
}
public static float NowSongTime
{
get
{
if (!initialized)
{
return 0f;
}
// While paused the value is frozen (pauseStartDspTime), so it's trivially stable;
// while playing, reuse the value already computed this frame.
int frame = Time.frameCount;
if (frame == nowSongTimeFrame)
{
return nowSongTimeCached;
}
double currentDsp = paused ? pauseStartDspTime : AudioSettings.dspTime;
double elapsed = currentDsp - chartStartDspTime - accumulatedPauseSeconds;
float result = chartStartSongTime + Mathf.Max(0f, (float)elapsed);
nowSongTimeFrame = frame;
nowSongTimeCached = result;
return result;
}
}
public static float SongTimeFromDsp(double dspTime)
{
if (!initialized)
{
return chartStartSongTime;
}
double effectivePause = accumulatedPauseSeconds;
if (paused && dspTime > pauseStartDspTime)
{
dspTime = pauseStartDspTime;
}
double elapsed = dspTime - chartStartDspTime - effectivePause;
return chartStartSongTime + Mathf.Max(0f, (float)elapsed);
}
public static float ToAbsoluteChartTime(float songTimelineTimeSeconds)
{
return chartStartSongTime + songTimelineTimeSeconds;
}
public static System.Collections.IEnumerator WaitForSeconds(float seconds)
{
if (seconds <= 0f)
{
yield break;
}
if (!initialized)
{
yield return new UnityEngine.WaitForSeconds(seconds);
yield break;
}
float targetTime = NowSongTime + seconds;
while (NowSongTime < targetTime)
{
yield return null;
}
}
}