延迟补偿
客户端看到的其它玩家是“若干毫秒前”的插值画面。为了让“我瞄准时看到的目标”在服务器上也能被命中,服务器在处理某个玩家的射击命令时,临时把其它实体退回到该玩家当时看到的位置,做完命中检测后再恢复。
相关页面:输入与预测 · 武器与射击 · 实体系统 · 实体句柄与列表 · 模型、动画与粒子
相关源码文件#
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_unlag | 1 | 总开关 | game/server/player_lagcompensation.cpp:35 |
sv_maxunlag | 1.0 秒(范围 0–1) | 最大回溯时间,也决定历史保留长度 | game/server/player_lagcompensation.cpp:36 |
sv_lagcompensateself | 0 | 是否回溯射击者自己 | 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#
- 清理已失效的实体条目与上一次的恢复数据:
game/server/player_lagcompensation.cpp:207-242。 - 以下情况不做补偿:玩家关闭补偿、单人、
sv_unlag=0、Bot、旁观者:game/server/player_lagcompensation.cpp:246-252。 - 计算目标时间:先用网络延迟加插值时间
m_fLerpTime得到修正量并钳制到[0, sv_maxunlag];再由命令的tick_count减去插值时间得到目标时间;若两种估计相差超过 200 ms,则改用基于延迟的估计:game/server/player_lagcompensation.cpp:268-299。 - 记录武器位置/角度/射程(缺省时用眼睛位置与角度,射程无限):
game/server/player_lagcompensation.cpp:301-320。 - 遍历所有被补偿实体:跳过射击者本人(除非
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_CHANGED | game/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。