跳转至

状态同步与帧同步

先确定谁裁决

第一次做联机时,最先纠结的是“选状态同步还是帧同步”。后来发现这个问题问得太早。真正要先确定的是:谁来裁决结果,客户端能容忍多少延迟,以及是否需要回放。

状态同步和帧同步只是两套常见的组织方式,实际项目里也经常混着用。1

状态同步

服务器保存权威状态,定期向客户端发送快照或增量。客户端收到后更新画面,本地角色可以提前预测。

客户端:发送输入 { tick, move, fire }
服务器:模拟,得到 tick 100 的权威状态
服务器:广播快照
客户端:应用快照,重放尚未确认的输入

快照里通常不发送完整场景,而是只发玩家附近、当前可见、优先级高的实体。位置和旋转也会量化,减少包体。2

public readonly struct PlayerSnapshot
{
    public readonly ushort PlayerId;
    public readonly int Tick;
    public readonly Vector3 Position;
    public readonly ushort Yaw;
    public readonly ushort Health;
}

位置量化得太粗会抖动,瞄准方向量化得太粗会有台阶感。量化误差要控制在玩法允许的范围内,不能只看带宽。

帧同步

帧同步一般只传输入,客户端按相同规则在本地推进模拟。只要初始状态、输入顺序、随机数和浮点结果一致,各端就会得到相同结果。

第 N 帧:
  收集所有玩家输入
  按固定顺序执行模拟
  计算状态哈希,和其他玩家比较

这种方式带宽低,适合单位数量多、输入频率稳定的 RTS 或格斗游戏。代价是确定性要求高,尤其是浮点运算、物理引擎和随机数。

常见做法是使用定点数,给随机数固定种子,并且每帧按固定顺序处理输入。即使如此,不同平台、不同编译选项也可能产生细微差异,所以需要状态哈希和断线重连方案。

怎么选

维度 状态同步 帧同步
权威方 服务器 输入共识或服务器转发
带宽 跟状态复杂度有关 通常较低
客户端预测 常用 本地直接模拟
回放 需要记录快照或输入 天然适合
反作弊 服务器可直接裁决 更依赖校验和监控
跨平台一致性 较容易 很难

如果是 FPS、MMO,或者需要服务器裁决大量战斗结果的实时对战,通常更适合状态同步。服务器可以直接处理伤害、命中和物品变化,客户端作弊空间相对小一些。

如果是 RTS、格斗、部分体育游戏,或者回放需求很强的项目,帧同步更容易控制带宽。但前提是团队能控制确定性。

客户端预测

状态同步如果完全等服务器回包,本地移动会有明显延迟。常见做法是保存未确认输入,先本地模拟,再在收到权威状态后重放:

预测状态 = 服务器确认状态
for 每个未确认输入:
    预测状态 = Simulate(预测状态, 输入)

如果预测和服务器结果差得不多,可以平滑修正;差得太多,直接拉到服务器位置更稳。这里不要在两种策略之间反复切换,否则玩家会看到角色来回抽搐。

多留一份输入记录

联机调试时,把每个 Tick 的输入和关键状态存成固定长度的环形 Buffer。出现不同步时,直接回放到出问题的 Tick,比在日志里猜快很多。

状态同步还可以额外记录服务器快照和客户端预测值。这样能区分是输入丢了、预测逻辑不一致,还是插值参数设置不对。