同步值
每个客户端都持有的变量、能往里放什么,以及两种需要把变化说出来的情况。
声明一个
同步值是 behaviour 上的一个字段,你也可以用自动属性:
private Synced<int> score = new Synced<int>();
public Synced<string> Message { get; } = new Synced<string>();
两者都会变成一个按你所写命名的复制变量。自己写访问器的属性无法工作,会被拒绝:Udon 复制的是有名字的变量,而访问器是在每个客户端上重新运行的代码。
它必须在 behaviour 上。放在 behaviour 使用的普通类里的同步值能编译却永远不会复制,所以那种写法也会被拒绝,并指出是哪个字段。
能放什么
所有 C# 基元类型、string、Vector2、Vector3、Vector4、Quaternion、Color、Color32、VRCUrl,以及它们的数组。这是 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 不会触发所有权请求,这一项必须在上传的世界里验证。