同步值

每个客户端都持有的变量、能往里放什么,以及两种需要把变化说出来的情况。

更新于 2026-09-10

声明一个

同步值是 behaviour 上的一个字段,你也可以用自动属性:

private Synced<int> score = new Synced<int>();

public Synced<string> Message { get; } = new Synced<string>();

两者都会变成一个按你所写命名的复制变量。自己写访问器的属性无法工作,会被拒绝:Udon 复制的是有名字的变量,而访问器是在每个客户端上重新运行的代码。

它必须在 behaviour 上。放在 behaviour 使用的普通类里的同步值能编译却永远不会复制,所以那种写法也会被拒绝,并指出是哪个字段。

能放什么

所有 C# 基元类型、stringVector2Vector3Vector4QuaternionColorColor32VRCUrl,以及它们的数组。这是 VRChat 的清单,不是这个包的。其他类型会在编译时被拒绝,而不是在运行时被悄悄丢掉。

初始值放在构造函数里:

private Synced<int> lives = new Synced<int>(3);

读和写

score.Value = score.Value + 1;

写就会复制。没有需要配套调用的序列化,也就没有了“赋了值却忘了调用、同步于是悄悄停掉”这种失败。一帧里写多次仍然只发一次,VRChat 会合并。

只有所有者能写。 在别的客户端上赋值只会留在本地,这是这个包唯一没有替你藏起来的 Udon 规则。要么先用 Ownership.Claim(gameObject) 取得所有权,要么把这个值放在本来就由正确客户端拥有的物体上。

听到变化

score.Subscribe(OnScoreChanged);

值到达时,每个客户端的处理器都会跑,包括写入的那一个。同一个处理器订阅两次不会多加,Unsubscribe 可以移除。

想同时知道它从什么变过来的处理器,两个都接收:

score.Subscribe(OnScoreChanged);

private void OnScoreChanged(int previous, int current)
{
    scoreboard.Show(current, current - previous);
}

第一次变化之前没有更早的值,所以旧值是该类型的默认值。同一个值可以同时订阅两种形态,两个都会跑。

处理器在整批更新落地之后才跑。 VRChat 一次应用一个变量,所以逐个到达就调用的话,处理器读到的另一个值可能还是旧的。这在两个客户端上量过:订阅者看到计数器到了,旁边的字符串还是空的。现在处理器可以读同一批更新里进来的值。

数组

数组是整体复制的,所以整个替换会如你所料:

private Synced<int[]> scores = new Synced<int[]>(new int[8]);

scores.Value = fresh;

写其中一个元素,是编译器只看赋值看不出来的情况,所以要说出来:

scores.Value[2] = 7;
scores.MarkChanged();

MarkChanged 会复制,并通知本客户端的订阅者,和整个数组赋值时完全一样。没有它,这次改动会悄悄留在写入的客户端上。

所有权

Ownership.IsMine(gameObject) 这个客户端能否写该物体的同步值
Ownership.Claim(gameObject) 若无人拒绝就取得
Ownership.GiveTo(player, gameObject) 交给某人
Ownership.Of(gameObject) 谁拥有它

重写 OnOwnershipRequest 并返回 false 就能拒绝转移。MasterOwned 就是写好的那个重写:继承它,就只有仲裁客户端能拥有该物体,这是放置不许别人写的值的地方。

ClientSim 不会触发所有权请求,这一项必须在上传的世界里验证。

出了点问题 重新加载