第 6 章 · 网络预算的真实含义
到第 5 章为止,所有同步代码都默认「字段写进去就发出去」。这一章把「发」这件事拆开看:每次发能塞多少、单位时间能发多少、超了会怎样。
这些数字以官方 Network Specs and Tips 为准,可能随 SDK 版本调整。Vol.2 引用时记当前公开值。
| 数字 | 含义 |
|---|---|
| 约 200 字节 | 单次 Continuous 同步序列化上限 |
| 约 49 KB | 单次 Manual 同步序列化上限 |
| 约 11 KB/s | 整个客户端的发送总带宽预算(粗略级别) |
注意三件事:
- Continuous 和 Manual 的上限差了 250 倍。这不是用法差异,是设计差异:Continuous 是「频繁但小」,Manual 是「不频繁但可以大」。
- 49 KB 是 Manual 单次上限,不是「每帧 49 KB」。能不能连续发 49 KB 受 rate limit 约束。
- 总带宽是客户端级别。同时同步十个对象,那十个对象共享 11 KB/s 这一个池子,不是每个对象各有一份。
关于 11 KB/s 的写法:VRChat 官方原文是 11kb per second,小写 kb 在国际单位约定下是歧义的(可能指千比特,也可能指千字节)。同页面其他度量一律用字节(200 bytes、49 Kilobytes、OnPostSerialization 字段叫 byteCount),且只有按千字节/秒解读才与这些规格自洽(按千比特/秒约合 1.4 KB/s,连一次 200 字节的 Continuous 序列化都覆盖不下)。本书统一按 约 11 KB/s(千字节/秒) 使用,引用原文链接时保留官方写法以便核对。
还有一个隐藏的数字:Continuous 模式下单个同步字符串约 126 字符。Continuous 字段里有一个长字符串,超过这个长度就发不出去。需要发更长字符串时改 Manual。
一次能塞进去 vs 持续发得起
Section titled “一次能塞进去 vs 持续发得起”「一次能塞进去」指这次发送的所有同步字段加起来序列化后是多少字节。Continuous 上限约 200,Manual 上限约 49 KB。Continuous 几个 int、bool、Vector3 加起来很容易过 200,过了之后这次发送会失败,OnPostSerialization(result) 里 result.success 是 false。
「持续发得起」指单位时间内能持续送出的总数据量。VRChat 给客户端的预算是约 11 KB/s 的总带宽。Manual 模式下每个对象有 per-object 的 rate limit:发的越多,下一次能发的窗口被推后越远。
直觉上可以这样读:几百字节的 Manual 同步通常很快能等到下一次机会;接近 11 KB 量级时就要按秒级理解;接近 49 KB 上限时,下一次发送会被推到更晚。具体延迟看当前实例、对象数量和拥塞情况,不能只靠一个公式算死。
这就是「Manual 单次能塞 49 KB,但持续发不动 49 KB」的真实含义。
// Owner 端,连续 100 次想发一个 1 KB 的同步for (int i = 0; i < 100; i++){ bigField[i] = ComputeNewValue(i); RequestSerialization(); // 看似发了 100 次}实际效果:VRChat 不会在一帧内发 100 次。多余的 RequestSerialization 调用会被合并到同一次实际发送。本帧最终发出去的是「最后一次累计完成的值」,下次发送要等 rate limit 允许。
RequestSerialization 想调几次调几次,不会失败也不会出错,但实际发送频率受 rate limit 控制。
超限会发生什么
Section titled “超限会发生什么”官方文档的原话:超限不会崩游戏,但这次同步发不出去。
具体表现分两种。一种是单次 payload 超过 200 字节(Continuous)或 49 KB(Manual)时序列化失败,OnPostSerialization 报告 success == false,日志里有错误信息,该 UdonBehaviour 后续的网络事件也会跟着异常(官方文档原话)。另一种是总带宽吃紧时 Networking.IsClogged 返回 true,发送被推后,到达远端的延迟显著增加。
工程上要做三件事。第一,写 GameState 这种「大状态」对象时预估字段总大小,100 个 int 是 400 字节、已经过 Continuous 上限,必须 Manual。第二,关键路径上加 IsClogged 检查,频繁触发的代码里 if (Networking.IsClogged) return; 让本帧跳过提交,避免雪崩。第三,同步频率不能超过实际能力,一个对象每秒尝试 60 次 Manual 同步、每次 1 KB,总需求已经远超 11 KB/s 的预算,实际只能按 rate limit 发出其中一部分。
基础尺寸(VRChat 序列化后近似值):
| 类型 | 字节 |
|---|---|
bool / byte / sbyte | 1 |
short / ushort / char | 2 |
int / uint / float | 4 |
long / ulong / double | 8 |
Vector2 | 8 |
Vector3 | 12 |
Vector4 / Quaternion | 16 |
Color / Color32 | 4-16 |
string | 1+ 每字符约 1-3 字节(UTF-8) |
| 数组 | 4 字节长度头 + 元素总尺寸 |
记两条规则:一个 Vector3 是 12 字节,10 个 Vector3 已经 120 字节,离 Continuous 上限 200 不远;字符串容易爆,100 字符 ASCII 约 100 字节,UTF-8 中文 100 字符可能 300 字节,Continuous 直接超限。
合作防守世界的 GameState 估算:
currentWave int 4enemiesRemaining int 4phase byte 1totalScore int 4stateVersion int 4phaseStartTime float 4 -- 21 字节GameState 这种简单结构 Continuous 完全装得下。加上「每个玩家的状态」之后会爆,那时候要分对象:每个玩家的状态放到玩家对象上,GameState 只保留全局聚合。第二部 9 章和 11 章会展开这个分摊设计。
设计预算的三个工程动作
Section titled “设计预算的三个工程动作”写每一个新的同步对象时,做三件事。
第一件:选模式。 字段总尺寸是多少、变更频率是多少。频繁小变更走 Continuous,不频繁但关键的走 Manual。两者不能同时挂在同一个对象上(VRChat 会取最严格的)。
第二件:拆对象。 一个 GameState 不可能装下所有共享状态。原则是:玩家本人的姿态由 VRChat 头像同步搞定,物理对象交给 VRCObjectSync,剩下的状态再判断该归 GameState(全局聚合)还是该归玩家对象(per-player)。具体的 PlayerObject 与 GameState 分摊设计在第二部 9 章和 11 章。
第三件:监控发送。 在关键对象的 OnPostSerialization 里记 byteCount。等第 7 章上多客户端测试时打开 Debug View 6,能看到每个对象的实际发送字节数和 BPS。理论估算和实测对比,差距大就回头调整。
一个最小预算监控
Section titled “一个最小预算监控”加在 GameState 上:
private int sendCount;private int totalBytes;private float windowStart;
public override void OnPostSerialization(SerializationResult result){ if (Time.realtimeSinceStartup - windowStart > 5f) { Debug.Log($"[GameState] last 5s: {sendCount} sends, {totalBytes} bytes"); sendCount = 0; totalBytes = 0; windowStart = Time.realtimeSinceStartup; } if (result.success) { sendCount++; totalBytes += result.byteCount; } else { Debug.LogWarning("[GameState] send failed"); }}每 5 秒打一行日志。开发期看一眼,比起完全不看「这个对象发了多少」要靠谱得多。
合作防守世界要支持 4 个玩家,每个玩家有:HP(int 4 字节)、武器类型(byte 1)、是否倒下(bool 1)、击杀数(int 4),合计每人 10 字节。如果把 4 个玩家的状态都塞进 GameState 这一个对象:
- 该用 Continuous 还是 Manual?为什么?
- 玩家数量从 4 变成 16 时,预算占用还够吗?
- 如果把每个玩家的状态拆到他自己的玩家对象上(第二部 9 章会教),GameState 这一份对象的预算是多少?这种拆法对带宽的影响是?
不需要写代码,能在脑子里把字节数估出来即可。
这一章做了什么
Section titled “这一章做了什么”三个数字:Continuous 单次约 200 字节、Manual 单次约 49 KB、总带宽约 11 KB/s。「单次能塞」和「持续发得起」是两件事,Manual 49 KB 上限被 rate limit 卡住,连续发不动那么多。超限不会崩,但这次发不出去;OnPostSerialization(result) 里 result.success == false 是入口。写新同步对象前的三件事是选模式、拆对象、加发送监控。
- VRChat Creator Docs · Network Specs and Tips — 200 bytes / 49 KB /
11kb per second等数字的官方出处。 - VRChat 汉化文档 · 网络规范和贴士 — 中文镜像。
- 附录 · 术语表 —
Bandwidth、Latency、Continuous Sync、Manual Sync的客观定义。