CD Code Digest Hub /CS:GO

延迟补偿

15 分钟阅读 8 个章节

客户端看到的其它玩家是“若干毫秒前”的插值画面。为了让“我瞄准时看到的目标”在服务器上也能被命中,服务器在处理某个玩家的射击命令时,临时把其它实体退回到该玩家当时看到的位置,做完命中检测后再恢复。

相关页面:输入与预测 · 武器与射击 · 实体系统 · 实体句柄与列表 · 模型、动画与粒子

相关源码文件#

  • game/server/player_lagcompensation.h、game/server/player_lagcompensation.cpp
  • 调用点:game/shared/cstrike15/fx_cs_shared.cpp

1. 结构#

CLagCompensationManager 是一个 CAutoGameSystemPerFrame,因此每个服务器 tick 都会在实体 Think 之后被回调:game/server/player_lagcompensation.h:123、game/server/gameinterface.cpp:1494。

ConVar默认作用证据
sv_unlag1总开关game/server/player_lagcompensation.cpp:35
sv_maxunlag1.0 秒(范围 0–1)最大回溯时间,也决定历史保留长度game/server/player_lagcompensation.cpp:36
sv_lagcompensateself0是否回溯射击者自己game/server/player_lagcompensation.cpp:46

2. 记录历史:FrameUpdatePostEntityThink#

单人或关闭时清空历史;否则把所有玩家与额外登记的实体放入集合,为每个实体维护一条 LagRecordList 并 RecordDataIntoTrack:game/server/player_lagcompensation.cpp:141-200。RecordDataIntoTrack 先删除早于 curtime - sv_maxunlag 的旧记录:game/server/player_lagcompensation.cpp:695-715。

3. 回溯:StartLagCompensation#

  1. 清理已失效的实体条目与上一次的恢复数据:game/server/player_lagcompensation.cpp:207-242。
  2. 以下情况不做补偿:玩家关闭补偿、单人、sv_unlag=0、Bot、旁观者:game/server/player_lagcompensation.cpp:246-252。
  3. 计算目标时间:先用网络延迟加插值时间 m_fLerpTime 得到修正量并钳制到 [0, sv_maxunlag];再由命令的 tick_count 减去插值时间得到目标时间;若两种估计相差超过 200 ms,则改用基于延迟的估计:game/server/player_lagcompensation.cpp:268-299。
  4. 记录武器位置/角度/射程(缺省时用眼睛位置与角度,射程无限):game/server/player_lagcompensation.cpp:301-320。
  5. 遍历所有被补偿实体:跳过射击者本人(除非 sv_lagcompensateself),询问玩家 WantsLagCompensationOnEntity(参数包含当前命令与该玩家的实体发送位图),然后 BacktrackEntity:game/server/player_lagcompensation.cpp:322-349。

BacktrackEntity 从新到旧查找第一条不晚于目标时间的记录;遇到死亡记录或相邻记录间位移超过“瞬移”阈值时放弃回溯;找到区间时在前后两条记录之间插值:game/server/player_lagcompensation.cpp:351-420。

4. 恢复:FinishLagCompensation#

若有实体被移动,逐个用保存的恢复记录还原:game/server/player_lagcompensation.cpp:667-693。

5. CS:GO 中的调用点#

射击的共享函数 FX_FireBullets 在服务端分支中,对整批子弹(霰弹枪的多颗弹丸也在其中)只做一次 Start/Finish,并传入 LAG_COMPENSATE_HITBOXES_ALONG_RAY 与武器射程:game/shared/cstrike15/fx_cs_shared.cpp:380-383、game/shared/cstrike15/fx_cs_shared.cpp:548-577。

6. LagRecord:保存了什么,动画如何回放与恢复#

记录内容#

字段含义证据
m_fFlags历史记录中只用 LC_ALIVE;恢复/变更记录中存 LC_ORIGIN/ANGLES/SIZE/ANIMATION_CHANGEDgame/server/player_lagcompensation.h:101-102、game/server/player_lagcompensation.cpp:22-28
m_vecOrigin、m_vecAngles、m_vecMins、m_vecMaxs绝对位置、角度与世界对齐包围盒game/server/player_lagcompensation.h:104-108
m_flSimulationTime记录对应的模拟时间,回溯按它查找game/server/player_lagcompensation.h:110
m_layerRecords[MAX_LAYER_RECORDS]每个动画覆盖层的 sequence、cycle、weight、order;层数上限等于 CBaseAnimatingOverlay::MAX_OVERLAYS(15)game/server/player_lagcompensation.h:20-49、game/server/player_lagcompensation.h:112-113、game/server/BaseAnimatingOverlay.h:213
m_masterSequence、m_masterCycle主序列与其进度game/server/player_lagcompensation.h:114-115
m_flPoseParameters[MAXSTUDIOPOSEPARAM]姿态参数(上限 24)game/server/player_lagcompensation.h:117、public/studio.h:80

每个被补偿实体对应一个 EntityLagData,以 EHANDLE 为键存放在 CUtlMap 中;历史是 CUtlFixedLinkedList<LagRecord>,新记录插在表头:game/server/player_lagcompensation.h:120、game/server/player_lagcompensation.h:192、game/server/player_lagcompensation.cpp:728。下一次 StartLagCompensation 时,EHANDLE 已解析为空的条目被删除(句柄失效机制见 实体句柄与列表):game/server/player_lagcompensation.cpp:210-240。

RecordDataIntoTrack 只在实体的模拟时间比表头新时追加记录;对动画实体复制全部覆盖层、主序列/进度以及模型实际拥有的姿态参数:game/server/player_lagcompensation.cpp:717-774。三个调用点(记录、回溯、恢复)都传 wantsAnims = true:game/server/player_lagcompensation.cpp:198、game/server/player_lagcompensation.cpp:347、game/server/player_lagcompensation.cpp:691。

回溯时的取值#

状态回溯取值证据
原点、角度、包围盒在“不晚于目标时间的记录”与其后一条之间按时间比例插值game/server/player_lagcompensation.cpp:412-442
主序列/进度、覆盖层、姿态参数直接取较旧的那条记录 record,不插值game/server/player_lagcompensation.cpp:585-586、game/server/player_lagcompensation.cpp:596-597、game/server/player_lagcompensation.cpp:619-622
  • 是否回放动画取决于补偿类型:LAG_COMPENSATE_BOUNDS 不回放;LAG_COMPENSATE_HITBOXES_ALONG_RAY(CS:GO 射击使用)只对武器朝向 20° 圆锥内的实体回放:game/server/player_lagcompensation.cpp:547-572、game/server/player_lagcompensation.cpp:48-49。
  • 只要回放动画就无条件置 LC_ANIMATION_CHANGED(注释说明服务器上角色也会“呼吸”,比较所有浮点不划算),并先把当前值存入恢复记录:game/server/player_lagcompensation.cpp:574-617。
  • 原点最后才改,因为它会触发空间分区重新链接(源码注释):game/server/player_lagcompensation.cpp:538-545。
  • sv_lagflushbonecache(默认 1)时作废骨骼缓存,使后续命中盒检测按回放后的姿态重建骨骼:game/server/player_lagcompensation.cpp:37、game/server/player_lagcompensation.cpp:637-638。
  • 可选的 sv_unlag_fixstuck(默认 0)会追踪检查回溯位置是否卡进其他实体,必要时先回溯阻挡者或只回溯一部分:game/server/player_lagcompensation.cpp:42、game/server/player_lagcompensation.cpp:444-507。

恢复规则#

RestoreEntityFromRecords 只恢复带对应变更标志的部分,并在恢复任何部分时一并恢复模拟时间:game/server/player_lagcompensation.cpp:776-861。

部分规则证据
包围盒、角度只有当前值仍等于回溯写入的值(即补偿期间没被别的逻辑改过)才恢复game/server/player_lagcompensation.cpp:780-802
原点恢复为“原位置 + 补偿期间的位移”;位移达到 64 单位视为传送,保留新位置game/server/player_lagcompensation.cpp:804-816、game/server/player_lagcompensation.cpp:30
动画无条件恢复主序列/进度、全部覆盖层与姿态参数game/server/player_lagcompensation.cpp:818-855

原点恢复经 RestoreEntityTo:默认 sv_lagcompensationforcerestore 1 时先直接设回目标位置,再做卡住检测与部分回退:game/server/player_lagcompensation.cpp:40、game/server/player_lagcompensation.cpp:73-118。

约束与边界#

  • 补偿依赖命令的 tick_count 与客户端上报的插值时间;两者与真实延迟偏差过大时会退回到基于延迟的估计(第 3 步),这是一个对客户端伪造 tick_count 的限制(推断)。
  • 单次射击期间断言不允许嵌套补偿:game/server/player_lagcompensation.cpp:207。
  • 动画是“取整到记录”的:位置在两条记录间插值,而动画姿态取较旧记录,二者在一个 tick 内可能不完全一致(由第 6 节表格推出,推断其影响为命中盒相对包围盒的轻微偏差)。
  • 历史粒度:同一模拟时间只记录一次,历史长度受 sv_maxunlag 限制;实体死亡(无 LC_ALIVE)的记录会中止回溯。
  • 未展开:WantsLagCompensationOnEntity 在 CS:GO 玩家类中的具体判定、AddAdditionalEntity 的地图用法、调试叠加层 sv_showlagcompensation。