216 lines
6.4 KiB
C#
216 lines
6.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Replay 录制/回放系统:记录输入流(trackIndex + dspTime)并可重放。
|
|
/// 供后续快速接入(需配合 InputManager 和 autoplay 逻辑连接)。
|
|
/// </summary>
|
|
[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<InputEvent> inputEvents = new List<InputEvent>();
|
|
|
|
// 元数据:录制时的游戏版本、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<ReplayData>(json);
|
|
}
|
|
}
|
|
|
|
public static class ReplayRecorder
|
|
{
|
|
private static ReplayData currentReplay = null;
|
|
private static bool isRecording = false;
|
|
|
|
/// <summary>
|
|
/// 开始录制。通常在 GameManager 启动游玩时调用。
|
|
/// </summary>
|
|
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})");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 停止录制并返回录制数据。通常在结算时调用。
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 记录一次输入事件。在 InputManager.OnTrackPressedWithDspTime / OnTrackReleasedWithDspTime 中调用。
|
|
/// </summary>
|
|
public static void RecordInput(int trackIndex, double dspTime, bool isPress)
|
|
{
|
|
if (!isRecording || currentReplay == null) return;
|
|
|
|
currentReplay.inputEvents.Add(new ReplayData.InputEvent(trackIndex, dspTime, isPress));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取消当前录制。
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 开始回放。在 GameManager 启动游玩前调用(需先设置 autoplay 模式)。
|
|
/// </summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 停止回放。
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 触发回放输入。需接入 InputManager 的事件系统或直接调用判定逻辑。
|
|
/// 当前为占位实现,供后续快速接入。
|
|
/// </summary>
|
|
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;
|
|
}
|