AlicizaX/Client/Assets/Books/Framework/Runtime/Timer.md

673 lines
13 KiB
Markdown
Raw Normal View History

2026-04-01 13:20:06 +08:00
# Timer
## 模块概述
Timer 模块提供统一定时器服务,适合处理:
- 延时执行
- 循环轮询
- UI 倒计时
- 技能延时结算
- 超时控制
- 轻量轮询逻辑
它由 `TimerComponent` 注册 `TimerService`,并通过 `GameApp.Timer``AppServices.Require<ITimerService>()` 暴露给业务层。
从实现上看,`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<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`
定义:
```csharp
public delegate void TimerHandler(params object[] args);
```
适合:
- 参数数量不固定
- 通用回调
- 快速搭建原型
### `TimerHandlerNoArgs`
定义:
```csharp
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. 一次性延时执行
```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<int>(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<T>`
## 性能注意事项
- 少量长生命周期定时器成本很低
- 大量高频定时器建议业务上合并
- 能用泛型单参数回调时,优先别用 `object[]`
- 大量短周期循环定时器应谨慎使用,优先考虑合并成统一更新器
## 适用场景建议
### 适合使用 Timer 模块
- 秒级倒计时
- UI 展示延迟
- 技能或状态延后执行
- 轻量循环任务
### 不适合使用 Timer 模块
- 每帧复杂逻辑
- 高频实时物理计算
- 长链路异步流程编排
这些场景更适合:
- `Update`
- `Coroutine`
- `UniTask`
- 专门的状态机/调度器