# Timer ## 模块概述 Timer 模块提供统一定时器服务,适合处理: - 延时执行 - 循环轮询 - UI 倒计时 - 技能延时结算 - 超时控制 - 轻量轮询逻辑 它由 `TimerComponent` 注册 `TimerService`,并通过 `GameApp.Timer` 或 `AppServices.Require()` 暴露给业务层。 从实现上看,`TimerService` 使用时间轮来管理定时器,重点优化的是: - 大量定时器的调度效率 - 低 GC 的回调执行 - 支持缩放时间和非缩放时间两套时间基准 ## 快速开始 ### 最少步骤 1. 在场景中挂载 `TimerComponent` 2. 调用 `GameApp.Timer.AddTimer(...)` 3. 保存返回的 `timerId` 4. 在对象销毁或逻辑结束时调用 `RemoveTimer(timerId)` ### 最小示例 ```csharp 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"); } } ``` ## 架构说明 ```text TimerComponent └─ TimerService ├─ TimerHandler ├─ TimerHandlerNoArgs ├─ TimerGenericInvokerCache └─ 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(Action 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` 定义: ```csharp public delegate void TimerHandler(params object[] args); ``` 适合: - 参数数量不固定 - 通用回调 - 快速搭建原型 ### `TimerHandlerNoArgs` 定义: ```csharp public delegate void TimerHandlerNoArgs(); ``` 适合: - 无参延时执行 - 最常见的循环 tick ### 泛型 `AddTimer` 适合: - 单参数且类型明确的回调 - 希望避免 `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(Action 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. 一次性延时执行 ```csharp 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. 循环计时器 ```csharp 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. 带参数的定时器 ```csharp 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. 泛型参数定时器 ```csharp using AlicizaX; using UnityEngine; public sealed class GenericTimerExample : MonoBehaviour { private int _timerId; private void Start() { _timerId = GameApp.Timer.AddTimer(OnDamageDelay, 200, 1.5f); } private void OnDestroy() { GameApp.Timer.RemoveTimer(_timerId); } private void OnDamageDelay(int damage) { Debug.Log($"Delayed damage: {damage}"); } } ``` ### 5. 不受暂停影响的 UI 倒计时 ```csharp 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. 查询剩余时间 ```csharp 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. 暂停、恢复与重启 ```csharp 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. 组件生命周期绑定 ```csharp 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. 循环定时器不移除 现象: - 组件销毁后仍继续运行 规避: - 在 `OnDisable` 或 `OnDestroy` 中移除 ### 2. 暂停界面仍使用缩放时间 现象: - 游戏暂停后倒计时也停住 规避: - UI 倒计时使用 `isUnscaled = true` ### 3. 在非循环定时器上依赖 `Restart` 现象: - 行为不像“重新开始原延时”,而是几乎立即触发 规避: - 直接重新创建一次性定时器 ### 4. `object[] args` 中频繁装箱拆箱 现象: - 代码可读性差 - 更容易写错类型转换 规避: - 单参数时优先用 `AddTimer` ## 性能注意事项 - 少量长生命周期定时器成本很低 - 大量高频定时器建议业务上合并 - 能用泛型单参数回调时,优先别用 `object[]` - 大量短周期循环定时器应谨慎使用,优先考虑合并成统一更新器 ## 适用场景建议 ### 适合使用 Timer 模块 - 秒级倒计时 - UI 展示延迟 - 技能或状态延后执行 - 轻量循环任务 ### 不适合使用 Timer 模块 - 每帧复杂逻辑 - 高频实时物理计算 - 长链路异步流程编排 这些场景更适合: - `Update` - `Coroutine` - `UniTask` - 专门的状态机/调度器