备份,准备做双端

This commit is contained in:
2026-07-30 23:15:58 +08:00
parent add675e45d
commit ec4ec53545
88 changed files with 4050 additions and 872 deletions
+104 -9
View File
@@ -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;