673 lines
13 KiB
Markdown
673 lines
13 KiB
Markdown
# 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`
|
|
- 专门的状态机/调度器
|