CD Code Digest Hub /CS:GO

主循环、HostState 与 Tick

13 分钟阅读 7 个章节 3 张图

本页解释“一帧”在引擎里如何被切分:外层 CEngine::Frame 负责限帧与状态机,CHostState 负责换图/存读档等宏观状态,_Host_RunFrame 把真实时间切成固定 tick 并依次运行输入、服务器、客户端,最后渲染与混音。

相关页面:引擎总览 · 网络通道与协议 · 实体复制 · 游戏 DLL 契约

相关源码文件#

  • engine/sys_engine.cpp:CEngine::Frame、FilterTime
  • engine/host_state.cpp:CHostState
  • engine/host.cpp:Host_RunFrame、_Host_RunFrame、_Host_RunFrame_*
  • engine/sv_main.cpp:SV_Frame、SV_Think、SV_SendClientUpdates
  • game/server/gameinterface.cpp:CServerGameDLL::GameFrame

1. 外层:CEngine::Frame#

  1. 窗口失焦且未播放内嵌视频时,按 engine_no_focus_sleep 等待输入:engine/sys_engine.cpp:420-428。
  2. 用 Sys_FloatTime 计算增量并累积到 m_flFrameTime:engine/sys_engine.cpp:430-475。
  3. FilterTime 判断是否该跑一帧;否则按差值睡眠并记录被“吞掉”的时间:engine/sys_engine.cpp:477-499。客户端由 fps_max(以及菜单、分屏专用上限)限帧;在多人、未开作弊的 PC 客户端上,fps_max 若被设为 0 以外且小于 30 的值,会被强制改为 30:engine/sys_engine.cpp:278-288、engine/sys_engine.cpp:361-373。专用服务器则把最小帧时间锁到下一个 tick:engine/sys_engine.cpp:266-271。
  4. 非专用服务器时通知 client.dll FRAME_START:engine/sys_engine.cpp:530-535。
  5. DLL 状态为 active/close/restart 时调用 HostState_Frame;状态切换为 close/restart 时设置退出标志:engine/sys_engine.cpp:552-581。

2. 宏观状态机:CHostState#

证据:

  • 状态枚举:engine/host_state.cpp:54-64。
  • State_Run 根据 m_nextState 转移:新游戏/读档会先显示加载提示,然后与关闭、重启一起先进入 HS_GAME_SHUTDOWN(源码注释说明“新游戏前必须先关闭旧游戏”),换图则直接进入对应状态:engine/host_state.cpp:622-652。
  • State_NewGame 在 Host_NewGame 成功时进入 HS_RUN;失败时 GameShutdown 后同样回到 HS_RUN(服务器停留在控制台):engine/host_state.cpp:414-454。
  • FrameUpdate 循环执行当前状态,只有 HS_RUN 每帧只跑一次,其它状态会在同一帧内连续推进直到回到 HS_RUN;关闭/重启状态执行后跳出;调试版检测超过 8 次循环视为状态死循环:engine/host_state.cpp:753-844。
  • FrameUpdate 用 setjmp(host_abortserver) 作为致命错误的回退点:engine/host_state.cpp:759-763。
  • 进入任何加载类状态前都会调用 g_pMDLCache->BeginMapLoad():engine/host_state.cpp:786-816。

State_Run 还会在客户端正在连接时把帧时间钳制到一个 tick(换图后的“短帧”窗口),并在专用服务器上包裹看门狗:engine/host_state.cpp:588-613。

3. Tick 切分:_Host_RunFrame#

时间累积#

host_remainder 累积帧时间;当它不小于一个 tick(sv_alternateticks 且单人时为两个 tick)时,计算本帧要模拟的 numticks = floor(remainder / interval) 并扣除;host_nexttick 记录距离下一个 tick 还差多少:engine/host.cpp:4051-4096。

单线程路径(专用服务器始终如此)#

证据:tick 循环与各阶段调用 engine/host.cpp:4185-4277;HLTV 在循环后运行 engine/host.cpp:4289-4293;预测与鼠标外插 engine/host.cpp:4322-4344;渲染与声音 engine/host.cpp:4528-4555。各阶段函数:输入 engine/host.cpp:3256-3295、服务器 engine/host.cpp:3304-3320、客户端 engine/host.cpp:3352-3386、渲染 engine/host.cpp:3430-3459。

要点:

  • 只在最后一个 tick 发送网络更新。 bFinalTick 只在最后一次循环为真,注释说明这是为了避免一帧内多次编码网络数据:engine/host.cpp:4210-4211;对非回环连接(或开启 host_limitlocal 时),CL_Move 只在最后一个 tick 且到达发送时间时才发包,本地回环连接默认不受此限制:engine/cl_main.cpp:2767-2774;SV_Frame 只在 finalTick 时发送客户端更新:engine/sv_main.cpp:3475-3483。
  • numticks == 0 也会渲染。 渲染与 tick 无关,客户端插值量取 remainder / interval:engine/host.cpp:4327。

多线程路径(仅监听服务器)#

Host_RunFrame 在本地服务器与客户端都活跃时,按 host_thread_mode 决定是否启用引擎线程化(默认取决于线程池是否有线程):engine/host.cpp:4648-4660。线程化时客户端使用上一帧的 tick 数与余量在主线程运行,服务器 tick 被封装成 CFunctorJob 投递到线程池,帧末等待并执行延迟的服务器工作:engine/host.cpp:4354-4508、engine/host.cpp:4596。在此模式下单人游戏的 SV_SendClientUpdates 会被延迟到主线程执行:engine/sv_main.cpp:3478-3481。源码注释写明“threaded path only supported in listen server”:engine/host.cpp:4168-4172。

4. 服务器 tick:SV_Frame#

证据:SV_Frame engine/sv_main.cpp:3428-3495;CBaseServer::RunFrame 处理套接字、超时、用户设置、待发服务器信息、主服务器更新:engine/baseserver.cpp:3261-3289;SV_Think 设置 gpGlobals 后交给插件处理器:engine/sv_main.cpp:3327-3366;插件处理器先回调所有插件再调用 server.dll:engine/sv_plugin.cpp:424-435;SV_SendClientUpdates 先 PreClientUpdate 再 SendClientMessages,并把字符串表 tick 推进 1:engine/sv_main.cpp:3402-3417。

注意几个条件:

  • 只有存在客户端时才递增服务器 tick;没有客户端的多人服务器仍会调用 SV_Think(false) 让游戏系统运转:engine/sv_main.cpp:3457-3473。
  • 单人游戏只有本地客户端处于 active 时才真正模拟:engine/sv_main.cpp:3354-3359。
  • 网络字符串表在帧内解锁、帧末重新加锁,用于在调试版中发现“非预期修改”:engine/sv_main.cpp:3451-3452、engine/sv_main.cpp:3485-3486。

5. 游戏帧:CServerGameDLL::GameFrame#

顺序动作证据
0读档恢复中直接返回;sv_alternateticks 时奇数 tick 只更新客户端数据game/server/gameinterface.cpp:1423-1447
1清理延迟删除的实体game/server/gameinterface.cpp:1462
2所有 IGameSystem 的 FrameUpdatePreEntityThink,GameStartFramegame/server/gameinterface.cpp:1468-1469
3导航网格更新、统计上传、查询缓存、基准测试game/server/gameinterface.cpp:1472-1489
4Physics_RunThinkFunctions:模拟/Think 所有实体(玩家在此执行排队的 CUserCmd)game/server/gameinterface.cpp:1492、game/server/physics_main.cpp:2282-2339
5FrameUpdatePostEntityThink(延迟补偿记录等在此)与实体 I/O 事件队列game/server/gameinterface.cpp:1494-1498
6再次清理删除列表、UpdateAllClientData、g_pGameRules->EndGameFrame()game/server/gameinterface.cpp:1502-1512
7触发定时的网络属性变更事件game/server/gameinterface.cpp:1533

CS:GO 的 EndGameFrame 在这里驱动 Bot 管理器的 StartFrame,注释说明是为了让 Bot 在物理模拟之后做可见性检查:game/shared/cstrike15/cs_gamerules.cpp:9886-9893。

约束与边界#

  • 服务器 tick 内的 gpGlobals->curtime 会被玩家命令执行临时改写为玩家自己的 tickbase 时间,执行完再恢复,见 输入与预测。
  • _Host_RunFrame 用 setjmp(host_enddemo) 处理 demo 结束跳出:engine/host.cpp:4048-4049。
  • 本次没有对帧时序做任何实测;“每帧顺序”来自源码调用顺序。