Skip to main content

Udon Graph 基础教程-简单的网络同步

本文所讲的网络同步部分只是基础中的基础!如果要深入研究请观看官方文档https://creators.vrchat.com/worlds/udon/networking/

本期依然会以官方文档为基础进行讲解,并且在容易产生疑问的地方安插讲解——如果你还是不懂,请私信,这就是绝对是写的人的问题而不是你的问题!


VRC Graph:简单的网络同步

大家好,欢迎来到本期UDON Graph网络同步篇章,本篇将会以简单的方式来讲述一些关于网络同步的故事……

概述:网络原理

Udon 中用于联网的三个主要概念是 变量 、 事件 和 所有权 。

变量是值的容器 - 如数字、一组颜色或 3D 位置。

事件是在某个时刻发生的事情,也就是Event

所有权是决定基于哪个用户的本地数据为准,在同步时会将拥有所有权的玩家的数据同步给其他人


所有权(Owner)

默认情况下,世界中的对象是本地的。这意味着您捡起的物体只会为您移动,其他人不会看到它在移动。要同步对象,您需要告诉 VRChat 您希望它成为”网络同步对象(Networked对象)“

要使对象联网,您可以向其添加 UdonBehaviour 和/或 VRC 对象同步组件(Vrc Object Sync)

第一个打开世界的玩家将拥有所有权——是的,一般来说房主=所有者(Owner),如果这个地图没有发生所有者转让(后面我们会提到),一般来说这个所有者会一直由房主继承,直到房主退出房间,所有者会随着房主的转移而转移。

拥有所有权的玩家会成为所有Networked Objects以及UdonBehaviour脚本的所有者——当这些中的某些发生更改的时候,VRC将会以它为基准,更新数据。而其他人只需要接受来自所有者的数据即可。

当我们通过(set Owner)来更改所有者的时候,所有权将会转交给新的人——房主不会转交,只有所有权会转交。而当新的数据需要同步时,VRC将会以新设置的所有者为基准来同步数据。当新的所有者退出房间时,他会优先转移给房主。

示例:最简单的网络同步

大家一定在很多教程中都见过一个组合——Pickup+VRC Object Sync,就像是这样

922418cdf7e65eaf41f330298acca8a1.png

这个组合,就是VRC中最简单的网络同步

其中的VRCObjectSync的作用就是标记这个物体为”网络同步对象“,并且自动同步对象 - 将其位置、旋转、缩放和一些物理属性发送给其他玩家

注意:VRCObjectSYNC脚本有个特殊之处——他会自动将所有权转移到拿着这个物体的人手中,并且将以拿着它的人为基准为其他人发送数据。后面将会为您讲述这——为什么特殊

Master(房主/实例主)

Master是拥有从未手动设置或转移其所有权的任何对象的玩家。我们可以通过VRCplayerAPI中的”Is master“来确定玩家是否是房主。

8b45482d96fbbf7876307c647d2fce5d.png

玩家是否是 instance master 不应用作对世界某些特征的访问的门控。为此,请考虑改用 “instance owner”(VRC原文)

1919c77e70845af5e3f1f68cdd4bbab4.png

Master玩家选择遵循以下规则:

  • 实例中将始终存在有效的Master
  • 第一个进入先前为空实例(没有玩家)的玩家将成为初始主控。
  • 当Master Player离开实例时,Master会发生变化。
    • 如果Master Player在 Android 上并将 VRChat 置于后台太长时间,他们也可能会发生变化。
  • 当当前 master 离开时,将从 instance 中的其他 player 中选择一个新的 master,然后再被调用。OnPlayerLeft
  • 你不能依赖任何特定的玩家成为Master。将根据服务器端的各种标准(平台、网络条件等)选择新的主玩家。

这些是 VRChat 目前对网络主控行为所做的唯一保证。观察到的任何其他行为都可能发生变化。

在构筑网络同步的时候,请不要依赖于Master机制

请使用Get Owner来获取某个物体/脚本是由谁来更新的,而不是一味的默认这个物体会由Master来进行更新

在某些情况下,Master Player可能会在一段时间内无响应,并且在此期间的事件可能不会执行。

为了解决这个问题,我们提议在任何需要同步的重要参数时,使用SET Owner先转移所有者至确实正在正常运行的Player——他们一般也会是网络同步事件的申请者。

变量

变量是值的容器。UdonBehaviours 运行 Udon 程序,你可以向这些程序添加变量。

a297a81c48dd5443f8f2e7378115b63d.png

在UdonGraph中,任何可以进行同步的参数都会有Synced的选项,当您选择它为需要同步的参数时,他前面的勾会亮起且后面会出现名为”SMOOTH的选项“

Smooth选项为参数平滑——选择None以外的任何选项都会导致您的参数的值不会以绝对正确的形式同步到其他人身上。

示例:同步滑块

本文将把这个部分改造为使用UDONGraph来进行同步,如果需要阅读原版,请访问

https://creators.vrchat.com/worlds/udon/networking/

fae4662319c6204ea5611fe2db1e0df7.png

在此示例中,Slider 的 Owner 将其值同步到其他所有人。

要同步 Slider,我们只需要获取它的值。这将是一个小数点在 0 到 1 之间的数字,我们称之为浮点值,或简称浮点数。因此,我们创建一个名为 sliderValue 的变量,其类型为 float

070289df8138f3bb5faef97215d1332a.png

将滑块设置为在移动滑块时更新此值,当滑块的值更新时,会向UdonGraph脚本发送一个名为:”Valueupdate“的CustonEvent事件。UdonGraph会从Slider中获取值并且存入到已经标记为SYNC的值SliderValue中。

8630497e1c0d3eb6a6346bc0833215c4.png

b2a09a7f1d66188af98ab399fb0f97de.png

如果此时您的脚本设置为:Continuous(自动同步),那么太好了——您的参数会在更改时自动同步给所有人,接下来您只需要让其他玩家在收到同步时,更新以下slider值就行,恰巧,VRCEvent中的OnDeserialization可以完美完成我们的要求。

44226743ded00cdf9a9555be4eccd9a3.png

只要在参数更新时,将更新的参数同步到Slider上,我们就完成了一次同步。恭喜你,完成了第一个网络同步脚本。

从Owner上传同步,这称为 Serialization。玩家接受并同步参数,这称为 Deserialization(来自官方)

事件

事件发生,然后执行,之后它们就消失了。与只能由对象的 Owner 更新的变量不同,任何人都可以对 Object 调用事件——在本地。

但是事件的同步不需要通过Owner来进行——只需要使用SendCustomNetworkEvent来发送事件,任何的人都可以从发送同步事件

注意这里的区别

参数同步:基于Owner,并且只会同步Owner的参数 

事件同步:由发起者发出,发起者即为调用SendCustomNetworkEvent的人。他并不要求发起人时Owner或者master

在发起事件同步的时候,我们可以选择这个事件时发出给所有人,还是只发出给Owner,这个可以在 target选项中选择

de91d804a0e880fc60d7d177bc6639b8.png

示例:泡泡枪

(以下为官方示例——官方终于记得他还有个UdonGraph了)

在此示例中,我们有一个具有粒子系统的对象和一个动画师,该动画师旋转其气泡棒并生成气泡粒子。我们希望当用户握住魔杖按下扳机时,世界上的每个人都能做到这一点。

在我们的 Udon Graph 中,我们有一个称为 “Trigger” 的自定义事件,它播放 'Spin' 动画并触发 22 个粒子来发射 - 这只是我们图表中的一个局部事件。

为了实现这一点,我们绑定了 OnPickupUseDown 事件,该事件在有人拿着泡泡枪按下 Use 时触发,我们使用 SendCustomNetworkEvent,目标为 All 来触发所有人的“Trigger”事件,包括对象的所有者。

udon-networking-e21b3b0-bubble-gun-graph-1e56fe00c5fc0c771b773b149cbef4b9.png

网络同步概念:迟到者

在发生一些同步后,加入你的世界的人会发生什么情况?很简单:变量将被更新,事件不会。当有人加入您的世界时,OnDeserialization 事件将针对世界上具有最新数据的每个 Networked Object 触发,并且他们将运行您现有的任何逻辑来根据该数据更新内容。但是——事件不会:因为事件的概念的即时的,在事件完成之后,就消失了——就像是没有理由在有人按下扳机一小时后发射气泡粒子一样

概述 回顾

同步是通过 Variables 和 Events 完成的。对于变量,Owner会更新变量,并将该数据发送给所有其他 Deserialize 该变量的玩家。任何进入世界的人都会获得最新的数据来 Deserialize。对于事件,任何人都可以发送 NetworkEvent。届时,所有者或世界上的每个人都会收到它。

官方网络同步示例包

官方网络同步示例包.package

官方上面的三个示例包含在一个简单的包中,您可以导入到任何具有 Udon SDK 的项目中,以便查看它们的工作情况并自己探索


同步方式

VRC有四种方法可以同步 World 中的数据和事件

1. 连续变量(Continuous)

当你有一个想要频繁更新的变量时,请使用此选项,如果它有时不更新以节省其他事情的带宽,那也没关系。这将为较晚加入的人同步。

6d3cb6efff2df9c1a90974793002124a.png

2. 手动变量(Manual)

当你有一个更新频率较低的变量时,请使用此选项,并且其值始终是最新的,这一点非常重要。这将为较晚加入的人同步。此选项与 Object Sync 不兼容。

image.png

需要配合 RequestSerialization(同步申请)使用

2fbcdcb6a5d1b3e2be8aed53ca9c6696.png

 

3.自定义网络事件(SendCustomNetworkEvent)

de91d804a0e880fc60d7d177bc6639b8.png

使用此选项可为实例中当前的每个玩家或对象的所有者触发事件。它肯定会到达,但会有相当大的延迟和开销。发送事件之后加入的任何人都不会收到此事件。

4. 自动

某些特定于 VRChat 的对象会自动同步。这包括:

  • 玩家(Players):包括他们的位置、声音和 IK 运动。
  • VRCObjectSync:包括对象的 Transform 和 Rigidbody

对象所有权Owner

在前文中我们已经十分详细的叙述了所有权的概念,让我们看看VRC官方是如何定义的:

在 VRChat 中,每个联网的游戏对象一次都由一个玩家“拥有”。只有对象的所有者才能更改其同步的 Udon 变量或影响其对象同步。然后,可以将这些更改发送给实例中的其他所有人。如果您希望玩家能够更改对象上的变量,请务必先检查或请求所有权!

第一个加入实例的玩家将成为实例主服务器。默认情况下,他们拥有所有联网对象。如果该玩家离开,则其他玩家将成为 instance master。它们也将成为前一个实例 master 对象的所有者。

当玩家使用 Object Sync 组件拾取拾取物时,他们将自动成为该对象的所有者。所有者会不断将拾取物的位置发送给其他玩家。

可以通过调用 Udon 来更改对象的所有权。这将导致实例中的每个玩家调用 ,其中 是对对象的新所有者的引用。新所有者可以立即更改同步的变量。(如果您的脚本使用手动同步,请不要忘记在更改 vairables 后调用 call。Networking.SetOwner(VRCPlayerApi player, GameObject obj)OnOwnershipTransferred(VRCPlayerApi player)playerRequestSerialization()

当玩家拥有一个对象时,该对象可能会不断将同步的数据发送给其他玩家,例如该对象的位置或同步的 Udon 变量。

请求所有权 (Advanced)

如果你需要手动将所有权赋予给其他玩家,您可以使用set owner来做到这一点

0451233c89249d755a7c2d3b430dd123.png

当这个模块被Flow激活时,绑定在OBJ上的任何网络同步物体/脚本的所有权将会转交至Player接口上的玩家——您可以使用Get LocalPlayer来指定这个对象为本地玩家。

下面这部分十分晦涩难懂——一般来说你也不会用到,要不跳过把~~~


我警告过了哟~那么我们继续看吧

如果您希望对象的所有者能够接受或拒绝所有权转移,请将该事件添加到您的脚本中。OnOwnershipRequest(VRCPlayerApi requester, VRCPlayerApi newOwner)

通过添加到脚本,将在所有权转移期间执行其他步骤

  1. 与以前一样,请求玩家必须调用才能开始所有权转移。Networking.SetOwner(VRCPlayerApi player, GameObject obj)
    • “请求”玩家可以是任何玩家,也可以是所有者。如果(前)所有者发出了请求,他们将跳过第 4 步和第 5 步。
    • 所有者可以将所有权授予任何人,但非所有者只能为自己请求所有权。(没有此限制的脚本没有此限制。OnOwnershipRequest()
  2. OnOwnershipRequest(VRCPlayerApi requester, VRCPlayerApi newOwner)请求玩家调用。
    • 请求玩家必须返回到请求。否则,请求将提前取消。true
  3. OnOwnershipTransferred(VRCPlayerApi player)请求玩家调用。
    • 这种情况发生在所有者确认所有权之前。如果转让被拒绝,所有权可能会更改回来。
  4. OnOwnershipRequest(VRCPlayerApi requester, VRCPlayerApi newOwner)所有者调用。
    • 如果所有者返回 ,则接受所有权转移。(在 Udon Graph 中,用于返回 。trueSetReturnValuetrue
    • 如果所有者返回或根本不返回值,则所有权转移将被拒绝。 呼叫请求玩家,通知他们初始所有者仍然拥有该对象。falseOnOwnershipTransferred()
    • 如果所有者将所有权转让给某人,则会跳过此步骤。新玩家无法拒绝所有权。
  5. 如果请求被接受,则由前所有者所有其他玩家调用。OnOwnershipTransferred(VRCPlayerApi player)e3ebd62b6281060b788e9fba080ee132.png

没听懂是吗?那就让我来在解释一次~~~~

我们来假设一个场景——一个

1.本地玩家申请成为Owner,后称为申请玩家,他需要触发SetOwner,如下:

ba835bff901e95054cd8669461fdd6ec.png

我们将OBJ留空——这将会默认为我们的脚本

2.申请玩家接收到OnOwnershipRequest,并且同意转移

7855d2ab0f1872d22a74d01d7e9ae6da.png

如果申请玩家返回False或者不返回任何值,这一次申请将会直接结束——其他人不会知道发生了什么

3.申请玩家接收到OnOwnershipTransferred

588051b07f5a8f1b1d1db74e62d3fcfa.png

4.原Owner收到申请并触发OnOwnershipRequest,原Owner需要返回True来同意这次转移。

7855d2ab0f1872d22a74d01d7e9ae6da.png

如果原Owner返回False或者不返回任何值,那么这次所有权转移将会失败,申请人的OnOwnershipTransferred会触发并且返回原Owner的名字(可以认为是告知申请失败)

5.当原Owner同意了这一次转移,那么原Owner的OnOwnershipTransferred将会被触发,并且输出新的Owner的信息(可以认为是告知自己不再是Owner),随即其他玩家将会收到OnOwnershipTransferred,并且接收到新的Owner的信息。

到这里Owner转移结束。

在Owner转移结束时,转移结果并不会以任何形式告知新Owner——所以我建议您将该脚本的同步方式改为Continuous(自动同步)并且让其尽量独立——我们并不确定后续的事件和Owner权转交完成哪一个会先执行


使用变量

使用变量同步数据分为三个步骤:
  1. 创建变量。
  2. 更新 Owner 上的值。
  3. 对从 Owner 收到的值变化做出反应。

创建变量

  1. 单击 Variables 窗口中的 + 按钮
  2. 选择变量类型
  3. 重命名您的变量(可选,但请执行此操作)
  4. 点击变量名称旁边的箭头以显示更多选项,开启 'synced'。

更新 Owner (所有者) 上的值

  1. 在按住 'Ctrl' 的同时将变量拖放到图表上,以创建 'Set Variable' 节点。
  2. 将事件或流连接到此节点上的流端口,并将新值连接到值端口。
  3. 如果此 UdonBehaviour 正在使用自动同步(在 Inspector 中的 UdonBehaviour 本身上选择),则该值会自动更新。如果您使用的是手动同步,则需要添加一个“UdonBehaviour.RequestSerialization”节点,并将 Set Variable Flow Port 的输出连接到此节点上的 Flow Input 端口。您可以将此节点上的 'instance' Value Port 留空,它将默认为当前的 UdonBehaviour,这就是我们想要的。

6eadd7e1f274f2da1d1f957dd462d361.png

网络同步变化做出反应
  1. 将 “OnDeserialization” 节点添加到同一图表中。
  2. 将变量拖放到图表上,无需按住 Ctrl 键即可创建“Get Variable”节点。
  3. 使用来自 OnDeserialization 节点的流和 Get Variable 节点中的值,使用此新值更新另一个节点。

44226743ded00cdf9a9555be4eccd9a3.png


请求序列化

此节点在 手动同步 模式下用于标记目标 UdonBehaviour 上的变量,以便在下一个 Network Tick 期间进行序列化,这不会每帧发生。此节点与 OnPreSerialization Event 节点配合极好。触发 “RequestSerialization”,然后 OnPreSerialization 事件将在下一个 Network Tick 期间触发。此时,您可以将任何变量更新为要同步的值。

您可以同步以下类型的变量和变量数组:

  • 整数类型
    • bytesbyte
    • double
    • float
    • intuint
    • longulong
    • shortushort
  • 复合类型
    • ColorColor32
    • Quaternion
    • Vector2Vector3Vector4
  • 文本数据类型
    • stringchar
    • VRCUrl
  • 逻辑数据类型
    • bool

官方警告!

当同步带有同步数组变量的行为时 - 确保始终将这些数组初始化为某个值,例如空数组。如果任何同步的数组未初始化 - 行为将不会同步!您可以通过 OnPostSerialization 节点检查序列化是否成功


使用自定义事件

SencustonNetworkEvent

Udon 脚本可以为实例中的其他玩家触发自定义事件

  1. 确保您的 UdonBehaviour 的同步模式设置为“连续”或“手动”,而不是“无”。
  2. 创建 “Event Custom” 节点。
  3. 使用该节点的输入框为其指定唯一名称。
  4. 添加 “Send Custom Network Event Node”(发送自定义网络事件节点)。
  5. 在输入中输入相同的事件名称。eventName
  6. 将默认值保留为目标,以便在 Room 中的每个 Player 上触发此事件,或将其更改为仅在 Owner 上触发此事件。AllOwner
  7. 您可以将输入留空以定位当前 UdonBehaviour,或将引用连接到另一个 UdonBehaviour 以在该 UdonBehaviour 上触发自定义事件。instance

c16c21b92033c16bb19f65e89384e416.png

仅限本地的活动

如果事件名称以下划线开头,则无法通过网络调用它们。Udon 这样做是为了保护 VRChat 的内部方法(如 _start、_update 和 _interact)免受恶意网络调用的侵害。VRChat 计划向事件添加一个属性,以将其标记为“仅限本地”,而无需下划线。如果要同时阻止远程执行事件,可以使用唯一的下划线前缀(如“_u_eventName”)来确保它与任何现有或将来的 VRC 方法不匹配。

事件参数

Udon 目前不支持发送带参数的事件。无法将事件发送到特定Player(Owner除外)。

解决方法 1

为每个玩家提供对游戏对象的所有权,这样每个玩家现在都有一个与他们关联的 UdonBehaviour。使用 Networking.GetOwner(GameObject) 找出要将事件发送到哪个 UdonBehaviour 以定位指定玩家。此方法的设置非常复杂。

解决方法 2

使用同步变量(displayName 的 string 或 playerID 的 int)告诉每个人指的是哪个玩家。但是,由于下面将进一步描述同步变量/网络事件交互的问题,此方法有点棘手。


调试

如果您在 VRChat 中按调试菜单 8 启动并进入调试菜单 8,则可以在客户端中查看有关联网对象的一些信息。 这些叠加层向您展示--enable-debug-guiRight Shift8

  • 游戏对象的网络 ID
  • 游戏对象的显示名称
  • P: Ping 时间,
  • Q:数据质量(100% 表示没有丢弃的数据包)和
  • O:游戏对象的所有者。

网络相关Event速查:

以下事件作为 Networking 系统的一部分提供,用于控制数据的同步方式。

OnPreSerialization(发送数据准备)

此事件在发送序列化数据之前触发,这是设置要为其他玩家更新的同步变量的好地方。

OnDeserialization(接受数据完成)

当同步数据已从字节转换回可用变量时,将触发此事件。它不会告诉您哪些数据已更新,但可以作为更新监视同步变量的所有内容的起点,或者是检查新数据与旧数据并进行特定更新的地方。

OnDeserialization<DeserializationResult>(接收数据·详细版)

与 OnDeserialization 相同,但包含有关发送和接收请求的时间的其他信息。

OnPostSerialization(发送完成触发)

此事件在尝试发送序列化数据后立即触发。

OnOwnershipRequest (请求所有权-申请流程触发)

当有人请求获得所有权时,将触发此事件。它包括 Requester 和 Requested Owner 的 PlayerObjects。要批准或拒绝更改,请在 “Set Return Value” 节点中设置布尔值。此逻辑在请求者和所有者上本地运行,因此请注意,两者之间的逻辑不一致将导致不同步。这很可能表现为所有权转让被所有者意外拒绝。

OnOwnershipTransferred(请求所有权-所有权转移触发)

当对象所有权发生更改时,将对实例中的每个人触发此事件,并包括新所有者的 PlayerObject。

OnMasterTransferred(房主已离去触发)

当实例 master 因前一个实例 master 已离开实例而发生更改时,会为实例中的每个人触发此事件。 它包括一个参数,即已成为主控的玩家的VRC-Player-API对象。此参数始终有效。 对于第一个加入新实例的用户,此事件将在之后触发,以指示 master 状态是从 “nobody” 转移的

OnVariableChanged<变量>(指定变量发送变化后触发)

这是您可以为任何变量创建的特殊类型的事件。在 Udon Graph 中,您可以通过按住 alt 将变量拖放到图表中来创建它。此事件检测变量何时更改,其中可能包括您何时收到来自其他玩家的同步变量。

  • 更改数组的内容不会触发更改,因为数组本身仍然相同。
  • OnVariableChanged 在写入变量本身时立即触发,这与 OnDeserialization 不同,后者在写入完所有同步变量后触发。这意味着,如果您从一个同步变量中使用 OnVariableChanged 并尝试获取其他同步变量的内容,则无法保证它已使用最新的同步数据进行更新。

本期教程到这里结束