实时对战的网络同步:业界做法 vs 我们现在的做法
起因:老大提了一个判断 —— "不应该是跟后端通讯一次再变化,而应该是前端正常变化、后端做验证。 这样是不是能更好地解决动作卡顿?不然服务器受性能限制,人一多就会卡。" 结论:方向完全正确,而且移动部分我们本来就是这么做的;但卡顿的根因不是"等服务器确认", 而是同步的平滑度和广播量的 O(N²) —— 后者才是"人一多就卡"的真凶。 本文给出业界对照、实测数据与改进路线。 状态:本文描述的优化已落地并稳定运行;文中的"积分"字样是当时的口径, 积分系统已于 2026-10-15 整体下线,现在对应的字段是kills(NPC 击杀记stats.npcKills)。 协议现状见 接口与协议。
一、先回答老大的两个判断
判断 1:「前端先变、后端验证」→ 对,这是业界标准架构(叫客户端预测)。
但要说清楚:我们的移动本来就是这样。
| 环节 | 我们现在的做法 | 是否已是"预测" |
|---|---|---|
| 自己移动 / 转向 | 本地 cannon 物理立即算,不等服务器 | ✅ 是 |
| 自己开火(甩纸飞机) | 本地立即播动作 + 生成实体,不等服务器 | ✅ 是 |
| 自己撞树 / 落水 | 本地判定 + 本地反馈 | ✅ 是 |
| 别人被打中掉血 | 服务端结算后回推 | ❌ 故意不预测(防作弊) |
| 击杀 / 死亡 / 战绩 | Server-authoritative | ❌ 故意不预测(防作弊) |
也就是说:卡顿不是"等服务器确认"造成的 —— 你自己跑起来的那一刻就已经动了。 真正让画面顿的是下面三件事(本轮已修):
- NPC 位置精度不够:服务端广播用 3 位小数 → 球面上 0.82 单位的台阶,
而车每 100ms 才走 2 单位 —— 台阶比步长还大四成;
- 5Hz 阶梯输入 + 纯插值:目标每 200ms 跳一次,速度就跟着周期性快-慢-快-慢;
修法是速度外推(用位置差分估速度,把目标推到"现在应该在的位置");
- 远程玩家的动作压根没同步:客户端克隆只播 idle 呼吸,别人看不到你跑/飞/开火,
也没有环绕无人机和飞出去的纸飞机。本轮把动作状态从位置变化里反推出来(不用加网络字段), 攻击靠一条 fire 广播触发。
判断 2:「服务器受性能限制,人一多就卡」→ 完全正确,而且我实测了。
罪魁是 state 中继:每个玩家的状态要转发给其他所有人,10Hz —— 这是 O(N²)。
| 在线人数 | 下行消息/秒(优化前) | 下行带宽(优化前) |
|---|---|---|
| 30 人 | 9760 条/秒(≈30×30×10) | 22.7 Mbps |
| 50 人(按斜率推算) | ≈25000 条/秒 | 约 63 Mbps |
两个人对打时这点量无所谓,人数一上来就是平方增长,服务器上行先爆。 这跟"要不要预测"无关 —— 是广播范围没管。
二、业界标准做法(四种技术,缺一不可)
1. 客户端预测 + 服务器校正(Client-Side Prediction / Reconciliation)
- 客户端按键立刻用和服务器同一套逻辑本地模拟,玩家看到即时反馈;
- 同时把输入(带序号)发给服务器,服务器按同样的逻辑算一遍,回传"权威状态 + 已处理到第几号输入";
- 客户端拿权威状态对比自己的预测:一致就继续;不一致就回滚到权威状态,把还没被确认的输入重放一遍;
- 关键:位置校正要平滑(100~200ms 内插值过去),不能硬 snap,否则就是"橡皮筋"。
出处:webgamedev.com、 backbonetutorials
2. 远程实体的插值 / 外推(Interpolation / Dead Reckoning)
- 别人的角色你没法预测他的输入,所以:服务器发快照,客户端缓冲 100ms 再插值渲染;
- 或者用
dead reckoning:按"最后已知速度 + 方向"外推他在哪(我们这轮给 NPC 和远程玩家加的就是这个); - 代价:外推太久会在真实数据到达时"弹一下",所以要限制外推窗口(我们取 0.45 秒)。
3. 兴趣管理 + 降频(Interest Management / Relevancy)
- 只把每个客户端真正需要的状态发给他:按距离、可见性、玩法重要性排优先级;
- 高优先级(身边的敌人)按满帧率发;低优先级(远处队友)每 3~5 帧发一次。
- 这是解决 O(N²) 的正解 —— 我们这轮做的就是这一条。
Search结果原文:"Broadcasting full world state to every client every tick, causing bandwidth to scale with the square of player count and collapsing at any meaningful scale." —— 正好是我们犯的毛病。
4. 延迟补偿(Lag Compensation)
- 服务器保存一小段历史命中盒,收到"我在 t 时刻开了枪"时,回滚到那个时刻判定命中;
- 这样高延迟玩家也能打中他当时看到的目标。
- 我们还没有(原型阶段命中判定比较简单:客户端报
hit+ 目标 id)。
三、大厂的真实参数(破除"必须 128 tick"的迷思)
| 作品 | 服务器 tick | 客户端 | 关键做法 |
|---|---|---|---|
| Overwatch | 20Hz | 60fps 本地预测 | 每个连接一份 dirty set + 增量编码;ping > 220ms 时关闭命中预测;ECS 架构便于回滚 |
| Valorant | 128Hz | 预测 + 回滚 | 竞技向:用带宽换公平性 |
| CS2 | 64Hz | 预测 | 延迟补偿(服务器回滚判定) |
| 格斗作品(GGPO) | — | Rollback | 只传输入(每帧 5~20 字节),本地确定性重演 |
| Web 实时作品通用 | 10~30Hz | 插值缓冲 100ms | 兴趣管理 + 量化 |
注意 Overwatch 只有 20Hz(我们 NPC 位置是 5Hz 广播、玩家状态 10Hz), 它看起来流畅靠的是预测 + 插值 + 增量编码,不是"服务器很快"。
出处:GDC 2017 Overwatch Gameplay Architecture and Netcode 翻译、 Phil Orwig:回放与同步(dirty set / 增量编码)
四、本轮已落地的优化(P0)
1. state 中继改成"按距离分频"(新增 relayState())
- 近处(800 单位内):每条都发 —— 走路、开火、擦身而过要看得清;
- 远处:每 3 条发 1 条(3.3Hz)—— 够画雷达上的"边缘方向指示",省 70% 带宽。
- 800 这个距离是照着客户端的实际用途定的:名牌只显示 900 内、雷达 380 内,
但雷达的"边缘指示"需要知道远处玩家还存在,所以远处不能一刀切砍掉 —— 降频而不是丢弃。
2. 位置精度 3 位小数 → 5 位
3. 客户端速度外推(EXTRAPOLATE_MAX = 0.45s,必须盖得住最稀疏的同步间隔)
4. 远程玩家动作同步(跑/走/飞/攻击)+ 环绕无人机 + 飞出去的纸飞机
实测(30 个傀儡,同机压测)
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 下行消息/秒 | 9760 | 3794 | −61% |
| 下行带宽 | 22.7 Mbps | 9.2 Mbps | −60% |
| HTTP 探活 p50 | 20.4 ms | 10.2 ms | −50% |
| HTTP 探活 p95 | 24.9 ms | 17.5 ms | −30% |
按斜率推算 50 人:63 Mbps → 约 23 Mbps。
五、还没做的(按性价比排序)
| 优先级 | 项 | 预计收益 | 成本 |
|---|---|---|---|
| P1 | 聚合快照:把"所有玩家位置"每 tick 打包成一条广播,替代逐条中继 | 消息数再降 3~5 倍(小包合并) | 中(要改协议 + 客户端解析) |
| P1 | 位置量化:方向向量用 int16 定点数(现在 5 位小数每轴约 9 字节 → 2 字节) | 带宽再降 50%+ | 低 |
| P2 | 延迟补偿:服务器保存历史命中盒,按客户端时间回滚判定 | 高延迟玩家的命中更公平 | 中高 |
| P2 | 多进程/分片:跨区域分服 | 单机容量上限提升 | 高(要加网关) |
| P2 | NPC 位置改为"关键帧 + 确定性重演":服务端只发"每台车的巡航参数",客户端各自算 | NPC 带宽降到接近 0 | 高(要保证前后端浮点一致) |
六、有一条不能改成"前端自己算"
老大的思路适用于自己的角色,但共享实体(NPC 车、障碍物摧毁、别人的血量)必须服务器权威:
- 50 台车是所有玩家共享的目标:A 打爆一台,B 那边也必须看到它消失;
如果各自本地模拟,两人看到的车队位置和存活状态会越飘越远;
- 而且"谁打中了谁"直接关系到击杀统计(
kills/stats.npcKills),客户端说了算等于敞开作弊(改个客户端就能刷战绩)。
正确的分工(也就是业界共识):
客户端只负责"我自己的输入与表现",服务器负责"世界真相"。
我们的实现正好落在这条线上 —— 移动和攻击表现是本地即时,伤害 / 击杀统计 / 存活是服务端说话。
七、一句话总结
- 卡顿:不是"等服务器",而是位置精度、阶梯插值、动作没同步 → 本轮全修;
- 人一多就卡:是
state广播的 O(N²) → 本轮降了 60%,还有聚合快照 + 量化两招能继续压; - 架构方向:老大说的"前端先变、后端验证"就是业界的客户端预测 + 服务器权威,
我们已经在正确的路上,缺的是兴趣管理(已做)和延迟补偿(P2)。