Godot 官方文档对多人游戏的建议很明确:"服务器权威"(server-authoritative)。但 set_multiplayer_authority 加 @rpc 只是权威模型的路由层,真正的难点——客户端本地预演输入、服务器驳回后和解——引擎一行代码都没写。这篇先把 Godot 给的和没给的分清楚,再把预测/和解的数学模型推一遍,最后落到具体实现结构。
Godot 给的:权威路由
Godot 给的这部分其实不少:
set_multiplayer_authority(peer_id):以节点为粒度的权威声明,配合@rpc("authority")自动校验调用方;- 服务器默认权威:整棵树归 1 号(服务器),需要玩家自主控制的角色再移交;
server_relay:客户端间 RPC 经服务器中转,服务器有机会审查每一跳;MultiplayerSynchronizer:权威→远端的状态下发管道(delta 机制见七-2)。
这套东西解决的是"谁能改哪个节点的状态"的声明问题,是个静态权限系统。它对棋牌、回合制、低频状态游戏(大部分休闲网游)已经够用——每个操作一个可靠 RPC,服务器校验后广播,完事。
Godot 没给的:延迟隐藏三件套
动作游戏(FPS、格斗、竞速)走不下去,因为 RTT 是物理事实:本地按一下跳跃,等服务器确认再动,操作延迟就是 $R$。100ms 的 RTT 意味着手感像在泥里。行业标准的答案是三件事,Godot 都没有内建:
- 客户端预测(prediction):输入立刻在本地应用,不等服务器;
- 和解(reconciliation):服务器状态回来后发现和本地预测分叉,回滚重放;
- 快照插值(entity interpolation):别人的角色不画在"现在",画在"100ms 前"(七-6 展开)。
预测与和解的数学模型
设客户端在本地 tick $t$ 应用输入 $i_t$,状态推进函数为 $f$(含物理步进,比如 move_and_slide)。预测状态下客户端自己走:
$$ \hat{S}_{t+1} = f(\hat{S}_t, i_t) $$
每个输入带本地序号 $t$ 发往服务器。服务器在自己的 tick 上应用同样的 $f$,周期性回传确认:最后处理的输入序号 $k$ 和权威状态 $S_k$。
客户端收到确认时,本地可能已经预测到 $t \gg k$。和解就是:以服务器确认的 $S_k$ 为基点,重放从 $k$ 到 $t$ 的全部已发输入:
$$ \hat{S}_t = f\big(f(\dots f(S_k, i_{k+1})\dots, i_{t-1}), i_t\big) $$
如果 $f$ 和服务器端完全一致、输入序列一致,重放结果和服务器终将到达的状态逐位相同,客户端的"提前量"毫无代价。这是 lockstep 理想。现实里误差有两个来源:
- $f$ 不一致:本地物理和服务器物理走了不同分支(浮点非确定性、不同物理引擎版本)。动作游戏的常见对策是服务器只发位置/速度校正值而不是追求逐位一致:和解时把 $S_k$ 直接替换为服务器值,再重放后续输入,误差被"冲掉"而不是累积。误差项 $E_k = \|S_k^{client} - S_k^{server}\|$ 通常在毫米级,重放吸收;撞上障碍物这类离散分歧才需要硬校正(瞬移或 Rubber banding);
- 输入丢包:可靠通道保证最终到达,但 $k$ 确认滞后意味着本地预测窗口 $[k, t]$ 里任何一次丢包都让后续全部预测建立在未确认的沙地上。窗口长度取决于 RTT:$w = \lceil R \cdot f_{tick} \rceil$ tick。30Hz tick、150ms RTT 下窗口约 5 tick——要重放的状态和输入都得留这么长的环形缓冲。
落地到 Godot 的结构
一个能跑的最小结构:
- 本地先行:输入在
_physics_process里先应用到本地角色,再把(input, tick)用可靠 RPC 发服务器——同一个 tick 内"本地应用 + 外发",保证服务器输入序列和本地一致; - 输入缓冲:客户端环形数组存最近 $w + margin$ 个 tick 的输入;服务器确认到 $k$ 后丢弃 $\le k$ 的输入;
- 状态快照:服务器每个物理 tick 把权威角色的 (pos, vel, tick) 用 MultiplayerSynchronizer 或裸 RPC 下发;
- 和解点:收到快照且快照 tick > 上次和解 tick 时,保存当前预测状态 → 用快照状态重放缓冲输入 → 若新旧差异超阈值(比如 0.5m)则硬对齐并触发表现层过渡,否则静默采用重放结果。
表现层和逻辑层分离在这里是硬性要求:逻辑状态可以瞬间回滚重放,表现层(动画、相机)必须平滑——常见做法是给表现层一个追向逻辑状态的 spring,逻辑层和解跳变被 spring 吸收。
物理确定性的边界
和解重放要求本地 $f$ 和服务器 $f$ 至少弱一致(短时间分叉能被快照纠正)。Godot 物理的确定性问题在物理分类那几篇专门讨论过(四-19:Jolt 的确定性结论是不保证逐位确定性),这里只给一个实践结论:预测窗口短(5 tick)时,浮点分叉在几 tick 内不会长到用户可感,快照回滚兜得住;想搞输入回放的 lockstep(不存状态只存输入),Godot 物理目前的确定性水平撑不住,需要固定点数学或自己上确定性物理。
总结分工
Godot 给的是权威路由和传输,是这套系统的"信使";预测、和解、插值是"翻译",要你自己写。好消息是结构高度模式化——这篇的公式和环形缓冲就是全部核心,剩下的都是工程。写之前先看一遍官方 high_level_multiplayer 文档的 "Master/Slave" 迁移说明(4.x 改名 authority 那波),别在过时教程的术语里打转。
本文不涉及具体引擎源码行号(预测非引擎内建),状态模型参考经典文献 "Source Multiplayer Networking"(Valve)与 Gabriel Gambetta 的 client-server game networking 系列。