Unity 手游网络同步技术

网络同步是 Unity 手游里最「看起来简单、做起来要命」的一环。本文系统梳理两大核心范式------帧同步(Lockstep)状态同步(State Sync),从原理、确定性、权威归属、带宽、延迟补偿、断线重连、反作弊,到 Unity 落地要点,多方位拆解。最后给一份选型决策表,并附一个真实上线项目的取舍案例。

先厘清两层概念

聊同步前,先分清两个经常被混为一谈的层次:

层次 回答的问题 典型选项
同步范式 网络上「传什么」、对端「怎么用」 帧同步 / 状态同步
延迟补偿 怎么掩盖网络延迟 客户端预测 / 插值 / 回滚(Rollback)

回滚不是帧同步/状态同步之外的第三种并列范式,而是叠在它们之上的一层延迟补偿技术。 这是最常见的认知误区,后面会单独澄清。

判定一个项目属于哪种范式,唯一标准是:看对端收到数据后如何使用它------是「喂进本地确定性逻辑重算」,还是「直接套用收到的结果」。


一、帧同步(Lockstep)

1.1 核心原理:同步输入,各自重算

帧同步的「帧」,指的是逻辑帧(Logic Tick),不是渲染帧。

复制代码
客户端 A                      服务器 / Host                  客户端 B
  │ 上报输入(按键/摇杆) ──────────▶                             │
  │                             收集所有玩家输入                │
  │ ◀──────────── 广播第 N 帧所有玩家输入 ──────────────────────▶
  │  用确定性逻辑重算第 N 帧结果                                 │
  • 玩家只上报操作输入(第 N 帧按了什么、摇杆方向),不上报位置、伤害这些结果。
  • 服务器(或 Host 主机)不跑游戏逻辑,只把一段时间内所有玩家的输入打包广播
  • 每个客户端拿到输入后,用完全相同的确定性逻辑,各自重算出这一帧的游戏状态。
  • 因为所有客户端初始状态相同、输入序列相同、逻辑相同,所以逐帧算出的结果天然一致,无需互相传状态。

1.2 生死线:确定性(Determinism)

帧同步能成立的前提是确定性模拟 :同样的初始状态 + 同样的输入序列 → 必须算出逐位相同的结果。一旦某台机器算出一丁点偏差,后续所有帧都会累积放大,最终「双方看到的是两场不同的游戏」。

破坏确定性的四大元凶:

元凶 为什么坏 应对
浮点数 不同 CPU/编译器浮点行为不同,微小误差大量计算后放大 用**定点数(Fix64)**替代 float
随机数 每台机器 rand() 序列不同 共享种子的可重放随机流
系统时间 挂钟时间各端不一致 逻辑只用逻辑帧计数,不读墙钟
非确定性物理/库 物理引擎、字典遍历序等可能不确定 慎用甚至禁用,替换为确定性实现

浮点问题最直观:0.1 + 0.2 != 0.3。在帧同步里,一个 float 累积误差跑十几分钟就能让一个单位的位置差出好几格。所以成熟帧同步引擎几乎一律上定点数。

1.3 逻辑帧驱动:固定 tick + 等输入到齐

  • 所有客户端以相同的固定 tick rate 推进(如 15/30/60 逻辑帧每秒)。
  • 每推进一帧,必须等这一帧所有玩家的输入都到齐 。任何一个玩家延迟,所有人都要等------这就是帧同步最痛的「木桶效应」:游戏节奏被最慢的玩家拖累
  • 缓解手段:乐观帧锁定(先预测推进,错再回滚)、延迟缓冲(故意加少量延迟吸收抖动,本质已向回滚靠拢)。

1.4 优劣与典型场景

优点

  • 带宽极低:只传输入,同屏几百个实体也不怕(这是 RTS 选它的根本原因)。
  • 天然回放/观战:存输入序列即可完整回放,录像文件极小。
  • 服务器压力小:只转发输入,不做模拟。

缺点

  • 确定性改造成本极高:定点数、固定 tick、禁随机,物理与浮点相关代码都要重写。
  • 断线重连难:错过中间输入就无法继续,需从头重放全部帧。
  • 作弊防护弱:每个客户端都掌握完整游戏状态,地图全开(Maphack 天然可行)。
  • 同步调试难:出现一帧不一致要逐帧 diff,定位极其痛苦。

典型场景:RTS(星际争霸、帝国时代)、MOBA(王者荣耀采用帧同步)、回合制策略。

1.5 Unity 落地要点

  • 逻辑帧 vs 渲染帧分离 :逻辑跑在 FixedUpdate(或自定义 tick 循环),渲染做插值平滑表现。
  • 确定性输入驱动 :用确定性随机(自定义 LCG 等)替代 UnityEngine.Random
  • 定点数:引入 Fix64 库,或用整数定点(如「毫米」做单位)。
  • 禁用非确定性 APITime.timefloat 运算、Dictionary 无序遍历(需排序后遍历)都要封禁或替换。
  • 帧率无关:所有移动/物理按逻辑帧 delta 推进,不依赖渲染帧率。

二、状态同步(State Sync)

2.1 核心原理:同步结果,直接套用

状态同步与帧同步正好相反------同步「结果」而非「输入」

复制代码
客户端 A (权威)                    服务器                    客户端 B
  │ 上报输入/本地算好状态 ──────────▶                            │
  │                           服务器裁决/转发状态               │
  │ ◀────────────────── 下发最新状态(位置/HP/伤害) ────────────▶
  │                              对端直接套用结果(或插值)       │
  • 权威端广播状态结果:位置、HP、伤害值、buff 状态......
  • 对端拿到结果后直接套用 ,或朝目标点插值 平滑过渡,不重算逻辑
  • 权威在谁手里,谁决定「真值」;对端只是「表现结果」。

2.2 权威归属:服务器权威 vs 客户端权威

状态同步的核心议题是「谁是权威」。两种主流取法:

服务器权威(Server Authority) 客户端权威(Client Authority / 分布式权威)
逻辑在哪算 服务器 客户端(各自/指定主机)
延迟 状态变化约 1 RTT 约 ½ RTT,更低
服务器开销 重(跑全量模拟) 轻(只转发/路由)
反作弊 强(服务器校验一切) 弱(信任客户端)
典型场景 竞技 PVP / FPS / 精确物理 社交 / 休闲 / Co-op
  • 服务器权威 :Unity Netcode for GameObjects 的默认拓扑。客户端发请求(ServerRpc),服务器裁决后写 NetworkVariable 下发。NetworkTransform 默认服务器权威,NetworkRigidbody 物理只在服务器跑。
  • 客户端权威 / 分布式权威 :Unity 6 + Netcode v2.2+ 引入的「Distributed Authority」拓扑,每个客户端当自己的 mini-host,可 spawn 自己的对象、NetworkVariable 默认 owner 写 / 所有人读。牺牲安全性换低延迟、低成本。

两者不是非黑即白------现实里常是**「角色层客户端权威 + 关键经济/胜负服务器权威」的混合**。

2.3 延迟补偿:状态同步的「手感」来源

状态同步的权威往返有天然延迟,不补偿就会「按键后人物愣一下才动」。三层补偿手段:

  1. 插值(Interpolation):渲染端故意滞后 1~2 个 tick,在已知的旧状态与旧状态之间平滑过渡,消除网络抖动带来的「瞬移」。代价是永远看到的是「过去 100ms 的世界」。
  2. 客户端预测(Client Prediction) :本地输入立即响应,不等服务器回包;服务器回包后做校正(Reconciliation),若本地预测与服务器权威不符,就回滚到服务器状态重放本地输入。
  3. 回滚(Rollback):见下一节。

2.4 优劣与典型场景

优点

  • 不要求确定性:各端独立模拟,以权威结果为准,客户端差异会被校正。
  • 断线重连容易:拉一份最新状态快照即可继续。
  • 作弊防护强(服务器权威时):服务器校验一切,客户端改不了真值。

缺点

  • 带宽较高:传实体状态,需压缩/差分/interest management 优化。
  • 服务器成本高(服务器权威时):服务器要跑全量模拟。
  • 实现复杂:预测/校正/插值/回滚一环套一环,调起来费劲。

典型场景:FPS(CS:GO/Valorant)、MMO(魔兽世界)、LOL/DOTA2、格斗。

2.5 Unity 落地要点

  • Netcode for GameObjects + Unity Transport :官方栈,NetworkVariable(状态)、NetworkTransform(位置)、ServerRpc/ClientRpc(消息)。
  • 传输层UnityTransport 底层是 ENet/UDP;WebGL 走 WebSocket。
  • 权威设置NetworkManager > Network Topology 选服务器权威或分布式权威;用 IsServer / HasAuthority 做对象级判断。
  • 带宽优化NetworkTransform 开插值、限同步频率;大状态用自定义序列化;Interest Management 只同步视野内实体。

三、回滚网络(Rollback):澄清它的位置

回滚不是第三种范式,而是「延迟补偿」的一个高级手段,可叠在状态同步或锁步上。核心是「不等待,先预测,错了再回滚」:

  1. 预测(Predict):对手输入没到时,先预测(通常「沿用上一帧输入」)。
  2. 立即模拟(Simulate):本地输入 + 对手预测输入,立即推进并渲染,本地操作零延迟。
  3. 比对(Compare):对手真实输入到达后与预测比对。
  4. 回滚重模拟(Rollback & Resimulate):若预测错,回到分歧帧的存档,用正确输入重新模拟到当前帧,一帧内完成,肉眼表现为短暂「跳变」而非卡顿。

硬性前提还是确定性模拟 ------必须能随时保存/载入状态、不渲染地执行一帧。GGPO 官方文档的名言:「难点是确定性本身,而不是 rollback 机制」

回滚是 1v1 格斗的「黄金标准」(街霸 6、GGPO 系),它让离线练出的反应速度能直接搬到线上。但它对确定性要求极高、状态序列化与回滚架构复杂,不适合实体多、非确定性的场景。


四、双范式全景对比

维度 帧同步(Lockstep) 状态同步(State Sync)
同步内容 输入指令 状态结果
对端如何用 喂确定性逻辑重算 直接套用 / 插值
逻辑在哪算 各客户端各自算 权威端算
确定性要求 极高(定点数/固定tick/禁随机) 无(结果为准)
带宽 极低 较高(需优化)
断线重连 难(从头重放) 易(拉快照)
反作弊 弱(客户端有全量状态) 强(服务器权威可校验)
回放/观战 轻量(存输入即可) 需额外快照/回放系统
实现复杂度 中(确定性+同步调试难) 高(预测/校正/插值复杂)
典型场景 RTS / MOBA FPS / MMO / 格斗

要点 :两者的本质分歧不是「用不用 UDP」或「用不用服务器」,而是同步输入还是同步结果。带宽、确定性、反作弊、断线重连这些差异,都是这个分歧的推论。


五、实战案例:一个 Unity 手游的取舍

本节给一个真实上线项目(Unity 手游项目,中轻度对抗)的取舍,作为「选型落到代码」的参照。细节从简,重在决策逻辑。

项目最终选的是:客户端权威状态同步。

为什么是状态同步而非帧同步? 同屏实体少(不需要帧同步的带宽优势),对抗强度中等(不必上服务器权威的强校验),状态同步不需要确定性改造、断线重连也简单。判定依据看对端用法------位置命令「直接赋值并插值」:

csharp 复制代码
public class xxxSyncMCommand : PooledCommand<xxxSyncMCommand>
{
    public Vector3 Position { get; private set; }   // 传的是位置结果,非输入

    public override void Execute(CommandTypes commandTypes)
    {
        // 对端收到后:把位置作为目标点交给控制器插值,不重算轨迹
        MessageSystem.Instance.SendSync(Character.FieldObjectController as IEventListener,
            SetTargetEvent.CreateSyncTarget(Position, Direction, AnalogueMode));

        if (Character.FieldObjectController is NetworkCharacterController nc)
        {
            nc.ReplicationPosition = Position;        // 直接覆盖复制位置
            nc.ReplicationDirection = Direction;
        }
    }
}

权威怎么放? 分布式客户端权威,双层模型:

  • 角色层 :每个玩家对自己的角色权威,谁的角色谁算。Player.Local 天然归属自己,只有本地玩家发布的命令才广播,且本地立即执行:
csharp 复制代码
public static void Publish(Player owner, Command command) {
    if (owner == Local) {                    // 只有本地玩家发布的命令才广播
        if (ShouldWriteCommand(command))
            xxxcommandPipeline?.SendCommand(command);
        command.Execute(CommandTypes.None);          // 本地立即执行
    }
}
  • 公共实体层 :怪物/NPC 归 SuperClient(第一个存活玩家),掉线时服务端推 reassign-owner 迁移。

传输怎么走? 关键在「位置可丢、伤害不可丢」的矛盾,用双 QoS 通道解决:

一句话总结这个案例:状态同步选范式、客户端权威省成本------是「中轻度对抗 + 同屏实体少」场景下很典型的一种落法。


六、选型决策:没有银弹

选同步方案,先问三个问题:

  1. 局内同屏实体多不多? RTS/MOBA 同屏几十上百实体 → 帧同步(只传输入才扛得住带宽);FPS/MMO 实体少 → 状态同步更简单。
  2. 是否要求强一致防作弊? 竞技 PVP → 服务器权威状态同步;休闲 Co-op → 客户端权威可接受,省成本降延迟。
  3. 能否承受确定性改造成本? 帧同步/回滚要定点数 + 固定 tick + 禁随机,成本极高;状态同步无需确定性。

实战高频出现的是混合方案 :核心战斗帧同步 + 经济/匹配状态同步,断线重连用「帧同步追帧 + 状态快照」加速(王者荣耀/LOL 手游同款)。把范式当光谱而不是开关,按子系统选,比纠结「全局用哪种」更实用。

相关推荐
深念Y31 分钟前
# CC-Switch + Claude/Codex 折腾教训记录
运维·服务器·网络·ai·agent·web·ccsiwtch
OpenCloudOS38 分钟前
高危|Linux 内核 XFS reflink 本地提权漏洞修复指南
linux·网络·安全
Sherotree41 分钟前
Agent 工程笔记①:工具调用失败时,先查哪三层
网络·数据库·笔记
LccKyI1 小时前
# C# 基础编程 面试题汇总(一)
开发语言·c#
Shadow(⊙o⊙)1 小时前
进程组、会话、守护进程
网络·网络协议·http
严谨的麻辣烫1 小时前
批量静态 IP 如何管理?用 Python 建立一个简单的 IP 资源监控方案
运维·服务器·网络·python·tcp/ip
wjcroom1 小时前
在PVE中的直通和共享文件访问
网络·经验分享·虚拟化
祀爱1 小时前
C# MQTT 连接服务
后端·c#·.net
niuniudengdeng2 小时前
Unity手游APK去广告实战:华为渠道包反编译、广告拦截与“网络不佳“弹窗修复全记录
unity·游戏引擎