跳转到内容

第 6 章 · 网络预算的真实含义

约 7 分钟 难度:1 动手章

到第 5 章为止,所有同步代码都默认「字段写进去就发出去」。这一章把「发」这件事拆开看:每次发能塞多少、单位时间能发多少、超了会怎样。

这些数字以官方 Network Specs and Tips 为准,可能随 SDK 版本调整。Vol.2 引用时记当前公开值。


数字含义
约 200 字节单次 Continuous 同步序列化上限
约 49 KB单次 Manual 同步序列化上限
约 11 KB/s整个客户端的发送总带宽预算(粗略级别)

注意三件事:

  1. Continuous 和 Manual 的上限差了 250 倍。这不是用法差异,是设计差异:Continuous 是「频繁但小」,Manual 是「不频繁但可以大」。
  2. 49 KB 是 Manual 单次上限,不是「每帧 49 KB」。能不能连续发 49 KB 受 rate limit 约束。
  3. 总带宽是客户端级别。同时同步十个对象,那十个对象共享 11 KB/s 这一个池子,不是每个对象各有一份。

关于 11 KB/s 的写法:VRChat 官方原文是 11kb per second,小写 kb 在国际单位约定下是歧义的(可能指千比特,也可能指千字节)。同页面其他度量一律用字节(200 bytes49 KilobytesOnPostSerialization 字段叫 byteCount),且只有按千字节/秒解读才与这些规格自洽(按千比特/秒约合 1.4 KB/s,连一次 200 字节的 Continuous 序列化都覆盖不下)。本书统一按 约 11 KB/s(千字节/秒) 使用,引用原文链接时保留官方写法以便核对。

还有一个隐藏的数字:Continuous 模式下单个同步字符串约 126 字符。Continuous 字段里有一个长字符串,超过这个长度就发不出去。需要发更长字符串时改 Manual。


「一次能塞进去」指这次发送的所有同步字段加起来序列化后是多少字节。Continuous 上限约 200,Manual 上限约 49 KB。Continuous 几个 intboolVector3 加起来很容易过 200,过了之后这次发送会失败,OnPostSerialization(result)result.successfalse

「持续发得起」指单位时间内能持续送出的总数据量。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 控制。


官方文档的原话:超限不会崩游戏,但这次同步发不出去。

具体表现分两种。一种是单次 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 / sbyte1
short / ushort / char2
int / uint / float4
long / ulong / double8
Vector28
Vector312
Vector4 / Quaternion16
Color / Color324-16
string1+ 每字符约 1-3 字节(UTF-8)
数组4 字节长度头 + 元素总尺寸

记两条规则:一个 Vector3 是 12 字节,10 个 Vector3 已经 120 字节,离 Continuous 上限 200 不远;字符串容易爆,100 字符 ASCII 约 100 字节,UTF-8 中文 100 字符可能 300 字节,Continuous 直接超限。

合作防守世界的 GameState 估算:

currentWave int 4
enemiesRemaining int 4
phase byte 1
totalScore int 4
stateVersion int 4
phaseStartTime float 4
--
21 字节

GameState 这种简单结构 Continuous 完全装得下。加上「每个玩家的状态」之后会爆,那时候要分对象:每个玩家的状态放到玩家对象上,GameState 只保留全局聚合。第二部 9 章和 11 章会展开这个分摊设计。


写每一个新的同步对象时,做三件事。

第一件:选模式。 字段总尺寸是多少、变更频率是多少。频繁小变更走 Continuous,不频繁但关键的走 Manual。两者不能同时挂在同一个对象上(VRChat 会取最严格的)。

第二件:拆对象。 一个 GameState 不可能装下所有共享状态。原则是:玩家本人的姿态由 VRChat 头像同步搞定,物理对象交给 VRCObjectSync,剩下的状态再判断该归 GameState(全局聚合)还是该归玩家对象(per-player)。具体的 PlayerObject 与 GameState 分摊设计在第二部 9 章和 11 章。

第三件:监控发送。 在关键对象的 OnPostSerialization 里记 byteCount。等第 7 章上多客户端测试时打开 Debug View 6,能看到每个对象的实际发送字节数和 BPS。理论估算和实测对比,差距大就回头调整。


加在 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 这一个对象:

  1. 该用 Continuous 还是 Manual?为什么?
  2. 玩家数量从 4 变成 16 时,预算占用还够吗?
  3. 如果把每个玩家的状态拆到他自己的玩家对象上(第二部 9 章会教),GameState 这一份对象的预算是多少?这种拆法对带宽的影响是?

不需要写代码,能在脑子里把字节数估出来即可。


三个数字: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 汉化文档 · 网络规范和贴士 — 中文镜像。
  • 附录 · 术语表BandwidthLatencyContinuous SyncManual Sync 的客观定义。