第 2 章 · 四种通道:Local、Event、Variable、Object Sync
第 1 章把状态分成四象限。这一章把这四象限对到 VRChat 给的四种通道,每一种都用同一个按钮例子写一遍。
按钮的承诺很简单:
- 在场景里有一颗按钮。
- 任意玩家按下后,旁边那盏灯亮起。
只要再问一句「灯亮多久」「迟入玩家看不看得见」「Master 离开会不会熄」,这同一个按钮就会逼出四种完全不同的实现。
默认的选择顺序
Section titled “默认的选择顺序”新项目接到一个新需求时,按这个顺序问下去:
- 能不能完全本地? 玩家自己看到反馈就够,那就别上网。
- 要让别人看到这件事一次,事后不重要? 用网络事件。
- 要让别人随时知道当前是什么状态,包括迟入玩家? 用同步变量。
- 要同步的是物体的位置 / 旋转 / 物理? 用
VRCObjectSync。
走到第几步停,由「这件事的状态结构」决定,不是由「这个对象看起来像什么」决定。能停在第 1 步就停在第 1 步,是 Vol.2 的工程纪律。
实现 A:纯本地
Section titled “实现 A:纯本地”只在按下的那个客户端上把灯亮起来。
public class LocalButton : UdonSharpBehaviour{ public Light targetLight;
public override void Interact() { targetLight.enabled = true; }}效果:按按钮的人看到灯亮了。其他玩家看到的是按按钮的人正在跟一颗按钮交互,但灯没有任何反应。
这不是 bug。Interact 只在按下交互的本地客户端触发,targetLight.enabled 只改本地组件状态。
什么时候要这种?最常见的是私有的 UI 反馈,比如鼠标悬停高亮、本地音量滑块、个人观感切换。调试用的临时开关(「让我自己看一眼这块墙关掉是什么样」)和单人解谜的本地状态机也走这一档。
第 1 章四象限里**「私有 × 任意」**的所有状态默认都走这一档。Vol.2 后面所有「这一项不需要同步」的判断,本质都在说「能不能停在实现 A」。
实现 B:网络事件
Section titled “实现 B:网络事件”让所有玩家看到一次「灯亮了」。
public class EventButton : UdonSharpBehaviour{ public Light targetLight;
public override void Interact() { SendCustomNetworkEvent(NetworkEventTarget.All, nameof(LightUp)); }
public void LightUp() { targetLight.enabled = true; }}效果:任意玩家按按钮,所有在场玩家都看到灯亮了一下。
但是迟入玩家看不到这件事曾经发生过。如果 30 秒前有人按过按钮、灯被点亮,新进来的玩家看到的灯仍然是默认的灭状态。
什么时候要这种?最典型的是一次性视觉反馈:陷阱触发的爆炸、击杀提示、捡到道具的弹幕公告。开门「叮」一声、广播鸣笛之类不需要回看历史的瞬时音效同理。
四象限里**「共享 × 瞬时」**的所有动作走这一档。
实现 C:同步变量
Section titled “实现 C:同步变量”让所有玩家(包括迟入玩家)都看到「现在灯是开的」。
public class StateButton : UdonSharpBehaviour{ public Light targetLight;
[UdonSynced] private bool isOn;
public override void Interact() { if (!Networking.IsOwner(gameObject)) { Networking.SetOwner(Networking.LocalPlayer, gameObject); } isOn = !isOn; ApplyLight(); RequestSerialization(); }
public override void OnDeserialization() { ApplyLight(); }
private void ApplyLight() { targetLight.enabled = isOn; }}这一段是本卷的第一段「认真」UdonSharp。逐行看:
[UdonSynced]把isOn标记成同步字段。值变了,VRChat 网络层会把它推给其他客户端。Interact里先把所有权抢过来。isOn只有 Owner 改才会被同步,非 Owner 改了也不会发出去。Owner 的概念第 3 章会专门讲,这里先记规则:改之前先 SetOwner。- 把组件的状态翻转之后,本地直接
ApplyLight(),让按下的人马上看到反馈。不能等同步回环回来再亮,那是几十毫秒之后的事。 RequestSerialization()告诉网络层「我改完了,下一次网络刻请把它发出去」。注意它不是立即发送,原文是「请求」,第 4 章会展开。- 远端收到数据后会触发
OnDeserialization,里面再次ApplyLight()把灯切到新状态。
迟入玩家进来的时候:VRChat 把当前 isOn 的最新值送给他,触发一次他本地的 OnDeserialization,灯就立刻是对的状态了。
四象限里**「共享 × 持续」**的状态走这一档。Vol.2 大部分时间都在和这一档打交道。
实现 D:VRCObjectSync
Section titled “实现 D:VRCObjectSync”如果按钮本身需要被推、被拽、能掉到地上、能被另一个人捡起来,再加同步变量就不够了,需要把物体当前的位置和旋转同步出去。
这种场景下不写代码,把 VRC Object Sync 组件挂上去即可。它做的事是同步 GameObject 的 Transform 位置和旋转,围绕带 Rigidbody 的可移动物体使用。官方组件页明确写的是位置和旋转,不要把自定义物理状态当成已同步字段依赖。它还提供 Respawn()、FlagDiscontinuity()(瞬移时跳过插值)等少量 API。
这是为「有物理表现的物体」设计的专用通道。它不负责同步 Udon 字段;如果同一个对象还要承载手动同步的大状态,通常把状态脚本拆到另一个 GameObject 上,避免同步模式互相影响。
四象限之外的「物理 / Transform 同步」单独走这一档。第六部会专门展开 VRCObjectSync 的限制和它的几个社区替代方案的取舍。
把四种实现并排
Section titled “把四种实现并排”| 维度 | A · 本地 | B · 网络事件 | C · 同步变量 | D · VRCObjectSync |
|---|---|---|---|---|
| 写法 | 本地代码 | SendCustomNetworkEvent | [UdonSynced] + RequestSerialization | 挂组件 |
| 别人能看到 | ❌ | ✅ 触发的瞬间 | ✅ 任何时刻 | ✅ 任何时刻 |
| 迟入玩家看到 | — | ❌ | ✅ 收到当前值 | ✅ 收到当前 Transform |
| 适合什么 | 私有反馈 | 一次性动作 | 持续状态 | 物理对象 |
| 通道开销 | 0 | 中 | 低 | 高(频繁) |
「能停就停」的意思是:能用 A 就别用 B,能用 B 就别用 C,能用 C 就别用 D。每往下一档,状态的复杂度、所有权问题、网络预算压力都上一个台阶。第 6 章讲网络预算时会回到这张表。
挑一个回合制卡牌世界的场景,列出至少五个状态或动作,给每个分配 A / B / C / D 一档。可以从下面这几样里挑:
- 玩家手牌(只对自己可见)
- 桌面上摆出来的牌(所有人都看到)
- 当前是谁的回合
- 抽牌时的「沙沙」音效
- 玩家面前那个 3D 杯子(可以被推倒)
写完之后再问一遍:有没有把「瞬时动作」错放进 C?有没有把「持续状态」错放进 B?有没有哪一项其实可以停在 A?
这个练习不需要写代码,目的是让选择动作变成肌肉记忆。
这一章做了什么
Section titled “这一章做了什么”同一个按钮可以写成 A / B / C / D 四种实现,每一种回答的问题不同。默认选择顺序是能停在 A 就停在 A,否则按 B → C → D 往下走。实现 C 的代码骨架([UdonSynced] + SetOwner + RequestSerialization + OnDeserialization)会在后面所有同步变量例子里反复出现。
- VRChat Creator Docs · Networking and Synchronization — 四种同步方式的官方说明。
- VRChat 汉化文档 · Udon 网络 — 中文镜像,含同步选型译者注与已知 bug 列表。
- VR Creators · Udon Networking Decision Guide — 社区版选型决策表,可作横向参考。
- 附录 · 术语表 — 本章涉及的同步通道术语的客观定义。