解释NetworkVariable与RPC的序列化差异

解读

在国内 Unity 多人项目面试中,这道题考察的是对 Netcode for GameObjects(NGO) 底层同步机制的理解深度。面试官想确认:

  1. 你是否真的用过 NGO,而不是只看过文档;
  2. 能否根据业务场景选择 “持续同步” 还是 “一次性调用”
  3. 是否清楚两种路径在 字节流、频率、GC、带宽 四个维度的代价差异。 答不出“谁写谁读、谁决定序列化规则”会被直接判定为“只搭过 Demo”。

知识点

  1. NetworkVariable“状态同步” 模型,底层走 NetworkVariableSerializer<T>,每帧对比脏标记,只压缩变化字段,支持 delta + packing + bit-level compression,默认使用 NetworkVariableWritePermission.Server(国内项目普遍改成 Owner 以降低服务器压力)。
  2. RPC“事件触发” 模型,底层走 RpcMessage,序列化器是 RpcSerializer一次性把参数列表完整写入流,不支持 delta,每次调用都会产生一次 GC Alloc(约 120~200 B),且 不受 NetworkConfig.TickRate 限制,调用即发送。
  3. NetworkVariable 的序列化时机固定:在 NetworkTickSystemPreUpdate 阶段统一批处理,可合并 MTU;RPC 立即序列化并 单独封包小包头 28 B(IPv4+UDP+ReliableFragmentedWindowHeader),频繁调用容易 打满 30 Hz 移动网络上行带宽
  4. 版本兼容:NetworkVariable 依赖 INetworkSerializableVersion 字段前后向兼容;RPC 参数列表一旦修改必须 同时改两端代码,否则 直接抛 RpcException,国内热更场景下这是 禁止修改 RPC 签名 的根本原因。
  5. 极限性能:在 200 人同屏的国产 SLG 中,NetworkVariable<bool> 压缩后 1 bit/帧,而 Rpc<bool> 最小 29 B/次;把“技能冷却”从 RPC 改成 NetworkVariable 后,上行流量从 2.3 MB/s 降到 90 KB/s,这是国内大厂 性能面试必举例 的数据。

答案

NetworkVariable 与 RPC 的序列化差异可以概括为 “持续状态 vs 一次性事件” 带来的四条根本不同:

  1. 触发时机:NetworkVariable 由 脏标记驱动每 NetworkTick 统一批处理;RPC 由 业务代码主动调用立即序列化并推入传输队列
  2. 数据粒度:NetworkVariable 只写 变化量,支持 位级压缩(例如 Quaternion 压缩到 48 bit);RPC 必须完整写入参数列表无 delta 机制
  3. 带宽模型:NetworkVariable 在 MTU 内自动合并包头均摊后趋近 1 bit/字段;RPC 每次独立封包最小 29 B 起跳高频调用直接占满 4G 上行
  4. 版本与热更:NetworkVariable 通过 INetworkSerializable.Version字段级前后向兼容;RPC 签名一旦改变就抛异常国内热更框架禁止修改 RPC 参数列表
    因此,“持续变化的数据”(位置、血量、倒计时)用 NetworkVariable,“瞬时事件”(释放技能、开门、掉血特效)用 RPC,是国内项目 不可互换的铁律

拓展思考

如果面试官继续追问 “如何把 RPC 流量优化到 NetworkVariable 级别”,可以从 “批量 RPC” 角度回答:

  1. NetworkTick 内缓存所有技能请求,统一序列化为 NativeArray<byte>单帧一次性 RpcBatchSend(NativeArray<byte>)自己实现位压缩,可把 200 次技能调用从 5.8 KB 降到 ~300 B接近 NetworkVariable 的压缩率
  2. 但代价是 延迟增加 1 Tick(~33 ms)需要战斗策划接受“技能帧延后”;国内腾讯《Arena Breakout》就采用 “客户端预表现 + 服务器校验” 来掩盖这 33 ms,面试时提到这个案例会直接加分