回滚网络同步是“预测+回退”机制,即先基于假设输入向前模拟若干帧,发现错误时回滚重算;而普通帧同步要求所有客户端严格按序等待每帧输入,无法容忍丢包或延迟。

什么是回滚网络同步,它和普通帧同步有什么区别
回滚网络同步不是“等所有输入都到了再模拟”,而是“假设对方输入已知,先往前跑几帧;如果后来发现猜错了,就倒回去重算”。这直接对抗网络延迟带来的操作卡顿,但代价是逻辑必须确定性、且能快速 rewind 和 replay。
普通帧同步(如 lockstep)要求所有客户端严格同步每帧输入,一丢包就得等或断连;而回滚允许 1~3 帧的预测窗口(通常叫 rollback_depth),靠本地输入 + 历史输入缓存 + 确定性模拟来掩盖延迟。
关键前提有三个:
- 所有客户端的模拟逻辑必须 100% 确定性(
rand()不行,浮点运算需禁用 FMA/控制舍入) - 输入必须打上精确帧号(
input_frame),且每个客户端维护一个固定长度的输入缓冲区(如std::array<input>) - 每帧模拟前,要检查远端最新确认的输入是否与本地“预测”一致;不一致就触发 rollback
怎么实现确定性模拟:C++ 里最容易翻车的几个点
确定性不是“写得一样就行”,而是“在任何 CPU、编译器、优化等级下输出完全一致”。C++ 里常见崩点:
立即学习“C++免费学习笔记(深入)”;
-
std::rand()和std::random_device都不保证跨平台确定性,必须用自实现 LCG 或 xorshift(如xorshift128+),种子固定且全程不依赖系统时间 - 浮点数:
float运算在 x86 vs ARM、/fp:fast vs /fp:precise 下结果可能不同;推荐全程用int或定点数(如int32_t表示 1/65536 单位),或强制#pragma float_control(precise, on)+ 禁用 FMA 指令 - STL 容器迭代顺序不保证(尤其
std::unordered_map),遍历前必须排序 key 或改用std::map -
std::chrono::steady_clock::now()只用于测时,绝不能参与逻辑计算;帧调度必须基于本地帧计数器(int frame_counter)
一个最小可验证的确定性帧函数长这样:
void simulate_frame(int frame_num, const Input& local_input, const Input& remote_input) {
// 所有状态更新只读取 input[frame_num],不调用 time()、rand()、new 等非确定性源
player.x += local_input.x_axis * PLAYER_SPEED;
player.y += local_input.y_axis * PLAYER_GRAVITY; // 注意:这里不能用 sin/cos 查表,除非查的是预生成的 int 表
}
怎么组织输入缓冲与 rollback 触发逻辑
核心结构是一个环形输入缓冲区,每个客户端为对方维护一份:
struct InputBuffer {
std::array<Input, 12> data;
int head = 0; // 最新收到的帧号对应索引
int confirmed = 0; // 对方已确认不会变更的最老帧号(即该帧及之前都可信)
};
每帧开始时,你拿 local_input[local_frame] 和 remote_input[local_frame - rollback_depth] 去模拟——注意:你永远用比当前帧早 rollback_depth 的远端输入做“保守预测”。
当收到新的网络包,更新 remote_input 后,检查从 confirmed + 1 到当前帧之间是否有不一致:
- 如果
remote_input[i] != predicted_input[i],说明预测失败,需要 rollback 到i帧,然后用真实输入重放i到当前帧 - rollback 不是“删内存”,而是把状态数组(
std::array<state></state>)按帧号存好,或实现State::rewind_to(int frame)
常见错误:
- 把
rollback_depth设成 0 或 1:太小扛不住抖动,建议从 3 开始调 - 在 rollback 重放时没清空中间产生的临时效果(如音效、粒子),导致视觉重复
- 没限制最大 rollback 深度(比如连续 10 帧错),应设硬上限并 fallback 到插值或暂停
怎么调试回滚逻辑是否真在工作
最有效的办法不是看画面,而是打日志+断言:
- 每帧记录
frame_num、local_input、remote_input_used、is_rollback_triggered - 在
simulate_frame()开头加assert(state_hash() == expected_hash[frame_num]),hash 用简单 XOR 累加关键字段(x/y/health/frame) - 用确定性网络模拟器(如
netem在 Linux 下加 80ms 延迟 + 5% 丢包)测试,观察 rollback 触发频率是否合理(理想是
真正难的不是写 rollback,而是让整个游戏逻辑适配它:动画状态机要支持“倒带一帧”,碰撞检测要能复用旧 AABB 而不重新分配内存,输入延迟显示(input delay indicator)要实时反映当前 rollback 深度。这些细节不暴露在主循环里,但一出问题就表现为“偶尔闪退”或“两人动作突然不同步”。



















