定时器是帧同步的节拍器而非计时工具,需用独立高精度定时器每16ms/33ms触发全局帧回调,仅执行指令消费、确定性逻辑、状态广播三件事;网络io须异步剥离;帧号与时间戳分离管理;广播按房间等逻辑单元精准过滤;udp下需ack+心跳兜底。

定时器是帧同步的节奏控制器,不是计时工具,而是逻辑推进的节拍器。它不负责“测时间”,而是强制所有环节按统一步调执行——输入采集、状态模拟、指令广播,必须卡在同一个时间点上触发。
帧定时器必须独立于网络和IO事件
不能依赖 onMessage 或 WebSocket 收包回调来驱动帧更新,否则不同客户端操作到达时间不一致,帧节奏立刻失锁。正确做法是用独立高精度定时器(如 Workerman 的 Timer::add、C++ 的 std::chrono + thread sleep、或游戏引擎的 fixed timestep)每 16ms(60FPS)或 33ms(30FPS)触发一次全局帧回调。
- 该回调内只做三件事:消费已缓存的操作指令、运行一帧确定性逻辑、打包并广播当前帧状态或指令
- 所有网络收发、磁盘读写、数据库查询都必须剥离出帧回调,改用异步方式或延迟到空闲帧处理
- 若某帧计算超时(如超过 16ms),下帧不应“追赶”,而应跳过或合并,避免雪崩式延迟累积
帧号与时间戳必须分离管理
帧号(frame index)是逻辑序号,从 0 开始单调递增;时间戳(timestamp)反映物理时刻。两者不能混用。客户端靠帧号做插值、丢帧判断、指令对齐;服务端靠帧号做指令匹配、ACK 确认、重传控制。
- 每次广播的数据必须显式携带 frame 字段,例如 {"frame": 128, "inputs": {...}}
- 客户端收到帧后,只接受 frame == expected_frame 的数据,其余缓存或丢弃
- 服务端维护每个客户端的 lastReceivedFrame,用于识别乱序包或检测掉线
广播范围必须按逻辑单元过滤
帧数据不是发给所有连接,而是按“房间”“战场”“对战组”等业务边界精准投递。未过滤的全量广播会放大延迟、浪费带宽、引发状态错乱。
- 服务端在广播前查表:获取当前帧所属房间的所有在线 clientId 列表
- 逐个检查连接状态与帧订阅关系,跳过非目标玩家、观战者或已断连连接
- 格斗/RTS 类游戏还可进一步按视野分组,只推送给相互可见的单位所在客户端
心跳与 ACK 是帧可靠性的兜底机制
UDP 或弱保障 TCP 下,关键帧可能丢失。仅靠定时器无法解决送达问题,需叠加轻量级确认机制。
- 客户端收到帧后立即回一个简短 ACK 包,含已成功处理的最高帧号
- 服务端维护每个客户端的 ackedFrame,若连续 N 帧未收到 ACK,则重发最近 M 帧指令
- 心跳包可复用帧通道:在无操作帧时,插入空帧 {"frame": x, "ping": true} 维持连接与帧序











