时间

可暂停的游戏时间,分为三个独立时钟——Gameplay、UI 与 Background——每个时钟都有自己的速度控制。

三个独立时钟——Gameplay、UI、Background · 按时钟的时间缩放与可安全嵌套的暂停 · 空操作替代:NullTimeService

CGS 用 ITimeService 防止经典的暂停菜单 bug——音乐中断、UI 冻结、菜单开着游戏却还在背后运行。Unity 的全局 Time.timeScale 会一次性停下所有东西。时间服务则运行三个相互独立的时钟:

时钟典型用途暂停时
Clock.Gameplay移动、AI、战斗、与物理相邻的逻辑菜单打开时暂停的就是它
Clock.UI菜单与 HUD 动画继续流动,菜单照常播放动画
Clock.Background音乐、环境音继续流动,音乐永不卡顿

_time.DeltaTime(Clock.Gameplay) 而非 Time.deltaTime 读取时间。用 _time.SetTimeScale(Clock.Gameplay, 0.3f) 放慢单个时钟即可获得电影感的慢动作——UI 和音乐保持原速。暂停可以安全嵌套:如果三个系统都暂停了某个时钟,它会一直保持暂停,直到三个系统全部恢复它。

获取服务

Awake()Start() 中解析并缓存引用。不要在 Update() 内解析——每帧解析纯属浪费。

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class MyGameplay : MonoBehaviour
{
    private ITimeService _time;

    private void Awake()
    {
        _time = ServiceLocator.Resolve<ITimeService>();
    }

    private void Update()
    {
        float dt = _time.DeltaTime(Clock.Gameplay);
        // Use dt for movement, AI, and other game logic.
    }
}

API 参考

读取时间(全部 O(1),仅限主线程)

float DeltaTime(Clock clock)         // Scaled, pause-aware frame delta; 0 while paused
float UnscaledDeltaTime(Clock clock) // Raw frame delta; ignores pause and time scale
float FixedDeltaTime(Clock clock)    // Fixed-step delta, scaled and pause-aware
double Time(Clock clock)             // Accumulated game time (double precision;
                                     // affected by pause and time scale)
double UnscaledTime(Clock clock)     // Accumulated real time; always increases.
                                     // Use for elapsed-time measurement and timeouts.

时间缩放控制

**时间缩放(time scale)**是一个速度乘数:1.0 是正常速度,0.5 是半速,0 的表现相当于暂停。

float GetTimeScale(Clock clock)             // The clock's current scale
void SetTimeScale(Clock clock, float scale) // Set one clock's scale (0 allowed)
void SetGlobalTimeScale(float scale)        // Set all three clocks at once

暂停控制(基于计数,可安全嵌套)

void Pause(Clock clock)        // Increment the clock's pause counter
void Resume(Clock clock)       // Decrement it; going below zero throws
bool IsPaused(Clock clock)     // true while the counter is above 0
int GetPauseCount(Clock clock) // The current counter value (diagnostic)
void PauseAll()                // Pause all three clocks
void ResumeAll()               // Resume all three; clocks already at zero are
                               // skipped, so this never throws

正是这个计数器让相互重叠的暂停来源变得安全:暂停菜单、过场动画、失焦自动暂停处理器可以各自调用 Pause(Clock.Gameplay) 而互不知晓,游戏只有在最后一个调用 Resume 之后才会恢复。

完整示例

一个冻结游戏玩法、但保持 UI 动画的暂停菜单:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class PauseMenu : MonoBehaviour
{
    private ITimeService _time;

    private void Awake() => _time = ServiceLocator.Resolve<ITimeService>();

    public void Open()
    {
        _time.Pause(Clock.Gameplay);
        gameObject.SetActive(true);
    }

    public void Close()
    {
        _time.Resume(Clock.Gameplay);
        gameObject.SetActive(false);
    }
}

只作用于游戏玩法的电影式慢动作:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class BossIntro : MonoBehaviour
{
    private ITimeService _time;

    private void Awake() => _time = ServiceLocator.Resolve<ITimeService>();

    public void BeginSlowMotion() => _time.SetTimeScale(Clock.Gameplay, 0.3f);

    public void EndSlowMotion() => _time.SetTimeScale(Clock.Gameplay, 1.0f);
}

你可以在演示场景 Demo/MotionLab.unity 中实际看到时钟分离:其中的 Gameplay 时间缩放滑块通过 SetTimeScale(Clock.Gameplay, value) 放慢并冻结动画陈列,而运行在 Clock.UI 上的旋转指示器不受影响、继续转动。

关闭该服务

ServiceLocator.Replace<ITimeService>(new NullTimeService());

NullTimeService 直接透传 Unity 自己的 Time 值。所有增量与时间查询都返回 UnityEngine.Time 的值,暂停和时间缩放调用不做任何事。当你的游戏不需要分离的时钟时使用它——比如原型,或直接管理 Time.timeScale 的项目。

请释放被你替换掉的实例。 ServiceLocator.Replace<T> 只交换注册表条目,别的什么都不做——它不会释放被替换的实例,因为它无法知道你是否仍持有对它的引用。TimeService 拥有一个 [CGS] TimeService Ticker GameObject(Play 模式下标记为 DontDestroyOnLoad),因此被替换但未释放的实例会让那个 GameObject 一直存活并持续 tick:Hierarchy 里会多出第二个 ticker,在本次会话余下的时间里永远不会消失。玩家构建中也没有能清理它的销毁流程。

var previous = ServiceLocator.Resolve<ITimeService>() as System.IDisposable;
ServiceLocator.Replace<ITimeService>(new NullTimeService());
previous?.Dispose();   // destroys the old [CGS] TimeService Ticker

在交换之后再释放,这样中间不会有任何代码解析到已被释放的实例。同样的规则适用于每一个拥有内部 [CGS] … GameObject 的服务——对象池、音频、UI 框架、调度器、补间与补间序列。

常见陷阱

  • 仅限主线程。 在 Debug 构建中,从工作线程(Task.Run、线程池等)调用时间服务会抛出 InvalidOperationException。请在主线程上解析并缓存实例。

  • Update() 调用者中缓存实例。 ServiceLocator.Resolve<T>() 是一次快速的字典查找,但并非零开销。如果你每帧读取时间,请在 Awake() 中缓存 _time

  • 嵌套暂停基于计数。 调用两次 Pause(Clock.Gameplay) 就需要两次 Resume()。正是这一点让相互重叠的暂停来源互不干扰——但不成对的 Resume() 在 Debug 构建中会抛出异常,所以请保持配对平衡。

  • 不要碰 UnityEngine.Time.timeScale 时间服务假定 Unity 的全局时间缩放保持在 1.0,并以 Time.unscaledDeltaTime 为基准。自己设置 Time.timeScale 会破坏按时钟的分离。全局慢动作请改用 SetGlobalTimeScale()

  • UnscaledTime 永远向前走。 它完全忽略暂停和时间缩放,只在域重载(domain reload)时重置。用它来测量经过时间、加载画面超时和频率限制——任何不应受游戏时间影响的测量。它对每个时钟返回相同的值;那个参数只是为了可读性而存在。

相关页面