跳转至

延迟补偿与回滚

先看时间差

“我明明打中了,为什么没伤害?”这个问题通常不是服务器没收到射击,而是客户端、服务器和目标位置对不上时间。

玩家在本地看到敌人站在门口,按下开火。输入上行要花几十毫秒,等服务器处理时,敌人已经往旁边走了一步。如果服务器直接按当前状态判定,这一枪就会被判空。

用时间轴看清楚

客户端看到敌人在 P(t)
  ↓ 输入上行 50 ms
服务器在 t + 50 ms 收到射击
  ↓
敌人已经移动到 P(t + 50)

延迟补偿的做法是:服务器收到射击后,回退到玩家当时看到的世界状态,再用历史碰撞体做判定。1

保存最近的状态

服务器需要保留一小段时间的碰撞体或角色位置。可以用环形 Buffer,按 Tick 覆盖最旧的数据:

public sealed class SnapshotHistory
{
    private readonly Snapshot[] _buffer;
    private readonly int _capacity;

    public SnapshotHistory(int capacity)
    {
        _capacity = capacity;
        _buffer = new Snapshot[capacity];
    }

    public void Record(in Snapshot snapshot)
    {
        _buffer[snapshot.Tick % _capacity] = snapshot;
    }

    public bool TryGet(int tick, out Snapshot snapshot)
    {
        snapshot = _buffer[tick % _capacity];
        return snapshot.Tick == tick;
    }
}

实际项目里,位置和朝向不需要每个 Tick 存一份完整快照。可以只记录受影响实体的碰撞体、速度和关键状态,再在查询时插值。

命中判定的流程

  1. 客户端发送射击输入:方向、时间戳、客户端 Tick、武器状态。
  2. 服务器根据收到命令的时间和 RTT 估算玩家当时看到的时间,不要直接相信客户端传来的任意时间。
  3. 从历史状态里取出对应 Tick,并做插值。
  4. 在世界状态上执行射线或弹道检测。
  5. 再用当前状态检查目标是否仍然存活、是否在合法视野内。
  6. 广播命中结果,客户端播放反馈。

简化后的判定代码:

public HitResult ResolveHitscan(
    ShotCommand command,
    SnapshotHistory history,
    int rewindTick)
{
    if (!history.TryGet(rewindTick, out var past))
        return HitResult.RejectedOutOfWindow;

    var ray = new Ray(command.Origin, command.Direction);
    return PhysicsLayers.Raycast(past.Colliders, ray, command.MaxDistance);
}

rewindTick 由服务器计算,大致是当前 Tick 减去半程 RTT 和插值延迟,最后再夹到允许的回溯窗口里。这里的 PhysicsLayers.Raycast 只是示意,实际项目可以用物理查询、扫掠检测或自己的碰撞结构。关键是查询的对象来自历史状态,不是当前状态。

回溯窗口不能无限大

窗口越大,高延迟玩家越容易命中,但服务器需要保存更多历史状态,回放成本也更高。2 常见范围在 100–300 ms,具体要看服务器 Tick、最大 RTT 和玩法节奏。

窗口 影响
太小 高延迟玩家经常打空
太大 回放成本上升,作弊空间变大
没有上限 不公平,也不适合线上运营

回溯还要限制时间戳偏差、射速、弹药、视线和目标存活状态。否则客户端可以伪造一个很早的时间点,去打当时还没有出现的目标。

回滚与重模拟

如果客户端做了本地预测,收到权威状态后要重放未确认输入。3 格斗或高频物理游戏里,这个过程可能涉及角色碰撞和动画事件,不能只改位置。

一个可用的调试方式是记录:

  • 服务器确认的 Tick
  • 客户端当时的预测状态
  • 重放后的状态
  • 两者误差

这样出现“回滚后穿墙”或“命中后又被拉回”时,能直接定位是哪一步模拟不一致。

命中日志怎么记

把每次命中的回溯 Tick、玩家 RTT 和最终判定位置写进调试日志。只在训练场测不够,最好用不同延迟和丢包组合跑一遍,观察命中率是否随网络条件明显偏斜。

如果某档延迟的命中率突然高很多,通常不是补偿写得好,而是时间估算或窗口限制出了问题。