using System; using System.Collections.Generic; using UnityEngine; /// /// Replay 录制/回放系统:记录输入流(trackIndex + dspTime)并可重放。 /// 供后续快速接入(需配合 InputManager 和 autoplay 逻辑连接)。 /// [Serializable] public class ReplayData { [Serializable] public class InputEvent { public int trackIndex; public double dspTime; public bool isPress; // true=按下, false=松开 public InputEvent(int trackIndex, double dspTime, bool isPress) { this.trackIndex = trackIndex; this.dspTime = dspTime; this.isPress = isPress; } } public string songId; public string difficulty; public int totalScore; public float accuracy; public int maxCombo; public string recordedAt; // ISO8601 时间戳 public List inputEvents = new List(); // 元数据:录制时的游戏版本、offset 等(供回放时校验兼容性) public float recordedSpawnOffset; public float recordedVisualOffset; public string gameVersion; public ReplayData() { recordedAt = DateTime.UtcNow.ToString("o"); } public string ToJson() { return JsonUtility.ToJson(this, prettyPrint: true); } public static ReplayData FromJson(string json) { return JsonUtility.FromJson(json); } } public static class ReplayRecorder { private static ReplayData currentReplay = null; private static bool isRecording = false; /// /// 开始录制。通常在 GameManager 启动游玩时调用。 /// public static void StartRecording(string songId, string difficulty) { if (isRecording) { Debug.LogWarning("[ReplayRecorder] Already recording, stop first."); return; } currentReplay = new ReplayData { songId = songId, difficulty = difficulty, recordedSpawnOffset = PlayerPrefs.GetFloat("UserGlobalDelaySeconds", 0f), recordedVisualOffset = PlayerPrefs.GetFloat("UserVisualOffsetSeconds", 0f), gameVersion = Application.version }; isRecording = true; Debug.Log($"[ReplayRecorder] Started recording: {songId} ({difficulty})"); } /// /// 停止录制并返回录制数据。通常在结算时调用。 /// public static ReplayData StopRecording(int totalScore, float accuracy, int maxCombo) { if (!isRecording || currentReplay == null) { Debug.LogWarning("[ReplayRecorder] Not recording."); return null; } currentReplay.totalScore = totalScore; currentReplay.accuracy = accuracy; currentReplay.maxCombo = maxCombo; isRecording = false; Debug.Log($"[ReplayRecorder] Stopped. Recorded {currentReplay.inputEvents.Count} input events."); ReplayData result = currentReplay; currentReplay = null; return result; } /// /// 记录一次输入事件。在 InputManager.OnTrackPressedWithDspTime / OnTrackReleasedWithDspTime 中调用。 /// public static void RecordInput(int trackIndex, double dspTime, bool isPress) { if (!isRecording || currentReplay == null) return; currentReplay.inputEvents.Add(new ReplayData.InputEvent(trackIndex, dspTime, isPress)); } /// /// 取消当前录制。 /// public static void CancelRecording() { if (isRecording) { Debug.Log("[ReplayRecorder] Recording cancelled."); } isRecording = false; currentReplay = null; } public static bool IsRecording => isRecording; } public class ReplayPlayer : MonoBehaviour { private ReplayData replayData; private int nextEventIndex = 0; private bool isPlaying = false; private double replayStartDsp; /// /// 开始回放。在 GameManager 启动游玩前调用(需先设置 autoplay 模式)。 /// public void StartPlayback(ReplayData replay) { if (replay == null || replay.inputEvents == null || replay.inputEvents.Count == 0) { Debug.LogError("[ReplayPlayer] Invalid replay data."); return; } replayData = replay; nextEventIndex = 0; isPlaying = true; replayStartDsp = AudioSettings.dspTime; Debug.Log($"[ReplayPlayer] Started playback: {replay.inputEvents.Count} events"); } /// /// 停止回放。 /// public void StopPlayback() { isPlaying = false; replayData = null; nextEventIndex = 0; Debug.Log("[ReplayPlayer] Playback stopped."); } private void Update() { if (!isPlaying || replayData == null) return; double currentDsp = AudioSettings.dspTime; double elapsedDsp = currentDsp - replayStartDsp; // 按时间顺序触发录制的输入事件 while (nextEventIndex < replayData.inputEvents.Count) { var evt = replayData.inputEvents[nextEventIndex]; if (evt.dspTime > elapsedDsp) break; // 还没到时间 // 触发输入事件(需接入 InputManager 或直接调用 Note 判定逻辑) TriggerReplayInput(evt.trackIndex, evt.isPress, currentDsp); nextEventIndex++; } // 所有事件已回放完成 if (nextEventIndex >= replayData.inputEvents.Count) { Debug.Log("[ReplayPlayer] Playback finished."); isPlaying = false; } } /// /// 触发回放输入。需接入 InputManager 的事件系统或直接调用判定逻辑。 /// 当前为占位实现,供后续快速接入。 /// private void TriggerReplayInput(int trackIndex, bool isPress, double dspTime) { // TODO: 接入 InputManager 或 JudgeManager // 示例:InputManager.SimulateInput(trackIndex, dspTime, isPress); // 或:直接触发 Note.HandlePress / HoldNote.HandleRelease 等 if (JudgeManager.IsDebugEnabled) { Debug.Log($"[ReplayPlayer] Trigger: track={trackIndex} press={isPress} dsp={dspTime:F3}"); } } public bool IsPlaying => isPlaying; }