第20章 · 网络原理速成:延迟、同步与拓扑

📑 目录

导语:「远港 Online」第一次内部测试,一位策划在公屏刷屏:我明明滚出了那支箭的落点,为什么还是死了?查了一晚上日志,结论是他本地确实躲开了,服务器那边他却还站在原地——中间隔着 110ms 的往返和 100ms 的插值缓冲。那一晚我意识到,团队里一半人把复制系统当黑魔法用,而黑魔法迟早要收利息。这一章不交利息,先把本金讲清:延迟从哪来,真相放哪里,以及为什么 MMO 在网络模型上根本没有选择权。

本章地图

mindmap
  root((网络原理速成))
    网络三害
      延迟 RTT:距离与光速
      抖动 Jitter:WiFi 的原罪
      丢包 Loss:TCP 放大器
    同步模型
      帧同步:带宽 O(N²)
      状态同步:MMO 唯一解
    权威归属
      客户端权威=作弊温床
      服务器权威=唯一真相
    时间三件套
      插值:多看 100ms
      外推:猜错就瞬移
      延迟补偿:替射手回溯
    拓扑选型
      P2P
      Listen Server
      Dedicated Server
      分布式 Zone

网络三害有三个必须背下来的数字

写单机的时候,时间是你的朋友:每一帧 16.6ms,输入进来,画面出去,因果闭合成环。联机之后,时间变成债主。延迟、抖动、丢包这三样东西,每一个都足以让一套设计精美的同步方案当场散架,而它们的量级直接决定了后面所有架构决策。

先说延迟(Latency),工程上统一用 RTT(Round-Trip Time,往返时间)记账,因为游戏里的每一次请求-确认都走一个来回。RTT 的物理下限是光速:光纤里信号传播速度约为真空中光速的三分之二,也就是每公里约 5 微秒。听起来很快,但太平洋海底光缆一个来回就是一亿多米的路程,物理极限摆在那,谁也优化不掉。

下面是国内玩家常见的实测量级(光纤入户、晚高峰、跨网段取偏保守值):

链路RTT 量级对游戏的含义
同城同运营商1–10ms格斗游戏级,几乎无感
同城跨运营商15–40ms竞技射击可接受
跨省(北京—上海)25–50ms主流网游常态
国内—美西130–180msMMO 能玩,射击痛苦
国内—欧洲160–250ms必须靠预测与补偿硬扛

150ms 是个心理分界线:低于它,玩家把延迟归因于网络;高于它,玩家把延迟归因于你的游戏。跨国部署「远港 Online」就意味着一个技能从按下到服务器确认,乐观也要 200ms 上下,这个数会反复出现在后面每一节的推导里。

再说抖动(Jitter),即延迟的方差。平均延迟决定你慢多少,抖动决定你能不能预测这个"慢"。有线宽带的抖动通常在 1ms 以内,调度算法喜欢这种链路。WiFi 是另一回事:空闲时 2–10ms,信道拥挤或隔壁开了微波炉时,单包尖刺冲到 50–100ms 不稀奇。移动网络介于两者之间,4G 常态 10–50ms 抖动,5G 好一些但切换基站时同样会抽风。抖动无法消除,只能吸收——第四节讲的插值缓冲,本质上就是拿固定延迟换抖动免疫。

最后是丢包(Packet Loss)。UDP 本身不保证送达,1% 丢包听起来无害,放到具体场景里算一笔账:服务器以 20Hz 发快照,1% 丢包意味着每 5 秒丢一帧快照,插值缓冲吃得下;但如果你的关键事件(技能释放、拾取确认)也裸奔在 UDP 上不做可靠性处理,每 5 秒就有一次"我按了没反应"。TCP 更糟——它是丢包的放大器。让它治丢包?那只是错觉:丢一个包,后面所有包排队等重传,队头阻塞让延迟瞬间飙到几百毫秒。吞吐量的经典近似公式是:

TTCPMSSRTTpT_{TCP} \approx \frac{MSS}{RTT \cdot \sqrt{p}}

其中 MSSMSS 是单包大小,pp 是丢包率。pp 从 0.01% 涨到 1%,p\sqrt{p} 放大 10 倍,吞吐直接砍到十分之一。这就是联机游戏主通道一律走 UDP、自己造选择性重传的原因,也是 UE 的 NetDriver 栈站在 UDP 之上的原因。

三害还有一个合体形态值得单独提防:延迟尖刺(Lag Spike)。均值变差?不,它是偶发的单包几百毫秒延迟,来源五花八门——WiFi 重传、路由切换、运营商拥塞、家里有人开始刷视频。均值 30ms 的链路可能每十秒来一次 400ms 尖刺。对状态同步来说,尖刺表现为快照断供:插值缓冲被抽空,外推预算顶满,然后冻结或瞬移。所以评估链路质量只看平均 Ping 是不够的,必须看 P95、P99 分位。「远港 Online」的链路监控指标里,P99 延迟权重比均值还高——均值决定手感,尾部分位决定事故率。

顺带澄清一个常见误解:选 UDP 实际上是把可靠性拆成可选项,而非放弃可靠性。UE 的复制系统里,属性复制走"最新值覆盖旧值"的语义,丢一个旧快照无所谓,下一个快照自带新状态,天然免疫丢包;而 RPC 可以声明为 Reliable,引擎在 UDP 之上自己做确认与重传——但因为重传会占通道,Reliable RPC 滥用会把带宽烧穿,这是第 21 章要算的账。原则先立在这:状态类信息容忍丢失,事件类信息必须可靠,两条通道的脾气完全不同。

给你一个可执行的直觉训练:今晚就用 ping 打三个目标——同城市的服务器、跨省节点、境外节点,各打 200 个包,看平均值、标准差、丢包率。三个数字各差一个数量级的体验,比读十篇文章管用。

帧同步的账算完,MMO 只剩状态同步一条路

同步模型只有两大流派。帧同步(Lockstep):服务器只转发输入,所有客户端各自跑完全确定的模拟,大家输入相同、结果必然相同。状态同步(State Replication):服务器跑唯一的模拟,把世界状态的快照或增量发给客户端,客户端只负责显示。

帧同步的优点确实诱人:带宽极省(只传输入)、回放天然、观战天然。 RTS 和格斗游戏爱用它,不是没道理。但它有三条刻在基因里的约束,条条掐在 MMO 脖子上。

约束一,带宽随人数平方增长。每个客户端要拿到全员的输入,服务器出口带宽是:

Bserver=N2sfB_{server} = N^2 \cdot s \cdot f

NN 是玩家数,ss 是单帧输入大小,ff 是锁帧频率。取一组对帧同步相当友好的参数:s=32s = 32 字节(按键掩码加两个浮点朝向),f=30f = 30Hz。10 人小战场,服务器出口约 96KB/s,岁月静好。100 人团战,出口涨到 9.6MB/s(约 77Mbps),开始肉疼。1000 人同屏的 MMO 战场,出口约 960MB/s,接近 8Gbps——这还只是输入广播,世界状态一个比特都没传。天文数字不是说说的。

约束二,确定性是个瓷器活。浮点运算顺序、物理引擎版本、编译器优化、甚至数学库的平台差异,都会让"相同输入"在不同机器上跑出不同结果。RTS 用定点数加统一物理库勉强压住,而 UE 的 Chaos 物理和整套浮点管线从未承诺跨平台确定性,在 5.8 里依然如此。一个 MMO 客户端要在 Windows、主机、移动端全部保持逐比特一致,等于给每个平台套绞索。

约束三,最慢的玩家决定所有人的节奏。Lockstep 要求全员到齐才能推进下一帧,一个人卡住 500ms,99 个人陪他看幻灯片。MMO 里玩家随时进出、跨区迁移、弱网挂机,这个模型从第一天就不成立。

确定性的坑值得再展开半页,因为几乎每个转型团队都在这里交过学费。假设两个客户端同时模拟一次火球爆炸:A 机器上 cos(1.0000001) 和 B 机器上 cos(1.0) 在第七位小数分叉,弹片溅射角度差一丝,五秒后两只哥布林的站位差半米,十秒后两边的战斗结果完全不同——模拟已经不可逆地分道扬镳,而帧同步协议里没有任何机制能发现或修复这种分叉,除了定期校验哈希然后绝望地踢人。单机玩家不在乎,因为单机只有一个世界;帧同步把"全世界只有一个世界"的责任摊给了每台客户端,而浮点硬件根本担不起这个责任。

状态同步把账全翻过来:带宽随"你看得见多少实体"线性增长,与全服总人数无关;客户端不需要确定性,渲染即所得;任何人掉线只影响自己。代价是真相只在服务器,客户端看到的永远是旧世界——这正是后面两节要处理的麻烦。还有一个常被忽略的收益:状态同步允许不同客户端收不同精度的世界。远处的怪给你 5Hz 低清快照,脸上的怪给你 20Hz 高清快照,带宽花在刀刃上。帧同步做不到这种分级——所有人必须收全量输入,没有"远处模糊"这个选项。这个能力对应 UE 里的相关性(Relevancy)与更新频率分级,第 21 章会把它拆成可调参数。

quadrantChart
    title "同步模型:一致性 vs 响应速度"
    x-axis "响应慢" --> "响应快"
    y-axis "一致性弱" --> "一致性强"
    quadrant-1 "理想区(代价在带宽)"
    quadrant-2 "学术区"
    quadrant-3 "不可用区"
    quadrant-4 "手感区(代价在作弊面)"
    "帧同步": [0.3, 0.9]
    "纯客户端权威": [0.9, 0.2]
    "服务器权威+插值": [0.45, 0.85]
    "服务器权威+预测回滚": [0.75, 0.8]

图上没有免费的格子。帧同步占住强一致性但响应被最慢者拖死;纯客户端权威手感无敌但等于没有真相;MMO 能选的只有右半边那两种服务器权威方案,用客户端预测把响应速度买回来(预测的细节见第 23 章)。

真相只能放服务器:客户端权威等于给外挂开门

早期网游干过把伤害数值交给客户端上报的事。结局毫无悬念:内存修改器一抓一把,伤害数字随便填,运营半年就死在工作室手里。教训浓缩成一条行业公理:客户端发来的每一个字节,服务器只当它是"声称"。事实得服务器自己算——客户端说"我打中了",服务器必须自己算一遍到底中没中。

服务器权威(Server Authoritative)模型的分工是:客户端发意图(我要朝这个方向移动、我要释放这个技能),服务器做裁决(你能不能移、技能中没中、掉多少血),然后把裁决结果作为唯一真相广播回去。客户端本地显示的世界,只是真相的一份延迟副本——行话叫影子世界。

补课:分布式系统的 CAP 直觉。CAP 说的是网络分区(P)必然发生时,一致性(C)和可用性(A)只能二选一。游戏的答案很统一:选 A,然后靠"单一真相源加最终一致"来补救。服务器权威世界就是 Git 里的 main 分支,所有客户端都是本地 fork,可以暂时分叉(预测),但最终必须 rebase 回 main(服务器校正)。MMO 里跨 Zone 的边边角角允许短暂不一致,但每个实体在任意时刻只有一个进程说了算。这套思路会在第 25 章的架构拆分里反复出现。

服务器收到一个技能请求后,裁决是一条校验链,任何一个环节不过就驳回:

flowchart TD
    A[收到技能释放请求] --> B{玩家存活且未被控制?}
    B -- 否 --> R1["驳回: 状态非法"]
    B -- 是 --> C{冷却结束且资源足够?}
    C -- 否 --> R2["驳回: 消耗校验失败"]
    C -- 是 --> D{目标在射程与视线内?}
    D -- 否 --> R3["驳回: 目标非法"]
    D -- 是 --> E[延迟补偿回滚目标位置]
    E --> F{命中判定通过?}
    F -- 否 --> G["广播: 未命中表现"]
    F -- 是 --> H[结算伤害/效果]
    H --> I["广播: 命中结果与状态增量"]
    I --> J["回执给施法者: 确认或修正预测"]

链上每一环的排布有明确的代价考量:便宜的检查(存活、冷却)在前,贵的检查(视线射线、回滚重演)在后。外挂最常见的攻击就是把"贵检查"用非法数据绕过,所以链上每一环都必须独立成立,不能依赖客户端已经"自检过"。

客户端与服务器各自持有的世界,用类图表示是这样一种关系——两边对象同名,权力天差地别:

classDiagram
    class ServerWorld {
        +AuthoritativeState 唯一真相
        +ValidateIntent() 校验链
        +RewindForLagComp() 延迟补偿
        +BroadcastDelta() 增量广播
    }
    class ClientWorld {
        +RenderTime: "渲染时刻(落后)"
        +SampleAndRender() 采样影子
    }
    class SnapshotBuffer {
        +Snapshots 快照环形缓冲
        +Sample(t) 双快照插值
    }
    class PredictedSelf {
        +PendingInputs 未确认输入
        +ReplayAfterCorrection() 校正后重放
    }
    class InterpolatedProxy {
        +RemoteActors 其他玩家影子
    }
    ServerWorld --> ClientWorld : 快照/增量 (20Hz)
    ClientWorld *-- SnapshotBuffer
    ClientWorld *-- PredictedSelf
    ClientWorld *-- InterpolatedProxy
    PredictedSelf ..> ServerWorld : 输入意图 (上行)

自己的角色走 PredictedSelf 通道(按下去立刻动,事后对账),别人的角色走 InterpolatedProxy 通道(老老实实等快照插值)。同一个客户端上两套时间并存,这是 UE CharacterMovementComponent 在 5.8 里替你实现的骨架,下一章拆它之前,你得先在概念上接受这个分裂的世界观。

插值、外推、延迟补偿:用 100ms 延迟换不瞬移的世界

服务器真相以固定频率广播,这个频率叫快照率(Snapshot Rate),「远港 Online」按 UE 默认思路定档 20Hz,即快照间隔 Δ=50\Delta = 50ms。问题来了:客户端 60fps 渲染,每 16.6ms 就要画一帧,而真相 50ms 才到一份。中间那些帧,其他玩家的位置从哪来?

两个选择。外推(Extrapolation):根据速度向量往前猜,猜多远取决于网络多慢。插值(Interpolation):故意活在过去,渲染时刻固定在"最新快照之前两个间隔",这样任意一帧渲染,手里永远有前后两个真实快照夹着,做个线性混合就是精确位置。

工程界的共识是选插值,而且把外推压到最小。原因用一次实战事故就能说清:外推 50ms,一个急停转向的角色在其他人屏幕上会冲出真实位置半米到一米,然后被下一个快照硬生生拽回去——玩家看到的是"瞬移"。瞬移一次,射击游戏毁一次手感,MMO 毁一次技能判定观感。而插值的代价只是固定的 100ms 渲染延迟:世界是"晚"了一点,但它连续、平滑、永远可信。宁可稳定地晚 100ms,不可偶尔错 50ms,这是手感工程的铁律。

两个间隔而非一个——事情没有看起来那么简单。推导很短。设快照按间隔 Δ\Delta 到达(忽略抖动),渲染时刻为 trender=tnowkΔt_{render} = t_{now} - k\Delta。插值要求任意渲染时刻都存在快照 Sitrender<Si+1S_i \le t_{render} < S_{i+1}k=1k=1 时,渲染时刻卡在最新快照与"还没到的下一个快照"之间,缓冲里只有一个快照可依赖——只要网络抖动让某个快照晚到哪怕 10ms,或干脆丢一个,渲染时刻就越过缓冲边界,被迫外推。k=2k=2 时,缓冲里常备两个以上快照,丢一个、晚一个都不影响插值连续性。于是有:

Linterp=kΔ=2Δ=2fsnap=220Hz=100msL_{interp} = k \cdot \Delta = 2\Delta = \frac{2}{f_{snap}} = \frac{2}{20\text{Hz}} = 100\text{ms}

这 100ms 是拿抖动免疫和抗丢包换来的保险费。快照率提到 30Hz,保费降到约 67ms,但服务器出口带宽涨 50%——又一笔要算的账。

下面这张时间轴把缓冲区画出来。上面一轨是服务器快照抵达客户端的时刻(假设单向传输 25ms),下面一轨是客户端渲染游标,它始终落后最新快照两个间隔:

快照到达(20Hz + 单向 25ms)S@25S@75S@125S@175S@225S@275插值窗口:渲染时刻被两个真实快照夹住外推禁区:没有快照覆盖,只能猜渲染游标(落后最新快照 2Δ = 100ms)t_render固定插值延迟 100ms —— 保险费,不心疼

第三件是延迟补偿(Lag Compensation),它解决的是射手视角的公平:你瞄的是 100ms 前的敌人(插值所致),开火消息再过半个 RTT 到服务器,服务器"当下"的敌人早不在那了。不做补偿,玩家会觉得"瞄得准却打不中"。做法是让服务器为每个玩家保存最近几百毫秒的位置环形缓冲,命中判定时把目标回滚到射手开枪那一瞬看到的位置。回滚窗口的推导同样朴素——射手看到的是延迟了 RTT/2+LinterpRTT/2 + L_{interp} 的世界,开火指令回传又花 RTT/2RTT/2,所以:

W=RTT+Linterp=RTT+2ΔW = RTT + L_{interp} = RTT + 2\Delta

RTT 100ms、Δ\Delta 50ms 时,W=200W = 200ms。这个窗口必须设上限(业界常见钳制在 250ms 上下):窗口越大对高延迟玩家越友好,但对被击中者越残忍——你躲进掩体半秒后还可能被"回溯枪"打死。公平饼就那么大——补偿刀切下去,切给射手多一点,被击中者就得少一点。把补偿叫成"慈善",等于假装刀口不存在。补偿的本质是在两个玩家的公平之间切蛋糕,刀口位置就是设计决策。

三件套的工程落地,核心就是一个快照环形缓冲加一次回滚重演,伪代码如下:

# 实体插值缓冲:其他玩家影子位置的唯一来源
class SnapshotBuffer:
    def __init__(self, capacity=32):
        self.snaps = RingBuffer(capacity)   # 按服务器时间戳排序

    def add(self, snap):
        self.snaps.insert_sorted(snap)      # UDP 乱序到达是常态

    def sample(self, render_time):
        newer = self.snaps.first_after(render_time)
        older = self.snaps.last_before(render_time)
        if older and newer:                 # 正常路径:双快照夹逼
            t = (render_time - older.time) / (newer.time - older.time)
            return lerp(older.state, newer.state, clamp(t, 0, 1))
        if self.snaps.newest().age() < EXTRAPOLATE_BUDGET_MS:
            return extrapolate(self.snaps.newest())  # 短暂外推,封顶 50ms
        return self.snaps.newest().state    # 超预算:冻结,拒绝瞬移
# 简单延迟补偿:命中判定前把目标回滚到射手开枪时刻
def server_validate_hit(shooter, target, fire_msg):
    rtt        = shooter.conn.smoothed_rtt()
    rewind     = rtt + INTERP_DELAY            # W = RTT + 2Δ
    rewind     = min(rewind, MAX_COMP_MS)      # 钳制 250ms,防回溯枪滥用
    shot_time  = server_now() - rewind

    saved = target.position_history.at(shot_time)   # 环形缓冲取值
    with target.temporarily_at(saved):
        hit = trace(fire_msg.origin, fire_msg.dir)  # 在旧世界上重演射线
    if hit:
        apply_damage(target, fire_msg.weapon)
    return hit

要点只有三行:回滚深度由 RTT 加插值延迟决定,与帧率无关;重演必须可逆,改完位置立刻还原,否则一个判定的副作用会污染下一个判定;历史缓冲至少覆盖 MAX_COMP_MS,存不够就直接拒赔,不能让高延迟玩家把窗口顶穿。

把三件套串起来,一次技能命中的完整往返长这样。以 RTT 100ms、快照 20Hz、服务器 tick 50ms 为例,各段耗时标在图上:

sequenceDiagram
    participant A as 施法者客户端
    participant S as 服务器·权威
    participant B as 旁观者客户端
    Note over A: t=0 按下技能键
    A->>A: 本地预测: 动画/特效立刻播放 (16ms 内)
    A->>S: 技能请求上行 (RTT/2 ≈ 50ms)
    Note over S: t≈50ms 收到请求
    S->>S: 校验链 + 延迟补偿回滚 + 结算 (1 tick ≈ 50ms)
    S->>A: 确认回执: 预测成立 (t≈150ms 到达)
    A->>A: 预测与真相一致: 无感; 不一致: 校正重放
    S->>B: 命中结果随下一快照广播 (t≈150ms 到达)
    Note over B: 进入插值缓冲, 再等 100ms
    B->>B: t≈250ms 渲染出这次命中

三个数字值得记住。施法者 0ms 就有视觉反馈,这是预测买的手感;施法者 150ms 拿到权威确认,这是"子弹有没有中"的确定时刻;旁观者 250ms 才看到全程,其中 100ms 是插值保险。所以 MMO 里"我死了才看到他抬手"是有据可查的——预算表上早就写好了 250ms。玄学?不存在的。

测量这条链路的健康度,客户端侧最直接的两个指标是 Ping 和丢包。UE C++ 里读法很薄:

// 调试 HUD 的链路质量采样(UE 5.8)
void UNetDebugWidget::SampleLinkQuality(const APlayerController* PC)
{
    if (!PC) return;
    const float PingMs = PC->GetPingInMilliseconds(); // 平滑后 RTT
    float AvgLagSec = 0.f;
    if (const UNetConnection* Conn = PC->GetNetConnection())
        AvgLagSec = Conn->AvgLag;                     // 秒, 移动平均
    // 丢包看 STAT NET 的 In/Out Loss; 抖动自采 Ping 求标准差
    UE_LOG(LogTemp, Verbose, TEXT("Ping=%.0fms AvgLag=%.0fms"),
           PingMs, AvgLagSec * 1000.f);
}

GetPingInMilliseconds 对高帧率客户端做过修正,比裸除 RPC 时间戳准;抖动要靠自己采样算标准差,引擎不直接给。这两个数字接进调试 HUD,劣网排查时先看它们再怀疑代码,能省掉一半的瞎忙。

拓扑选型没有银弹:四种架构各拿什么换什么

同步模型解决"数据怎么流",拓扑解决"真相住在哪台机器上"。四档方案,一致性、成本、防作弊、容量四个维度,每上一档都用钱和复杂度换权力:

拓扑一致性单局成本防作弊容量天花板典型用途
P2P弱(各自为政)几乎为零裸奔4–8 人派对小游戏
Listen Server中(主机是真相)零(玩家自掏)主机玩家免疫8–16 人合作联机
Dedicated Server每房间一台进程16–200 人竞技射击/战场
分布式 Zone强(分片真相)一个服务器集群数千人同世界MMO
quadrantChart
    title "拓扑选型:成本 vs 权威性(气泡越大容量越高)"
    x-axis "成本低" --> "成本高"
    y-axis "权威性弱" --> "权威性强"
    quadrant-1 "商业游戏主战场"
    quadrant-2 "独立游戏舒适区"
    quadrant-3 "别住这里"
    quadrant-4 "钞能力特区"
    "P2P": [0.15, 0.2]
    "Listen Server": [0.25, 0.45]
    "Dedicated Server": [0.6, 0.8]
    "分布式 Zone 集群": [0.95, 0.9]

P2P 最致命的其实是 NAT 打洞的成功率和主机玩家退出的雪崩效应——房主一拔网线,全房陪葬。作弊反倒排在后面。Listen Server 本质是把 P2P 的房主扶正成权威,朋友间联机够用,但房主天然拥有 0ms 延迟加篡改豁免,拿去办公开匹配等于请外挂进门。

有一个现实的折中值得知道:不少合作类游戏(四人刷本那类)用的就是 Listen Server,靠"朋友之间不会开挂"的社会契约兜底。这个选择在熟人场景完全成立,成本为零、架构一天搭完。错的是把它平移到陌生人对局——社会契约换成公开匹配,房主优势立刻变成公平性事故。选型之前先回答一个问题:你的玩家之间互相认识吗?

Dedicated Server 是 UE 网络栈的主场:服务器不渲染、只跑逻辑与真相,一个进程扛几十到两百人(取决于 tick 开销,第 24 章专门拆它的构建与部署)。再往上,单进程撞墙——一张无缝大地图几万个实体、几千玩家,没有任何单机 tick 得动。分布式架构把世界切成 Zone,每个 Zone 一个进程,网关管连接,角色跨区做进程间交接。这是第 25 章的主题,本章只需要记住一句:分布式的每一层都是在给"单点真相"打补丁,补丁的代价是跨区边界上那一大堆最终一致的边角案例。

「远港 Online」的同步预算:先把数字写死在纸上

补课到此为止,收拢成一份可以贴在工位上的预算表。「远港 Online」对标 WOW 式大世界,不追求格斗游戏的严苛手感,但必须保证技能判定公平、大世界千人同图不崩:

预算项目标值依据
可玩 RTT 上限150ms超此值补偿窗口顶穿
服务器快照率20Hz(战斗区可提 30Hz)带宽与平滑的折中
客户端插值延迟100ms(2Δ)抗一帧丢包加常规抖动
外推预算≤50ms,超时冻结宁可定住,不可瞬移
延迟补偿窗口RTT+100ms,钳制 250ms公平蛋糕的刀口
单玩家下行带宽≤64KB/s相关性剔除后可达
关键事件可靠性100%(可靠 UDP 通道)技能/交易不走裸 UDP

这张表就是验收标准,愿望清单没有这么硬的门槛。后面每一章——复制的相关性调优(第 21 章)、GAS 预测窗口(第 23 章)、服务器 tick 预算(第 24 章)——都会回来对这张表报账。预算立不住,架构再漂亮也是沙上盖楼。

定预算时还有两条经验法则。其一,按最差的用户画像定档,不按平均值。「远港 Online」的画像里有三四线城市的 WiFi 玩家,也有网吧百兆光纤玩家,预算表必须让前者能玩——否则你就等于放弃了一大块 MMO 的核心用户盘。其二,预算要留余量给运营期。上线后总会有人提议"把这个技能的快照率加一倍手感更好",如果 64KB/s 的下行预算被零零碎碎的新需求啃光,劣网玩家会成批流失。带宽预算和帧时间预算一样,是制度——超支要走评审,不能靠热情。

至于这张表怎么验证,答案藏在 UE 自带的劣网模拟里:编辑器 PIE 和控制台都支持注入人工延迟、抖动与丢包,后边上手任务第 2 条用的就是这套开关。任何同步方案没过"100ms 延迟加 1% 丢包"这一关,都不配进真机测试。

小结

延迟、抖动、丢包各有量级:同城 10ms 内、跨省 50ms 内、跨国 150ms 起;WiFi 抖动是有线的十倍起步;1% 丢包能让 TCP 吞吐砍到十分之一——这三个数决定一切上层设计。帧同步带宽随人数平方增长且要求全平台确定性,1000 人场景服务器出口逼近 8Gbps,MMO 只能状态同步——算术早就把答案写好了。客户端发来的每个字节都是声称;服务器权威加一条"便宜检查在前、昂贵重演在后"的校验链,是防作弊的第一道也是最后一道墙。插值延迟 = 2×快照间隔,用固定 100ms 换抖动免疫与抗丢包;延迟补偿窗口 = RTT + 插值延迟,钳制在 250ms,补偿的是射手的公平,切割的是被击中者的体验。拓扑四档逐级加码:P2P、Listen Server、Dedicated、分布式 Zone,每一档都是拿钱和复杂度换一致性与容量。

上手任务

  1. ping 对同城、跨省、境外三个目标各打 200 包,记录均值、标准差、丢包率,并对照本章第一节的量级表。验收:能说清自己宽带下"抖动是平均延迟的百分之几"。
  2. 在 UE 5.8 的 PIE 里开 1 服 2 客户端,控制台用 Net PktLag=100 Net PktLagVariance=25 Net PktLoss=1 注入人工劣网,移动并施放技能。验收:观察到自己角色校正回弹、他人角色插值平滑两种行为,并能解释差异来源。
  3. 把本章的插值缓冲伪代码翻成真代码(语言不限),喂一组带 20% 乱序、2% 丢包的快照流。验收:输出位置曲线无跳变,外推时长不超过 50ms。
  4. 按 100 人、30Hz、32 字节输入重算帧同步带宽账,再按 50 个相关实体、20Hz、80 字节增量算状态同步账。验收:两个数字差三个数量级左右,并能指出参数里哪个最敏感。

下一章

预算表有了,该看 UE 拿什么兑现它:第 21 章拆复制系统的三件套——属性复制、RPC、相关性,看服务器真相是怎么一字节一字节流到客户端影子世界的。

延伸阅读

  • 官方文档词条:Unreal Engine → Networking and Multiplayer → Networking Overview,以及 Actor Replication 词条(API 细节以 5.8 文档为准)。
  • Valve 开发者社区经典:Source Multiplayer Networking(插值、延迟补偿概念的原典,概念跨引擎通用)。
  • Gabriel Gambetta 的 Client-Server Game Networking 系列(fast-paced multiplayer 那篇,把本章三件套讲成了图文教程)。
  • GDC 2015《Overwatch Gameplay Architecture and Netcode》相关公开分享:高 tick 率下预测与补偿的工业级实践。
  • 源码路径:Engine/Source/Runtime/Engine/Private/CharacterMovementComponent.cpp(UE 自己的预测-校正实现,读完本章再读它,坡度刚好)。