13 KiB
13 KiB
Timer
模块概述
Timer 模块提供统一定时器服务,适合处理:
- 延时执行
- 循环轮询
- UI 倒计时
- 技能延时结算
- 超时控制
- 轻量轮询逻辑
它由 TimerComponent 注册 TimerService,并通过 GameApp.Timer 或 AppServices.Require<ITimerService>() 暴露给业务层。
从实现上看,TimerService 使用时间轮来管理定时器,重点优化的是:
- 大量定时器的调度效率
- 低 GC 的回调执行
- 支持缩放时间和非缩放时间两套时间基准
快速开始
最少步骤
- 在场景中挂载
TimerComponent - 调用
GameApp.Timer.AddTimer(...) - 保存返回的
timerId - 在对象销毁或逻辑结束时调用
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:负责在场景中注册TimerServiceTimerService:真正执行定时调度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 都是安全的:
StopResumeRestartRemoveTimer
对应返回值行为:
IsRunning返回falseGetLeftTime返回0
2. 非循环定时器会在触发后自动移除
一次性定时器执行回调后,不需要手动调用 RemoveTimer
但如果组件生命周期不确定,仍建议在 OnDestroy / OnDisable 中做防守式移除。
3. 回调异常会被捕获
TimerService 内部会捕获回调异常并记录日志,不会因为单个定时器异常直接打断整个调度链。
4. Stop / Resume 的语义更像“运行标记”
源码层面:
Stop(timerId)只是把定时器标记为IsRunning = falseResume(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 模块
- 每帧复杂逻辑
- 高频实时物理计算
- 长链路异步流程编排
这些场景更适合:
UpdateCoroutineUniTask- 专门的状态机/调度器