主循环、HostState 与 Tick
本页解释“一帧”在引擎里如何被切分:外层 CEngine::Frame 负责限帧与状态机,CHostState 负责换图/存读档等宏观状态,_Host_RunFrame 把真实时间切成固定 tick 并依次运行输入、服务器、客户端,最后渲染与混音。
相关页面:引擎总览 · 网络通道与协议 · 实体复制 · 游戏 DLL 契约
相关源码文件#
engine/sys_engine.cpp:CEngine::Frame、FilterTimeengine/host_state.cpp:CHostStateengine/host.cpp:Host_RunFrame、_Host_RunFrame、_Host_RunFrame_*engine/sv_main.cpp:SV_Frame、SV_Think、SV_SendClientUpdatesgame/server/gameinterface.cpp:CServerGameDLL::GameFrame
1. 外层:CEngine::Frame#
- 窗口失焦且未播放内嵌视频时,按
engine_no_focus_sleep等待输入:engine/sys_engine.cpp:420-428。 - 用
Sys_FloatTime计算增量并累积到m_flFrameTime:engine/sys_engine.cpp:430-475。 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。- 非专用服务器时通知 client.dll
FRAME_START:engine/sys_engine.cpp:530-535。 - 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,GameStartFrame | game/server/gameinterface.cpp:1468-1469 |
| 3 | 导航网格更新、统计上传、查询缓存、基准测试 | game/server/gameinterface.cpp:1472-1489 |
| 4 | Physics_RunThinkFunctions:模拟/Think 所有实体(玩家在此执行排队的 CUserCmd) | game/server/gameinterface.cpp:1492、game/server/physics_main.cpp:2282-2339 |
| 5 | FrameUpdatePostEntityThink(延迟补偿记录等在此)与实体 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。- 本次没有对帧时序做任何实测;“每帧顺序”来自源码调用顺序。