AlicizaX/Client/Assets/Books/Framework/Runtime/Timer.md
2026-04-01 13:20:06 +08:00

13 KiB

Timer

模块概述

Timer 模块提供统一定时器服务,适合处理:

  • 延时执行
  • 循环轮询
  • UI 倒计时
  • 技能延时结算
  • 超时控制
  • 轻量轮询逻辑

它由 TimerComponent 注册 TimerService,并通过 GameApp.TimerAppServices.Require<ITimerService>() 暴露给业务层。

从实现上看,TimerService 使用时间轮来管理定时器,重点优化的是:

  • 大量定时器的调度效率
  • 低 GC 的回调执行
  • 支持缩放时间和非缩放时间两套时间基准

快速开始

最少步骤

  1. 在场景中挂载 TimerComponent
  2. 调用 GameApp.Timer.AddTimer(...)
  3. 保存返回的 timerId
  4. 在对象销毁或逻辑结束时调用 RemoveTimer(timerId)

最小示例

using AlicizaX;
using UnityEngine;

public sealed class TimerQuickStart : MonoBehaviour
{
    private int _timerId;

    private void Start()
    {
        _timerId = GameApp.Timer.AddTimer(OnTick, 1f, true);
    }

    private void OnDestroy()
    {
        GameApp.Timer.RemoveTimer(_timerId);
    }

    private void OnTick()
    {
        Debug.Log("Tick");
    }
}

架构说明

TimerComponent
  └─ TimerService
      ├─ TimerHandler
      ├─ TimerHandlerNoArgs
      ├─ TimerGenericInvokerCache<T>
      └─ HierarchicalTimeWheel

关键协作关系

  • TimerComponent:负责在场景中注册 TimerService
  • TimerService:真正执行定时调度
  • ITimerService:业务层使用的统一接口
  • GameApp.Timer:高频调用入口

时间基准

Timer 模块支持两种时间基准:

  • 缩放时间:使用 Time.time
  • 非缩放时间:使用 Time.unscaledTime

这由 isUnscaled 参数控制:

  • false:受 Time.timeScale 影响
  • true:不受 Time.timeScale 影响

核心类与接口

ITimerService

公开能力:

  • AddTimer(TimerHandler callback, float time, bool isLoop = false, bool isUnscaled = false, params object[] args)
  • AddTimer(TimerHandlerNoArgs callback, float time, bool isLoop = false, bool isUnscaled = false)
  • AddTimer<T>(Action<T> callback, T arg, float time, bool isLoop = false, bool isUnscaled = false)
  • Stop(int timerId)
  • Resume(int timerId)
  • IsRunning(int timerId)
  • GetLeftTime(int timerId)
  • Restart(int timerId)
  • RemoveTimer(int timerId)
  • RemoveAllTimer()

TimerHandler

定义:

public delegate void TimerHandler(params object[] args);

适合:

  • 参数数量不固定
  • 通用回调
  • 快速搭建原型

TimerHandlerNoArgs

定义:

public delegate void TimerHandlerNoArgs();

适合:

  • 无参延时执行
  • 最常见的循环 tick

泛型 AddTimer<T>

适合:

  • 单参数且类型明确的回调
  • 希望避免 object[] 拆装箱和手动转换

API 参考

int AddTimer(TimerHandler callback, float time, bool isLoop = false, bool isUnscaled = false, params object[] args)

  • 必填参数:callback
  • 必填参数:time
  • 可选参数:isLoop
  • 可选参数:isUnscaled
  • 可选参数:args
  • 返回值:timerId

说明:

  • 注册一个支持参数数组的定时器
  • time 为延迟秒数
  • isLoop = true 时会按相同间隔循环触发

适合:

  • 参数个数可变
  • 临时逻辑
  • 不想额外声明泛型回调的场景

int AddTimer(TimerHandlerNoArgs callback, float time, bool isLoop = false, bool isUnscaled = false)

  • 必填参数:callback
  • 必填参数:time
  • 可选参数:isLoop
  • 可选参数:isUnscaled
  • 返回值:timerId

说明:

  • 注册无参定时器
  • 是最常用、最简洁的用法

int AddTimer<T>(Action<T> callback, T arg, float time, bool isLoop = false, bool isUnscaled = false)

  • 必填参数:callback
  • 必填参数:arg
  • 必填参数:time
  • 可选参数:isLoop
  • 可选参数:isUnscaled
  • 返回值:timerId
  • 泛型约束:无额外约束

说明:

  • 注册单参数强类型定时器
  • object[] args 更清晰、更安全

void Stop(int timerId)

  • 必填参数:timerId
  • 返回值:无

说明:

  • 把指定定时器标记为停止运行
  • 对无效 timerId 为安全无操作

void Resume(int timerId)

  • 必填参数:timerId
  • 返回值:无

说明:

  • 恢复一个已停止的定时器
  • 对无效 timerId 为安全无操作

bool IsRunning(int timerId)

  • 必填参数:timerId
  • 返回值:bool

说明:

  • 返回该定时器当前是否处于运行状态
  • 对无效 timerId 返回 false

float GetLeftTime(int timerId)

  • 必填参数:timerId
  • 返回值:剩余秒数

说明:

  • 返回定时器剩余触发时间
  • 对无效 timerId 返回 0

void Restart(int timerId)

  • 必填参数:timerId
  • 返回值:无

说明:

  • 重新调度该定时器
  • 对无效 timerId 为安全无操作

void RemoveTimer(int timerId)

  • 必填参数:timerId
  • 返回值:无

说明:

  • 从系统中移除指定定时器
  • 是最推荐的结束方式

void RemoveAllTimer()

  • 返回值:无

说明:

  • 清空当前全部定时器
  • 通常只建议在服务销毁、场景彻底重置或特殊测试环境下使用

常见用法

1. 一次性延时执行

using AlicizaX;
using UnityEngine;

public sealed class DelayExample : MonoBehaviour
{
    private int _delayTimer;

    private void Start()
    {
        _delayTimer = GameApp.Timer.AddTimer(OnDelayFinish, 2f);
    }

    private void OnDestroy()
    {
        GameApp.Timer.RemoveTimer(_delayTimer);
    }

    private void OnDelayFinish()
    {
        Debug.Log("2 seconds later");
    }
}

2. 循环计时器

using AlicizaX;
using UnityEngine;

public sealed class LoopTimerExample : MonoBehaviour
{
    private int _loopTimer;
    private int _counter;

    private void Start()
    {
        _loopTimer = GameApp.Timer.AddTimer(OnLoop, 0.5f, true);
    }

    private void OnDestroy()
    {
        GameApp.Timer.RemoveTimer(_loopTimer);
    }

    private void OnLoop()
    {
        _counter++;
        Debug.Log($"Loop count: {_counter}");

        if (_counter >= 5)
        {
            GameApp.Timer.RemoveTimer(_loopTimer);
        }
    }
}

3. 带参数的定时器

using AlicizaX;
using UnityEngine;

public sealed class TimerArgsExample : MonoBehaviour
{
    private int _timerId;

    private void Start()
    {
        _timerId = GameApp.Timer.AddTimer(OnRewardDelay, 3f, false, false, "Gold", 100);
    }

    private void OnDestroy()
    {
        GameApp.Timer.RemoveTimer(_timerId);
    }

    private void OnRewardDelay(params object[] args)
    {
        string rewardType = (string)args[0];
        int amount = (int)args[1];
        Debug.Log($"Reward => {rewardType}, amount => {amount}");
    }
}

4. 泛型参数定时器

using AlicizaX;
using UnityEngine;

public sealed class GenericTimerExample : MonoBehaviour
{
    private int _timerId;

    private void Start()
    {
        _timerId = GameApp.Timer.AddTimer<int>(OnDamageDelay, 200, 1.5f);
    }

    private void OnDestroy()
    {
        GameApp.Timer.RemoveTimer(_timerId);
    }

    private void OnDamageDelay(int damage)
    {
        Debug.Log($"Delayed damage: {damage}");
    }
}

5. 不受暂停影响的 UI 倒计时

using AlicizaX;
using UnityEngine;

public sealed class UnscaledCountdownExample : MonoBehaviour
{
    private int _timerId;
    private float _left = 5f;

    private void Start()
    {
        _timerId = GameApp.Timer.AddTimer(OnTick, 1f, true, true);
    }

    private void OnDestroy()
    {
        GameApp.Timer.RemoveTimer(_timerId);
    }

    private void OnTick()
    {
        _left -= 1f;
        Debug.Log($"Countdown: {_left}");

        if (_left <= 0f)
        {
            GameApp.Timer.RemoveTimer(_timerId);
        }
    }
}

6. 查询剩余时间

using AlicizaX;
using UnityEngine;

public sealed class LeftTimeExample : MonoBehaviour
{
    private int _timerId;

    private void Start()
    {
        _timerId = GameApp.Timer.AddTimer(OnFinish, 10f);
    }

    private void Update()
    {
        float left = GameApp.Timer.GetLeftTime(_timerId);
        Debug.Log($"Left: {left:F2}s");
    }

    private void OnDestroy()
    {
        GameApp.Timer.RemoveTimer(_timerId);
    }

    private void OnFinish()
    {
        Debug.Log("Finished");
    }
}

7. 暂停、恢复与重启

using AlicizaX;
using UnityEngine;

public sealed class PauseResumeTimerExample : MonoBehaviour
{
    private int _timerId;

    private void Start()
    {
        _timerId = GameApp.Timer.AddTimer(OnTick, 1f, true);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            GameApp.Timer.Stop(_timerId);
        }

        if (Input.GetKeyDown(KeyCode.R))
        {
            GameApp.Timer.Resume(_timerId);
        }

        if (Input.GetKeyDown(KeyCode.T))
        {
            GameApp.Timer.Restart(_timerId);
        }
    }

    private void OnDestroy()
    {
        GameApp.Timer.RemoveTimer(_timerId);
    }

    private void OnTick()
    {
        Debug.Log("Running timer");
    }
}

8. 组件生命周期绑定

using AlicizaX;
using UnityEngine;

public sealed class SafeTimerOwner : MonoBehaviour
{
    private int _timerId = -1;

    private void OnEnable()
    {
        _timerId = GameApp.Timer.AddTimer(OnHeartbeat, 2f, true);
    }

    private void OnDisable()
    {
        if (_timerId > 0)
        {
            GameApp.Timer.RemoveTimer(_timerId);
            _timerId = -1;
        }
    }

    private void OnHeartbeat()
    {
        Debug.Log("Heartbeat");
    }
}

运行行为细节

这一部分基于当前 TimerService 实现整理,适合开发时理解边界行为。

1. 无效 timerId 的行为

以下方法对无效 timerId 都是安全的:

  • Stop
  • Resume
  • Restart
  • RemoveTimer

对应返回值行为:

  • IsRunning 返回 false
  • GetLeftTime 返回 0

2. 非循环定时器会在触发后自动移除

一次性定时器执行回调后,不需要手动调用 RemoveTimer

但如果组件生命周期不确定,仍建议在 OnDestroy / OnDisable 中做防守式移除。

3. 回调异常会被捕获

TimerService 内部会捕获回调异常并记录日志,不会因为单个定时器异常直接打断整个调度链。

4. Stop / Resume 的语义更像“运行标记”

源码层面:

  • Stop(timerId) 只是把定时器标记为 IsRunning = false
  • Resume(timerId) 只是把它重新标记为 true

注意点:

  • 如果定时器已经到达触发时刻,但当时处于 Stop 状态,那么该次调度不会执行
  • 对循环定时器来说,如果它在应触发的那一刻是停止状态,也不会自动重新挂回时间轮

因此更稳妥的经验是:

  • 短暂停顿并在触发前恢复:可以用 Stop / Resume
  • 需要明确重新开始计时:优先用 Restart

5. Restart 对循环定时器更直观

当前实现里:

  • 循环定时器的 Interval = time
  • 非循环定时器的 Interval = 0

这意味着:

  • 对循环定时器调用 Restart,会从当前时刻重新按原间隔开始计时
  • 对非循环定时器调用 Restart,会因为内部间隔是 0,变成“下一次 Tick 几乎立刻触发”

所以建议:

  • 循环定时器:可以使用 Restart
  • 一次性定时器:如果要重新延时,直接重新创建一个新的 timer 更清晰

最佳实践

推荐做法

  • timerId 与对象生命周期绑定
  • UI 倒计时优先用 isUnscaled = true
  • 对单参数回调优先使用泛型重载
  • 对复杂业务优先在回调中触发业务方法,而不是把整段逻辑都堆进匿名函数

推荐封装方式

如果你的项目里大量使用定时器,建议封装一层:

  • StartOnce(float delay, Action action)
  • StartLoop(float interval, Action action)
  • StopAndClear(ref int timerId)

这样可以减少重复样板代码和漏删问题。

常见错误

1. 循环定时器不移除

现象:

  • 组件销毁后仍继续运行

规避:

  • OnDisableOnDestroy 中移除

2. 暂停界面仍使用缩放时间

现象:

  • 游戏暂停后倒计时也停住

规避:

  • UI 倒计时使用 isUnscaled = true

3. 在非循环定时器上依赖 Restart

现象:

  • 行为不像“重新开始原延时”,而是几乎立即触发

规避:

  • 直接重新创建一次性定时器

4. object[] args 中频繁装箱拆箱

现象:

  • 代码可读性差
  • 更容易写错类型转换

规避:

  • 单参数时优先用 AddTimer<T>

性能注意事项

  • 少量长生命周期定时器成本很低
  • 大量高频定时器建议业务上合并
  • 能用泛型单参数回调时,优先别用 object[]
  • 大量短周期循环定时器应谨慎使用,优先考虑合并成统一更新器

适用场景建议

适合使用 Timer 模块

  • 秒级倒计时
  • UI 展示延迟
  • 技能或状态延后执行
  • 轻量循环任务

不适合使用 Timer 模块

  • 每帧复杂逻辑
  • 高频实时物理计算
  • 长链路异步流程编排

这些场景更适合:

  • Update
  • Coroutine
  • UniTask
  • 专门的状态机/调度器