186 lines
6.7 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|