13674307070 发表于 2026-8-20 08:44:08

定时器系统只有六个接口,时间驱动逻辑的骨架

一、两类核心定时器:全局和个人,绑定对象决定一切
VV 引擎的定时器体系,最核心的区分就是绑定对象。

全局定时器不绑定任何玩家或地图,它是全服唯一的。用 OpenGlobalTimer 开启,传一个定时器 ID 和执行间隔(秒),到时间就触发 OnGlobalTimer 函数,把 ID 传进来。全服不管多少玩家,同一个 ID 的全局定时器只有一个在跑。

个人定时器绑定玩家。用 OpenTimer 开启,多传一个玩家对象参数。每个玩家可以有自己独立的定时器,同一个 ID 在不同玩家身上是互不干扰的。触发时调 OnPlayerTimer,把玩家对象和 ID 一起传进来。

这个区分看起来简单,但它决定了定时器的适用场景。

全局定时器适合做全服统一的时间逻辑。比如全服双倍经验活动倒计时、每隔一小时刷一次世界 BOSS、服务器时间检测、全服公告定时发送。这些逻辑不针对某个特定玩家,是整个服务器层面的,用全局定时器最合适。

个人定时器适合做玩家个体的时间逻辑。比如某个玩家的 Buff 持续时间、玩家的任务倒计时、玩家的技能冷却检测、玩家的离线计时。这些逻辑是绑定在具体玩家身上的,每个玩家可能有不同的状态,用个人定时器最合适。

举个实际的例子:做 "在线奖励" 功能,玩家每在线 5 分钟领一次奖励。这个功能用个人定时器 —— 每个玩家开启一个 5 秒间隔的个人定时器,触发时检测该玩家的在线时长,到 5 分钟就发奖励。每个玩家的在线时长是独立的,用全局定时器做不了 —— 全局定时器触发时你不知道该给谁发,除非遍历所有玩家,那效率就低了。

再比如做 "全服攻城战倒计时",距离攻城开始还有 30 分钟,每隔 1 分钟全服公告一次。这个用全局定时器 —— 全服只有一个攻城战时间,统一倒计时统一公告,不需要绑定玩家。

选对定时器类型,是用好定时器的第一步。该用全局的用了个人,就会每个玩家都开一个重复的定时器,浪费资源;该用个人的用了全局,就不得不遍历玩家,逻辑复杂且效率低。

二、生命周期管理:开、关、查,三件事
不管全局还是个人,定时器的生命周期管理都是三个操作:开启、关闭、检测。

开启用 OpenGlobalTimer 或 OpenTimer。这里有个细节要注意:重复开启同一个 ID 的定时器会怎样? 文档里没有明确说,但按常规引擎的设计,重复开启应该是覆盖旧的 —— 同一个 ID 的定时器,后开的会替换先开的。但这个行为最好自己测试确认,因为如果重复开启是 "叠加" 而不是 "覆盖",就会出现同一个 ID 触发多次的 bug。

实际开发中的好习惯是:开启定时器之前先检测是否存在,存在就先关再开,或者直接开(如果确认是覆盖语义)。 不要假设开启一定成功,也不要假设重复开启的行为,写代码的时候处理一下边界情况。

关闭用 CloseGlobalTimer 或 CloseTimer。关闭定时器是非常重要但容易被忽略的操作。很多 bug 都来自 "定时器该关没关"—— 玩家下线了定时器还在跑、活动结束了定时器还在触发、Buff 过期了定时器还在检测。这些都会导致逻辑错误甚至性能问题。

所以开定时器的时候,就要想清楚什么时候关。个人定时器在玩家下线、退出地图、完成任务的时候要关;全局定时器在活动结束、功能关闭的时候要关。把关闭操作写在所有可能的退出路径上,不要只写在 "正常完成" 的路径里 —— 异常退出、玩家掉线、地图销毁,这些路径也要关。

检测用 CheckGlobalTimer 或 CheckTimer,返回布尔值告诉你这个定时器还在不在。检测的应用场景很多:开启前检测避免重复开启、关闭前检测避免关一个不存在的(虽然关不存在的应该也不会报错,但检测一下更稳妥)、逻辑判断时检测某个定时任务是否还在运行。

检测还有一个重要用途:做状态同步。比如你有一个 "正在进行中" 的标记,这个标记应该和定时器的存在状态保持一致 —— 定时器在跑就是进行中,定时器关了就是结束了。这时候用检测接口来判断状态,比自己维护一个变量更可靠,因为定时器的状态是引擎管理的,不会出现 "变量说在跑但定时器已经停了" 的不一致。

开、关、查三件事,看起来简单,但真正用好的关键在于把关闭和检测当成和开启同等重要的操作来对待。很多人只记得开,忘了关,也不查,到最后定时器满天飞,逻辑乱成一锅粥。

三、触发函数:统一入口,按 ID 分发
定时器的触发函数设计是整个体系里最值得说的部分。

全局定时器只有一个触发函数 OnGlobalTimer,不管你开了多少个全局定时器,触发时都走这一个函数,靠传入的 ID 来区分是哪个定时器。

个人定时器同理,只有一个 OnPlayerTimer,所有个人定时器触发都走这里,靠玩家对象和 ID 两个参数来区分。

这个设计的好处是集中管理。所有定时器的逻辑都在一个函数里,用 if-else 或 switch 按 ID 分发,想查某个定时器做了什么,直接在触发函数里搜 ID 就行。如果每个定时器都要注册一个独立的回调函数,那定时器多了之后回调函数散落在各个地方,维护起来很痛苦。

但这个设计也有要求:ID 要规划好,不能乱。如果全局定时器用了 1 到 100,个人定时器也用了 1 到 100,虽然两者不冲突(因为触发函数不同),但人看的时候容易搞混。建议做一套 ID 规划规范 —— 比如全局定时器用 1000 以上的 ID,个人定时器用 1 到 999,或者按功能模块分段(1-100 给活动、101-200 给 Buff、201-300 给任务),这样看到 ID 就知道是哪个模块的定时器。

还有一个心得:触发函数里的逻辑要尽量短,不要做太重的操作。定时器触发是引擎调度的,如果触发函数里做了耗时操作(比如大量循环、复杂计算、IO 操作),会阻塞引擎的主循环,影响整个服务器的性能。如果确实需要做重逻辑,考虑在触发函数里只做标记或分发,把重逻辑放到其他地方异步处理。

另外,触发函数里要注意判空和异常处理。个人定时器触发时传进来的玩家对象,理论上应该是有效的,但极端情况下(比如玩家刚好在触发的瞬间下线)可能已经失效了。在函数开头判断一下玩家对象是否有效,再往下走逻辑,避免对无效对象操作导致报错。

四、VV 引擎里的其他时间机制:不止这两种定时器
虽然这篇文档只给了全局和个人两类定时器,但在之前的模块里,我们已经遇到了好几种其他的时间机制。把它们放在一起对比,才能完整理解 VV 引擎的时间驱动体系。

地图定时器( setenvirontimer )是绑定地图的。和个人定时器绑定玩家类似,地图定时器绑定地图,每个地图可以有独立的定时器。做副本里的 "30 秒后刷 BOSS"" 每隔 1 分钟刷一波小怪 ",用地图定时器最合适 —— 因为这些逻辑是和副本地图绑定的,副本销毁了定时器也跟着没了,不需要手动关。

地图定时器和全局 / 个人定时器的区别在于绑定对象不同 —— 全局不绑定、个人绑玩家、地图绑地图。选择哪个,看你的逻辑是和什么绑定的:全服统一的用全局、玩家个体的用个人、地图空间的用地图。

延时跳转( delaygoto )是另一种时间机制。它不是 "每隔多久触发一次" 的循环定时器,而是 "延迟多久之后执行一次" 的一次性定时器。传一个延时(毫秒)和一个函数名,到时间后调用那个函数,调用完就结束了,不会重复触发。

延时跳转适合做一次性的延迟执行。比如玩家使用回城卷轴,3 秒后传送(期间可以被攻击打断);比如玩家死亡后 5 秒自动复活;比如技能释放后延迟 2 秒产生爆炸效果。这些都是 "只执行一次" 的延迟逻辑,用循环定时器反而麻烦 —— 还得在触发函数里自己关定时器。

延时跳转还有一个配套的 cleardelaygoto 可以取消还没执行的延时。做 "可被打断的延迟执行" 的时候,这个取消接口很关键 —— 回城读条被攻击打断了,就要调 cleardelaygoto 把延时取消掉,不然时间到了还是会传送。

QFunction 里的自定义定时器是更底层的方式。如果你熟悉 Lua,可以自己用 os.time() 或引擎提供的时间函数,在某个高频触发里(比如玩家登录、攻击、移动)做时间差计算,模拟定时器。但这种方式不推荐,因为它依赖触发频率,玩家不操作就不会触发,计时不准。除非引擎的定时器体系满足不了你的特殊需求,否则不要自己造轮子。

把这些时间机制放在一起看,VV 引擎的时间驱动体系其实是分层的:


[*]循环定时器:全局、个人、地图,按间隔重复触发,适合持续检测和周期性逻辑
[*]一次性延时:delaygoto,延迟后执行一次,适合延迟执行和可打断的读条
[*]自定义计时:自己算时间差,最灵活但最不可靠,特殊场景才用

选哪种机制,看你的逻辑是 "周期性重复" 还是 "一次性延迟",以及绑定的对象是什么。选对了机制,代码简洁又可靠;选错了,要么多写很多冗余逻辑,要么出各种边界 bug。

五、实际使用中的几个设计模式
把定时器用熟了之后,会发现一些反复出现的设计模式。总结几个最常用的。

模式一:定时器做状态机的时钟。 很多游戏逻辑本质上是状态机 —— 比如 BOSS 有 "待机、战斗、狂暴、死亡" 几个状态,状态之间的转换有时候是事件驱动的(血量到了就狂暴),有时候是时间驱动的(战斗状态超过 10 分钟就进入狂暴)。时间驱动的状态转换,就用定时器做时钟 —— 定时器每隔 1 秒触发一次,检测当前状态和持续时间,到点就转换状态。

这种模式的好处是状态转换逻辑集中在定时器触发函数里,不用散落在各个事件触发里。而且时间精度由定时器保证,不用自己在每个事件里算时间差。

模式二:定时器做倒计时。 活动倒计时、副本倒计时、Buff 倒计时,都是同一个模式 —— 开启定时器时记录结束时间,定时器每隔 1 秒触发一次,计算当前时间和结束时间的差值,更新显示或判断是否到期。到期了就关定时器,执行到期逻辑。

这里有个心得:不要用定时器的触发次数来倒计时(比如 30 秒就设个 1 秒间隔的定时器,数 30 次),而要用 "结束时间戳减当前时间戳" 来算剩余时间。因为定时器可能不精确(触发延迟、错过触发),数次数会越数越不准。用时间戳算,即使定时器偶尔延迟,剩余时间还是准确的。

模式三:定时器做轮询检测。 有些状态没有事件通知,只能靠轮询 —— 比如检测玩家背包里有没有某个物品、检测某个坐标有没有怪物、检测某个条件是否满足。这时候用定时器每隔几秒检测一次,条件满足了就执行逻辑并关定时器。

轮询的间隔要权衡 —— 间隔太短消耗性能,间隔太长响应不及时。根据实际需求选,比如检测 "玩家是否走到了目标位置",1 秒一次就够了;检测 "活动是否开始",1 分钟一次都行。

模式四:多个定时器配合。 复杂逻辑可能需要多个定时器配合 —— 比如一个 1 秒的定时器做倒计时显示,一个 5 分钟的定时器做阶段转换,一个 30 分钟的定时器做整体超时。不同频率的定时器各司其职,不要用一个定时器在里面判断 "到没到 5 分钟"" 到没到 30 分钟 ",那样每次触发都要做一堆时间判断,效率低且逻辑乱。

六、几个容易踩的坑
定时器系统接口少,但坑不少。总结几个最常见的。

第一,忘记关定时器。 这是最常见也最容易出问题的坑。玩家下线了个人定时器还在跑,可能导致对无效玩家对象操作报错;活动结束了全局定时器还在触发,可能导致活动逻辑继续执行;副本销毁了地图定时器没关(虽然理论上地图销毁定时器会跟着没,但最好手动关)。开定时器的时候就想好什么时候关,把关闭操作写在所有退出路径上。

第二,重复开启定时器。 如果重复开启同一个 ID 的定时器是覆盖语义还好,如果是叠加语义,就会出现触发多次的问题。开启前先检测是否存在,存在就先关再开,或者确认引擎的行为是覆盖。不要假设。

第三,定时器里做太重的操作。 定时器触发是在引擎主线程里的,如果触发函数里做了大量循环或复杂计算,会阻塞服务器。尤其是全局定时器,全服只有一个线程在跑,阻塞了影响所有玩家。重逻辑拆成多次触发逐步处理,或者放到事件驱动的逻辑里做。

第四,个人定时器的玩家对象失效。 定时器触发时玩家可能已经下线了,这时候传进来的玩家对象可能是无效的。在触发函数开头判断对象有效性,不要直接用。

第五,用循环定时器做一次性延迟。 该用 delaygoto 的时候用了定时器,就得在触发函数里自己关定时器,多一步操作还容易忘。一次性延迟就用延时跳转,不要用循环定时器模拟。

第六,定时器 ID 冲突。 不同模块的人开发时如果都用 1、2、3 这种 ID,很容易冲突。提前做好 ID 规划,按模块分段,用常量定义不要用裸数字。

第七,依赖定时器的精确性。 定时器的触发间隔是 "至少这么久",不是 "精确这么久"。引擎负载高的时候可能延迟触发。不要用定时器做需要高精度计时的逻辑(比如精确到毫秒的技能判定),那种应该用引擎的帧更新或事件驱动。


13674307070 发表于 2026-8-20 08:44:55

挠头的500贡献

13674307070 发表于 2026-8-21 08:24:40

挠头的500贡献
页: [1]
查看完整版本: 定时器系统只有六个接口,时间驱动逻辑的骨架