第 4 章 · 手动同步的生命周期
第 2 章和第 3 章都用过这一行:
RequestSerialization();第 2 章里写过一句「它是请求,不是立即发送」。这一章把这句话展开。理解这条生命周期是 Vol.2 后面所有「同步失败」「同步迟到」「同步多花了一倍带宽」类问题排查的基础。
同步模式:Continuous vs Manual
Section titled “同步模式:Continuous vs Manual”UdonBehaviour 的 Inspector 里有一个 Synchronization Method 字段,三个值各自含义不同。None 表示这个 UdonBehaviour 不参与网络,同步字段不会被同步、网络事件也发不出去。Continuous 是连续同步:Owner 端字段一变,VRChat 自动尝试发送,每次发送上限约 200 字节,单个连续同步的字符串约 126 字符。Manual 是手动同步:Owner 必须显式 RequestSerialization() 才会发送,每次发送上限约 49 KB(注意是上限,不是每帧 49 KB)。
数字以官方 Network Specs and Tips 为准。SDK 演进可能调整。
同一对象上多个 UdonBehaviour 模式不一致时,VRChat 会采用更严格的同步方式,例如 Manual 和 Continuous 混在一起时按 Manual 行为处理。VRC Object Sync 是专用同步组件,建议不要和承载大状态的 Manual 脚本挤在同一个 GameObject 上。
第二章实现 C 那种「按钮翻转 isOn」的场景默认 Continuous 就够。这一章想展开的细节,主要是 Manual 模式下的生命周期,因为 Manual 把每个时刻都暴露给了开发者。
一次手动同步的完整生命周期
Section titled “一次手动同步的完整生命周期”Owner 端:
┌─────────────────────────────┐你的代码 ─→ RequestSerialization() │ │ │ │ (等待下一个 network tick) │ ↓ │ OnPreSerialization() ← 这里更新真正要发的字段 │ │ │ (VRChat 把字段打包成数据包)│ │ (通过网络发出) │ ↓ │ OnPostSerialization(result) ← 报告 result.success / result.byteCount │ │ └─────────────────────────────┘远程客户端端:
(收到数据包) │ │ (VRChat 解包,准备应用字段) ↓ [FieldChangeCallback] 对应的 property setter │ (网络同步或 SetProgramVariable 更新字段时触发) │ (setter 里要自己写回 backing field) │ │ (整批同步数据应用完) ↓ OnDeserialization() ← 适合「已经全部收到,做整体处理」 OnDeserialization(DeserializationResult) ← 同上,可读发送/接收时间四个回调,每个都对应一个具体时刻。UdonSharp 里字段级回调通过 [FieldChangeCallback(nameof(SomeProperty))] 属性绑定到字段;当网络同步或 SetProgramVariable 更新这个字段时,新值会交给对应 property 的 setter,setter 里要自己写回 backing field。这背后接近 Udon Graph 的 OnVariableChanged 事件。
OnPreSerialization:发送前的最后一刻
Section titled “OnPreSerialization:发送前的最后一刻”OnPreSerialization 在 Owner 本地、序列化即将真正发生的那一帧触发。
OnPreSerialization 主要拿来做两类事:更新版本号或序号(stateVersion += 1)、打时间戳(sentAt = Time.realtimeSinceStartup)。把临时计算的派生数据写进同步字段、把 DataDictionary 转 JSON 字符串塞进同步字段(VRCJson 用法在第三部 19 章),也属于这一档。
不要在这里做耗时计算,它在 network tick 上跑,代价直接叠到本帧。如果一份「准备发送」的逻辑要改十个字段,先怀疑是状态结构没拆好,而不是把所有改动都塞进 OnPreSerialization。
[UdonSynced] private int stateVersion;[UdonSynced] private int currentWave;
public override void OnPreSerialization(){ stateVersion++; // 每次发出去都把版本号 +1}OnPostSerialization:发送后的回执
Section titled “OnPostSerialization:发送后的回执”OnPostSerialization(SerializationResult result) 在序列化尝试完成后在 Owner 本地触发。SerializationResult 有两个字段:
success(bool):这次发送是不是成功。byteCount(int):实际发送的字节数。
工程化同步层的入口在这一行:
public override void OnPostSerialization(SerializationResult result){ if (!result.success) { // 失败了。等下一次或者降级处理。 pendingResend = true; } Debug.Log($"[Sync] sent {result.byteCount} bytes");}为什么要关心 success?因为 Manual 同步有 per-object 的 rate limit:发送的数据越多,下一次能发送的时间窗口就被推后越远。如果在 success == false 的情况下继续狂调 RequestSerialization,结果只是把更多请求堆在队列里,VRChat 仍然按它的节奏走。这种时候应该让本地状态机停一下、降低发送频率、或者把 payload 切片。
第六部 36 章讲对象同步降级时会再次回到 OnPostSerialization,把它作为整个降级链路的事件源。
OnDeserialization 与字段级回调的区别
Section titled “OnDeserialization 与字段级回调的区别”远程客户端那边有两个层面的回调。一是字段级回调,每个发生变化的同步字段都会触发一次。在 UdonSharp 里通过 [FieldChangeCallback(nameof(SomeProperty))] 把字段绑定到一个 property;网络同步或 SetProgramVariable 更新字段时,不会直接写字段,而是调用这个 property 的 setter。setter 里要自己把 value 写回 backing field。二是 OnDeserialization,它是整批同步数据全部应用完之后再触发的一次。
字段级回调的最小写法:
[UdonSynced, FieldChangeCallback(nameof(CurrentWave))]private int _currentWave;
public int CurrentWave{ get => _currentWave; set { // FieldChangeCallback 不会自动写回字段,这里必须自己赋值 _currentWave = value; UpdateWaveLabel(); }}举个例子:Owner 在同一次序列化里更新了 currentWave 和 enemyCountThisWave,远程客户端的执行顺序是:
(VRChat 内部)→ 触发 CurrentWave property setter(参数是新的 currentWave,setter 内写回字段) → 触发 EnemyCountThisWave property setter(setter 内写回字段) → 触发 OnDeserialization() ← 此时整批同步已经应用完所以一条工程纪律:如果一个回调里要读多个同步字段,写在 OnDeserialization,不要写在字段级回调里。[FieldChangeCallback] 适合「这个字段变了我就立刻反应」,OnDeserialization 适合「整批应用之后做一次整体处理」。
OnDeserialization(DeserializationResult) 的扩展版会带 sendTime 和 receiveTime(基于 Time.realtimeSinceStartup),可以算出端到端延迟,做更精细的体感优化。第五部 28 章会用到。
一个真实的最小手动同步示例
Section titled “一个真实的最小手动同步示例”合作防守世界的「波次状态」对象。它要同步:当前波次号、本波剩余敌人数、阶段(准备 / 进行 / 结算)。这些字段不需要每帧更新,但每次更新都要可靠送达,适合 Manual 模式。
using UdonSharp;using UnityEngine;using VRC.SDKBase;using VRC.Udon.Common;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]public class WaveState : UdonSharpBehaviour{ [UdonSynced] public int currentWave; [UdonSynced] public int enemiesRemaining; [UdonSynced] public byte phase; // 0 prepare / 1 fight / 2 settle [UdonSynced] private int stateVersion;
public void StartNextWave() { if (!Networking.IsOwner(gameObject)) return; currentWave++; enemiesRemaining = currentWave * 5; phase = 1; RequestSerialization(); }
public override void OnPreSerialization() { stateVersion++; }
public override void OnPostSerialization(SerializationResult result) { if (!result.success) { Debug.LogWarning($"[WaveState] send failed at v{stateVersion}"); } }
public override void OnDeserialization() { // 这里所有 [UdonSynced] 字段都是最新值,可以做整体应用 UpdateUIPanel(); }
private void UpdateUIPanel() { // 把 currentWave / enemiesRemaining / phase 同步到 UI }}这段代码包含 Manual 模式下的所有关键元素:[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] 类级别的标记、Owner 检查、RequestSerialization、四个回调里只写有意义的两个、版本号自增。后面所有 GameState、玩家请求、卡牌出牌之类的同步对象都会复用这套结构。
回到第 2 章实现 C 的按钮,把它从 Continuous 改成 Manual。改动需要:
- 在类上加
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]或在 Inspector 里把 Sync Method 改成 Manual。 Interact里的RequestSerialization()保留(其实之前 Continuous 时这一行可以省略,但写了也没坏)。- 加上
OnPostSerialization(SerializationResult result)回调,把result.success打到Debug.Log。
改完之后多客户端测试一次,按几下按钮观察日志:每次按下都能看到一条「sent N bytes」。这是 Manual 模式给的第一手反馈:你能看到每次同步真的发出去了多少。
进阶问题:如果连续在 0.5 秒内按 10 下按钮,日志会有多少条?为什么?
(提示:rate limit + network tick 的组合。多余的 RequestSerialization 不会失败,但被合并到下一次实际发送里。)
这一章做了什么
Section titled “这一章做了什么”Manual 同步有四个明确时刻:请求、OnPreSerialization、发送、OnPostSerialization;远端有字段级回调(UdonSharp 用 [FieldChangeCallback])和 OnDeserialization(整批级)。RequestSerialization 是请求,下一个 network tick 才生效,可以重复调用、rate limit 会合并它们。OnPostSerialization(result) 是工程化同步层的入口,能告诉这次发送的成败和字节数。抢权后改字段必须 RequestSerialization,这一条是 Vol.2 同步代码里复用率最高的纪律。
- VRChat Creator Docs · Network Specs and Tips — Continuous / Manual 上限、rate limit、
SerializationResult字段的官方数据。 - VRChat 汉化文档 · Network Components — 中文版回调说明。
- 社区文章 · UdonSharp 同步方法指南 —
OnPreSerialization/OnPostSerialization的实战用法。 - 附录 · 术语表 —
Continuous Sync、Manual Sync、RequestSerialization、OnPreSerialization、OnPostSerialization、OnDeserialization的客观定义。