115 lines
3.8 KiB
C#
115 lines
3.8 KiB
C#
using UnityEngine;
|
||
using TMPro;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
using BaseGames.Core.Save;
|
||
|
||
namespace BaseGames.Support.Speedrun
|
||
{
|
||
/// <summary>
|
||
/// 速通计时器(架构 16_SupportingModules §8)。
|
||
/// 使用 Time.unscaledDeltaTime 以免被 HitStop(timeScale < 1)拉慢;游戏暂停时须由外部调用 PauseTimer()。
|
||
/// 实现 ISaveable,计时持久化到 <see cref="StatsSaveData.SpeedrunTime"/>。
|
||
/// </summary>
|
||
public class SpeedrunTimer : MonoBehaviour, ISaveable
|
||
{
|
||
[Header("显示设置")]
|
||
[SerializeField] private TMP_Text _display;
|
||
[SerializeField] private GlobalSettingsSO _settings;
|
||
|
||
[Header("事件频道")]
|
||
[SerializeField] private VoidEventChannelSO _onTimerReset;
|
||
[SerializeField] private BoolEventChannelSO _onTimerVisibilityChanged;
|
||
|
||
public float ElapsedSeconds { get; private set; }
|
||
public bool IsRunning { get; private set; }
|
||
|
||
private bool _paused;
|
||
/// <summary>上一帧已显示的整秒值,展示内容未变化时跳过字符串重建。</summary>
|
||
private int _lastDisplayedSecond = -1;
|
||
|
||
private void OnEnable()
|
||
{
|
||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
bool show = _settings != null && _settings.ShowSpeedrunTimer;
|
||
SetVisible(show);
|
||
if (show) StartTimer();
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (!IsRunning || _paused) return;
|
||
ElapsedSeconds += Time.unscaledDeltaTime;
|
||
// 仅当整秒数发生变化时才重建展示字符串,避免每帧分配
|
||
int currentSecond = (int)ElapsedSeconds;
|
||
if (currentSecond != _lastDisplayedSecond)
|
||
{
|
||
_lastDisplayedSecond = currentSecond;
|
||
UpdateDisplay();
|
||
}
|
||
}
|
||
|
||
// ── 控制接口 ──────────────────────────────────────────────────────────────
|
||
public void StartTimer()
|
||
{
|
||
IsRunning = true;
|
||
_paused = false;
|
||
}
|
||
|
||
public void StopTimer() => IsRunning = false;
|
||
|
||
public void PauseTimer() => _paused = true;
|
||
|
||
public void ResumeTimer() => _paused = false;
|
||
|
||
public void ResetTimer()
|
||
{
|
||
ElapsedSeconds = 0f;
|
||
UpdateDisplay();
|
||
_onTimerReset?.Raise();
|
||
}
|
||
|
||
public void SetVisible(bool visible)
|
||
{
|
||
if (_display != null) _display.gameObject.SetActive(visible);
|
||
_onTimerVisibilityChanged?.Raise(visible);
|
||
}
|
||
|
||
// ── ISaveable ─────────────────────────────────────────────────────────────
|
||
public void OnSave(SaveData saveData)
|
||
{
|
||
if (saveData?.Stats != null)
|
||
saveData.Stats.SpeedrunTime = ElapsedSeconds;
|
||
}
|
||
|
||
public void OnLoad(SaveData saveData)
|
||
{
|
||
if (saveData?.Stats != null)
|
||
ElapsedSeconds = saveData.Stats.SpeedrunTime;
|
||
UpdateDisplay();
|
||
}
|
||
|
||
// ── 内部 ──────────────────────────────────────────────────────────────────
|
||
private void UpdateDisplay()
|
||
{
|
||
if (_display == null) return;
|
||
int hours = (int)(ElapsedSeconds / 3600);
|
||
int minutes = (int)(ElapsedSeconds % 3600 / 60);
|
||
int seconds = (int)(ElapsedSeconds % 60);
|
||
int ms = (int)(ElapsedSeconds * 100 % 100);
|
||
_display.text = hours > 0
|
||
? $"{hours:00}:{minutes:00}:{seconds:00}.{ms:00}"
|
||
: $"{minutes:00}:{seconds:00}.{ms:00}";
|
||
}
|
||
}
|
||
}
|