网络
会告诉你何时变化的同步值,以及谁都能发送的带类型消息,场景里不用准备任何东西。
一个世界里的两个客户端要对同一件事达成一致,办法有两种,选错了就会安静地出错。
同步变量是所有人都持有的事实。 分数、门是开是关、轮到谁。它能留存:十分钟后进来的人不用谁重发,就能拿到当前的值。
消息是发生过一次的事。 打中了、回合开始了、按钮被按了。把它当变量发,出错的样子会像什么都没发生:同一个值赋两次只算一次变化,于是两次相同的攻击变成一次;晚来的人会收到自己没看见的那场战斗的最后一击,像刚刚发生一样;而且只有所有者能写,两个人打同一个目标就会争这个变量,其中一次攻击悄无声息地消失。
这个包两者都有,并且把通常出错的部分接管了。
public class Door : UdoniteBehaviour
{
private Synced<bool> open = new Synced<bool>();
public override void Start()
{
open.Subscribe(OnOpenChanged);
Room.Subscribe<Knocked>(OnKnocked);
}
public override void Interact()
{
open.Value = !open.Value; // 会复制,每个客户端的处理器都会跑
Room.Send(new Knocked()); // 包括发送者在内,每个人都收到一次
}
private void OnOpenChanged(bool value) => panel.SetActive(value);
private void OnKnocked(Knocked message) => sound.Play();
}
Room.Send 去的是整个房间,且任何人都能发。关于某一个物体的消息则在那个物体上流动,写成就是该物体的那个 behaviour 上的 this.Send,而那个需要拥有该物体。两者都在消息。
open.Value = ... 写入变量并请求复制,没有容易忘记的 RequestSerialization。Subscribe 会在值到达时在每个客户端调用你的处理器,而且是在整批更新落地之后,而不是其中一半还是旧值的时候。
谁需要拥有什么
所有权是 VRChat 的,不是这个包的:只有所有者对物体的写入会被复制。不同的是当你不是所有者时会发生什么。
| 你做的事 | 需要谁拥有 | 如果你不拥有 |
|---|---|---|
写 Synced<T> 的值 |
持有该字段的物体 | 写入只留在你的客户端,并被下一个到达的值覆盖 |
对这个物体的通道用 this.Send |
这个物体 | 什么也不会积攒、什么也不会送达,并在控制台说一次 |
对 NetworkBus 用 channel.Send |
那条总线所在的物体 | 同样 |
Room.Send |
不需要 | 每个客户端写进自己玩家物体上的总线,所以没有什么可争的 |
移动 NetworkTransform |
那个物体 | 由所有者发送,其他所有人朝它插值 |
唯一不需要所有者的是房间消息,这也是任何客户端都可能动手时优先选它的主要原因。物体的通道和同步字段最终都只有一个写入者,所以谁都可以动手的世界会先取得所有权:
if (!Ownership.IsMine(gameObject))
Ownership.Claim(gameObject);
pickup 或 Interact 通常已经取到了。MasterOwned 是相反的情形:继承它,就只有负责仲裁的客户端能拥有该物体,用于不许别人写的状态。
这里有什么
Synced<T> |
每个客户端都持有、并会告诉你变化的变量 |
Room |
发送和订阅带类型消息;谁在场、谁在仲裁 |
NetworkEvent |
带字段的消息,而不是一堆字节 |
NetworkBus |
某个物体自己的通道,适合关于该物体的消息 |
NetworkTransform |
位置、旋转、缩放,带插值和瞬移检测 |
Ownership |
谁拥有一个物体,以及取得或交出它 |
MasterOwned |
只允许仲裁客户端拥有的物体的基类 |
你不用去搭的东西
房间消息需要场景里的一个物体来承载,因为消息必须写进某个客户端拥有的东西,而这种东西无法在世界运行时创建。这个包会替你添加它,并在控制台说明一次原因。从不发送的世界不会被加任何东西。
如果你想自己放,就在带 VRCPlayerObject 的物体上加一个 NetworkBus,那它就会被使用。它按组件查找,而不是按名字。
Udonite → Open Window → Settings → Net 里有个开关管这件事:关掉它,不管世界发不发消息,这个包都不会再碰场景——放总线这件事就完全归你自己管了。默认是开着的。这一个设置放在 ProjectSettings/ 里,和排除列表一样会被提交,因为这是场景归谁所有的事实,不是某个开发者自己的偏好。