备份,准备做双端
This commit is contained in:
@@ -226,7 +226,25 @@ public class GameManager : MonoBehaviour
|
||||
private void BeginGameplayClock()
|
||||
{
|
||||
GameplayClock.Reset();
|
||||
GameplayClock.StartChart(0f);
|
||||
|
||||
// 练习模式:若开启,从指定的起始时间启动时钟(供段落练习/A-B 循环)
|
||||
float startTime = PracticeMode.Enabled ? PracticeMode.StartTimeSeconds : 0f;
|
||||
GameplayClock.StartChart(startTime);
|
||||
|
||||
if (PracticeMode.Enabled && startTime > 0f)
|
||||
{
|
||||
Debug.Log($"[GameManager] Practice mode: starting chart at {startTime:F2}s");
|
||||
}
|
||||
}
|
||||
|
||||
// 玩家级音频偏移(补偿音频输出延迟,如蓝牙耳机)。存 PlayerPrefs,单位秒,通常为负值(让音乐更早输出)。
|
||||
// 与谱面自带的 globalDelaySeconds(谱师校准音频开头)彼此独立、相加:谱面偏移随谱走,玩家偏移随设备走。
|
||||
// 只移动音乐播放时刻,绝不改音符视觉/判定时刻 —— 这是补偿音频延迟唯一不污染判定的手段(参照成熟音游 UO/global audio offset)。
|
||||
public const string AudioOffsetPrefsKey = "UserAudioOffsetSeconds";
|
||||
|
||||
public static float GetUserAudioOffsetSeconds()
|
||||
{
|
||||
return PlayerPrefs.GetFloat(AudioOffsetPrefsKey, 0f);
|
||||
}
|
||||
|
||||
// Public helper to reliably unmute and start music playback, respecting delay.
|
||||
@@ -235,6 +253,9 @@ public class GameManager : MonoBehaviour
|
||||
// reset flag until actual play occurs
|
||||
PlaybackStarted = false;
|
||||
|
||||
// 叠加玩家音频偏移。放在方法入口,保证所有调用点(谱面 globalDelaySeconds 各路径)统一生效、不遗漏。
|
||||
delaySeconds += GetUserAudioOffsetSeconds();
|
||||
|
||||
// Stop any previous delay coroutine
|
||||
if (playbackDelayCoroutine != null)
|
||||
{
|
||||
@@ -243,15 +264,26 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
|
||||
if (musicSource == null) return;
|
||||
|
||||
|
||||
// Documentation text normalized.
|
||||
try
|
||||
{
|
||||
musicSource.Stop();
|
||||
musicSource.time = 0f;
|
||||
|
||||
// 练习模式:若从中间起播,提前将音乐 seek 到起始位置
|
||||
if (PracticeMode.Enabled)
|
||||
{
|
||||
float practiceStart = PracticeMode.StartTimeSeconds;
|
||||
if (practiceStart > 0f)
|
||||
{
|
||||
musicSource.time = Mathf.Clamp(practiceStart, 0f, musicSource.clip != null ? musicSource.clip.length : 999f);
|
||||
Debug.Log($"[GameManager] Practice mode: music seeked to {musicSource.time:F2}s");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// Documentation text normalized.
|
||||
@@ -266,29 +298,49 @@ public class GameManager : MonoBehaviour
|
||||
if (delaySeconds < 0f)
|
||||
startTime = -delaySeconds;
|
||||
|
||||
float clampedStartTime = ClampAudioStartTime(startTime);
|
||||
|
||||
// Use PlayScheduled to precisely anchor music start to a future dspTime.
|
||||
// This eliminates the per-frame jitter from DelayAndPlayMusic's unscaledDeltaTime accumulation.
|
||||
if (delaySeconds > 0f)
|
||||
{
|
||||
playbackDelayCoroutine = StartCoroutine(DelayAndPlayMusic(delaySeconds));
|
||||
// Schedule music to start at dspTime = now + delaySeconds
|
||||
double scheduledDsp = AudioSettings.dspTime + delaySeconds;
|
||||
musicSource.time = clampedStartTime;
|
||||
musicSource.PlayScheduled(scheduledDsp);
|
||||
PlaybackStarted = true;
|
||||
|
||||
// Anchor game clock to this scheduled start so note timing matches actual audio output
|
||||
GameplayClock.SetMusicAnchor(scheduledDsp - clampedStartTime);
|
||||
|
||||
Debug.Log($"GameManager.PlayMusicWithDelay: Music scheduled at dsp={scheduledDsp:F3} (delay={delaySeconds:F3}s, startTime={clampedStartTime:F3})");
|
||||
}
|
||||
else
|
||||
{
|
||||
float clampedStartTime = ClampAudioStartTime(startTime);
|
||||
// Play immediately
|
||||
musicSource.time = clampedStartTime;
|
||||
double playDsp = AudioSettings.dspTime;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
Debug.Log("GameManager.PlayMusicWithDelay: Music playback started immediately (no delay)");
|
||||
|
||||
// Anchor: music started at playDsp, playing from clampedStartTime
|
||||
GameplayClock.SetMusicAnchor(playDsp - clampedStartTime);
|
||||
|
||||
Debug.Log($"GameManager.PlayMusicWithDelay: Music started immediately at dsp={playDsp:F3}, musicTime={clampedStartTime:F3}");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"PlayMusicWithDelay failed: {ex}");
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
float clampedStartTime = ClampAudioStartTime(delaySeconds < 0f ? -delaySeconds : 0f);
|
||||
musicSource.time = clampedStartTime;
|
||||
double playDsp = AudioSettings.dspTime;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
}
|
||||
GameplayClock.SetMusicAnchor(playDsp - clampedStartTime);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
@@ -540,6 +592,23 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 练习模式:检查是否到达终点,需要结束或循环
|
||||
if (PlaybackStarted && PracticeMode.Enabled && PracticeMode.ShouldEndOrLoop(GameplayClock.NowSongTime))
|
||||
{
|
||||
if (PracticeMode.LoopEnabled)
|
||||
{
|
||||
Debug.Log("[GameManager] Practice mode: looping back to start");
|
||||
RestartPracticeLoop();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[GameManager] Practice mode: reached end, stopping");
|
||||
// 可选:跳转结算或停止播放。当前仅停止,供后续接入
|
||||
if (musicSource != null) musicSource.Stop();
|
||||
PlaybackStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect Space or yellow note key to request start (only before gameplay starts)
|
||||
if (!startRequested)
|
||||
{
|
||||
@@ -557,6 +626,32 @@ public class GameManager : MonoBehaviour
|
||||
// Once startRequested is true, Escape is disabled and gameplay begins countdown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 练习模式 A-B 循环:重新加载场景并从起始位置继续。
|
||||
/// 供后续快速接入(当前仅重置时钟+音乐,未重新生成音符——需配合 NoteSpawner 重启)。
|
||||
/// </summary>
|
||||
private void RestartPracticeLoop()
|
||||
{
|
||||
// 简单实现:重置时钟和音乐到起点。完整实现需清理已生成音符、重新加载谱面。
|
||||
// 当前作为脚手架,供你后续接入完整的场景重启或音符池清理逻辑。
|
||||
PlaybackStarted = false;
|
||||
|
||||
if (musicSource != null)
|
||||
{
|
||||
musicSource.Stop();
|
||||
}
|
||||
|
||||
BeginGameplayClock();
|
||||
|
||||
// 重新播放音乐(带练习模式起点 seek)
|
||||
// BeatmapManager.globalDelaySeconds 是 public 字段,直接读取
|
||||
float globalDelay = beatmapManager != null ? beatmapManager.globalDelaySeconds : 0f;
|
||||
PlayMusicWithDelay(globalDelay);
|
||||
|
||||
// TODO: 清理已生成的音符、重启 NoteSpawner.LoadBeatmap(beatmap) 以重新生成从起点开始的音符。
|
||||
// 当前脚手架未实现完整的音符清理,仅提供时钟+音乐循环框架。
|
||||
}
|
||||
|
||||
private bool IsYellowStartKeyDown()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Space)) return true;
|
||||
|
||||
Reference in New Issue
Block a user