解释NetworkVariable与RPC的序列化差异
解读
在国内 Unity 多人项目面试中,这道题考察的是对 Netcode for GameObjects(NGO) 底层同步机制的理解深度。面试官想确认:
- 你是否真的用过 NGO,而不是只看过文档;
- 能否根据业务场景选择 “持续同步” 还是 “一次性调用”;
- 是否清楚两种路径在 字节流、频率、GC、带宽 四个维度的代价差异。 答不出“谁写谁读、谁决定序列化规则”会被直接判定为“只搭过 Demo”。
知识点
- NetworkVariable 是 “状态同步” 模型,底层走 NetworkVariableSerializer<T>,每帧对比脏标记,只压缩变化字段,支持 delta + packing + bit-level compression,默认使用 NetworkVariableWritePermission.Server(国内项目普遍改成 Owner 以降低服务器压力)。
- RPC 是 “事件触发” 模型,底层走 RpcMessage,序列化器是 RpcSerializer,一次性把参数列表完整写入流,不支持 delta,每次调用都会产生一次 GC Alloc(约 120~200 B),且 不受 NetworkConfig.TickRate 限制,调用即发送。
- NetworkVariable 的序列化时机固定:在 NetworkTickSystem 的 PreUpdate 阶段统一批处理,可合并 MTU;RPC 立即序列化并 单独封包,小包头 28 B(IPv4+UDP+ReliableFragmentedWindowHeader),频繁调用容易 打满 30 Hz 移动网络上行带宽。
- 版本兼容:NetworkVariable 依赖 INetworkSerializable 的 Version 字段 做 前后向兼容;RPC 参数列表一旦修改必须 同时改两端代码,否则 直接抛 RpcException,国内热更场景下这是 禁止修改 RPC 签名 的根本原因。
- 极限性能:在 200 人同屏的国产 SLG 中,NetworkVariable<bool> 压缩后 1 bit/帧,而 Rpc<bool> 最小 29 B/次;把“技能冷却”从 RPC 改成 NetworkVariable 后,上行流量从 2.3 MB/s 降到 90 KB/s,这是国内大厂 性能面试必举例 的数据。
答案
NetworkVariable 与 RPC 的序列化差异可以概括为 “持续状态 vs 一次性事件” 带来的四条根本不同:
- 触发时机:NetworkVariable 由 脏标记驱动,每 NetworkTick 统一批处理;RPC 由 业务代码主动调用,立即序列化并推入传输队列。
- 数据粒度:NetworkVariable 只写 变化量,支持 位级压缩(例如 Quaternion 压缩到 48 bit);RPC 必须完整写入参数列表,无 delta 机制。
- 带宽模型:NetworkVariable 在 MTU 内自动合并,包头均摊后趋近 1 bit/字段;RPC 每次独立封包,最小 29 B 起跳,高频调用直接占满 4G 上行。
- 版本与热更:NetworkVariable 通过 INetworkSerializable.Version 做 字段级前后向兼容;RPC 签名一旦改变就抛异常,国内热更框架禁止修改 RPC 参数列表。
因此,“持续变化的数据”(位置、血量、倒计时)用 NetworkVariable,“瞬时事件”(释放技能、开门、掉血特效)用 RPC,是国内项目 不可互换的铁律。
拓展思考
如果面试官继续追问 “如何把 RPC 流量优化到 NetworkVariable 级别”,可以从 “批量 RPC” 角度回答:
- 在 NetworkTick 内缓存所有技能请求,统一序列化为 NativeArray<byte>,单帧一次性 RpcBatchSend(NativeArray<byte>),自己实现位压缩,可把 200 次技能调用从 5.8 KB 降到 ~300 B,接近 NetworkVariable 的压缩率;
- 但代价是 延迟增加 1 Tick(~33 ms),需要战斗策划接受“技能帧延后”;国内腾讯《Arena Breakout》就采用 “客户端预表现 + 服务器校验” 来掩盖这 33 ms,面试时提到这个案例会直接加分。