网络网格可视化脚本最佳做法
概述
在网格中,大多数场景属性默认在连接到同一聊天室的所有客户端之间自动共享。 例如,场景对象的转换位置和旋转、组件的启用状态或 TextMeshPro 的文本。
作为经验法则,默认情况下会自动共享具有以下值类型的组件属性和对象变量:
集合类型(列表和集)和场景对象引用不共享。
访问或修改网格中的属性的可视脚本节点使用标签进行标记,该标签指示它们是否为“由所有客户端共享”或“此客户端本地”:
如果已使用上面列出的值类型之一声明对象变量,则默认情况下也会共享对象变量:
网格不支持场景变量,但你可以使用环境中的独立 变量 组件来存储可以独立于任何特定 脚本计算机 组件共享的变量。
如果不希望自动共享属性或对象变量,可以将本地脚本范围组件添加到场景中。 这将在此游戏对象及其任何后代本地生成所有场景属性和脚本变量。
提示:可以在网格 101 教程的第 3 章中 查看有关如何使用本地脚本范围组件的示例,该教程 重点介绍可视化脚本。
对于仅在单个 脚本计算机上使用的本地脚本变量,最好使用 Graph 变量,这些变量永远不会通过网格在客户端之间共享。
通过网格可视化脚本共享可提供以下保证:
保证最终一致性:所有客户端最终都将达到相同的共享状态。
保证每个组件的原子性:对同一场景组件(或同一 变量 组件)属性的所有更新都将在每个客户端上以原子方式应用。
但是:
无排序保证:一个客户端应用到多个 不同 场景组件的更新可能会以不同的顺序到达不同的客户端。
没有时间线保证:网格将尽量尽快跨客户端复制状态更改,但网络条件可能会延迟某些或所有客户端上任何给定状态更新的到来。
无粒度保证:任何客户端都可能无法看到共享状态的所有单个增量更新。 当网络条件强制网格服务器进行速率限制更新时,可能会发生这种情况。 客户端后期加入会议室时也会发生这种情况。
状态为共享事件
无法使用网格可视化脚本发送或接收显式网络消息。 这起初可能令人吃惊,但它有助于建立一个网络范例,以便轻松统一处理运行时更改和后期联接。 场景属性和脚本变量中存在 共享状态 ,而不是消息。
脚本可以统一的方式响应共享状态更新,而不管这些更新是由本地脚本还是用户、共享同一聊天室中的体验的另一个客户端,还是由你自己加入之前已在聊天室中的其他客户端做出响应。
无法显式发送网络消息意味着必须开始考虑 获取更新 的共享状态,而不是 导致状态更新的共享事件。 共享事件是共享状态更新的结果,而不是相反。
幸运的是,网格视觉脚本使视觉脚本能够轻松响应状态更新。 使用“状态更改”事件节点,并将其左侧输入与想要观察的任何脚本变量或组件属性连接,每当连接到脚本的任何变量或属性更改其值时,事件节点都会触发脚本(连接到其右侧)。
这适用于共享状态和本地状态。 无论 观察到的变量或属性是由本地客户端、远程客户端,还是远程客户端在本地客户端加入会议室之前,都会触发 On State Changed 事件。
使用 状态更改 来响应状态更改是有效的:没有空闲带宽或性能成本。 你可以以这种方式被动侦听任意数量的视觉脚本来侦听状态更新,而不会对环境的帧速率或带宽使用产生负面影响。
延迟加入
当客户端加入已连接到其他客户端的聊天室时,将发生延迟联接。
在后期联接时,Mesh 从服务器接收聊天室的当前状态(例如,该服务器已位于会议室中及其头像所在的位置),并快速准备加入客户端的共享环境的本地版本,使其与会议室中每个人共享的状态匹配。
在大多数情况下,网格视觉脚本执行相同操作。 在本地更新客户端刚加入之前在会议室中更改的任何共享组件属性和可视脚本变量,以匹配共享状态,然后触发观察到这些属性或变量的任何 On State Changed 事件节点。
后期加入者不会重播共享事件--它们获得共享状态。
从本地客户端的角度来看,环境始终从加载上传到网格的场景后的初始状态演变。 对于延迟加入,第一个状态更改可能大于本地用户在正在进行的会话中与聊天室交互时发生的情况,但原则上情况完全相同。
这一切发生在环境加载之前,它甚至从黑色淡入。 一旦用户实际可以看到环境并与之交互,就会完成后期加入。
使本地状态遵循共享状态
通常,用户可以在环境中观察到的“共享状态”实际上是由网格和本地状态直接共享的状态的组合,这些状态由视觉脚本建立,以响应聊天室中发生的事件。 例如,当用户在环境(共享状态)中翻转开关时,视觉脚本可能会更改天空框(本地状态)的颜色。 你可能很想直接应用本地更改(更新天空框颜色),以响应与交换机交互的用户。 但是,即使交互事件发生在聊天室中当前的所有客户端上,以后加入聊天室的任何客户端都不会仅仅因为事件发生时没有该事件。 相反,应 使本地状态遵循如下所示的共享状态 :
- 当用户交互(例如,翻转开关)时,将此触发器设置为更新 共享变量的本地 事件(例如开关的开/关状态)。
- 使用 “状态已更改” 观察共享变量。
- 当 On State Changed 事件触发(因为共享变量更改了其值),应用所需的任何本地更改(例如,更新天空框颜色)。
这样,本地状态(天盒颜色)将遵循共享状态(开关的状态)。 这一点很好,因为它适用于翻转交换机的本地客户端,对于同时存在于会议室中的所有其他远程客户端,以及以后将加入聊天室的任何将来客户端,它都能正常工作。
使本地状态遵循共享状态:最佳做法
本地事件:例如,观察网格可交互正文组件的“本地选择”属性的 On State Changed 事件节点:
- 🆗 可以将专用的本地状态更改为客户端。 这些状态更改将严格保留在本地客户端上,当客户端离开会话时,它们将会消失。
- 🆗 可以更改共享状态。
- ❌无法将本地状态更改为跨客户端保持一致。 本地事件仅在一个客户端上执行,因此在客户端之间保持本地状态一致所需的更新不会在任何其他客户端上发生。
共享事件:例如, 附加到共享物理触发器对撞机的 On Trigger Enter 事件节点:
- 🆗 可以更改瞬间效果的局部状态:例如粒子效果或短音频效果。 只有发生共享事件时会议室中存在的客户端才能看到本地效果;以后加入会议室的任何客户端都不会。
- ❌无法将本地状态更改为跨客户端保持一致。 共享事件仅在发生时存在的客户端上执行,但以后加入会话的客户端不会重播该事件。
- ⛔ 不得更改共享状态。 由于共享事件在所有客户端上执行,因此它所做的任何操作都是由所有客户端及时完成的。 根据更改的性质,它最终可能会重复多次(例如,分数计数器可能会递增多个,以响应单个事件)。
在共享变量或共享组件属性中观察共享状态的状态已更改事件:
- 🆗 可以更改本地状态,以便与客户端之间的共享状态 保持一致。 若要使这在所有客户端中以可重复且一致的方式运行良好,必须将观察到的共享状态的每个可能的新值转换为本地状态,而不仅仅是一些精心挑选的状态转换(如 “选择 ”变为 true)。
使本地状态遵循共享状态:示例
在此示例中,此环境中有两个交互式按钮:一个标记为“Star”,另一个标记为“Sponge”。 选择任一按钮应该执行两项操作:
- 将相应的标签存储在名为 ObjectKind 的共享字符串变量中。
- 将引用存储在名为 ObjectRef 的本地 GameObject 引用变量中。
下面是两个脚本流,每个按钮各有一个。 每个侦听一个按钮的网格可交互正文组件的共享 Is Selected 属性,并根据所选按钮更新 ObjectKind 和 ObjectRef:
一切似乎都正常,但仅适用于选择其中一个按钮时已在房间中的用户。 任何加入会话的用户以后都会在其共享环境的本地版本中找到不一致的状态:只有 ObjectKind 根据最近选择的按钮正确设置,但 ObjectRef 保持不变。
这两个脚本流有什么问题?
首先,请注意,这些脚本流由共享事件触发,因为它们都侦听每个按钮的共享 Is Selected 属性更改。 这似乎有意义,因为它是在所有客户端上更新本地 ObjectRef 变量的唯一方法。
但是:
- 共享事件不得更改共享状态 ,但这些脚本流正在更新共享 ObjectKind 变量。
- 共享事件不能将本地状态更改为跨客户端保持一致,但这些脚本流正在更新本地 ObjectRef 变量,我们打算在所有客户端上保持一致,就像 ObjectKind 一样。
因此,目前设置的方式,我们实际上不应该执行任何需要按钮执行的操作。
摆脱此问题的唯一明显方法是使触发这些流的事件在本地。 我们可以通过使 On State Changed 事件节点观察“本地选择”属性而不是“已选中”来执行此操作。
事件现在为本地,这意味着...
- 本地事件可以更改共享状态 ,以便我们现在可以安全地更新共享 ObjectKind 变量,并且其值将通过网格视觉脚本的内置网络自动在客户端之间共享。
- 本地事件无法将本地状态更改为跨客户端 保持一致,因此我们仍无法更新这些脚本流中的本地 ObjectRef 变量。 我们必须找到另一种方法。
这就是两个脚本流在这些更改后的外观:
我们可以做些什么来设置本地 ObjectRef 变量,使其与它保持一致? 幸运的是,这两个脚本流已经建立了一些可以遵循的共享状态:共享 ObjectKind 变量。 我们只需使用观察 此变量的 On State Changed 事件,并根据变量的值更新本地 ObjectRef 变量:
这是一种很好的做法,因为 观察共享状态的 On State Changed 事件可以更改本地状态,使其与它保持一致。 这将适用于按下按钮的客户端、同时存在于同一会议室中的所有其他客户端,以及所有稍后将加入会话的客户端。
网络陷阱
高频率共享更新
默认情况下,网格视觉脚本共享几乎整个场景状态。 这非常适合共享,但它也可以偶然偷偷溜进来,并造成不必要的网络负载。 例如,以下脚本流会将网络淹没转换轮换的冗余更新。 但是,由于所有客户端同时执行它,因此任何远程更新都不会对本地任何客户端产生实际影响:
在这种情况下,你可能应使用 本地脚本范围 使转换组件本地到每个客户端。 此外,你可能应该使用 动画器 组件而不是 On Update 脚本流来开始。
从网格工具包 5.2411 开始,网格可视化脚本诊断面板和内容性能分析器(CPA)显示此类构造的“高频率共享更新”警告。
在每个客户端上运行“开始” 时
你可能很想将 On Start 事件视为在会话启动时运行的内容,但在加入会话时,它实际上在每个客户端(本地)触发。 它非常适合用于初始化本地状态:
但是,当你尝试使用 On Start 初始化共享状态时,你会发现每当任何人加入会话时,共享状态都会无意中重新初始化每个人:
网格可视化脚本诊断面板(从网格工具包 5.2410 起)和内容性能分析器(CPA)(从网格工具包 5.2411 起)显示“会话加入共享更新”警告(检测到此情况时)。
共享类型化,但变量赋值不是
出于安全和安全原因,共享视觉脚本变量的类型非常强。 这意味着你在声明的脚本变量的“变量”组件中选择的类型定义将在客户端之间同步哪个确切的值类型。
遗憾的是,当你更新变量的值时,Unity 视觉脚本完全忽略变量的声明类型。 例如,很容易在为 Integer 类型声明的变量中意外存储浮点类型值。 在本地客户端中,视觉脚本不会注意到此错误,因为视觉脚本将根据需要自动将错误的 Float 转换为预期的 整数 。 但是,当涉及到在客户端之间同步此值时,网格视觉脚本不能采用相同的自由:“最终一致性”保证会排除外部转换的任何值转换,安全和安全注意事项使得无法接受远程客户端与为变量声明的值类型不同的值类型。
例如,请考虑此声明名为 MyIntegerVar 的共享变量:
下面是更新此变量的脚本流:
哪些方面可能会出错? 遗憾的是, 此示例中使用的随机 | 范围 脚本节点分为两种类型:一种是生成随机 整数 值,另一个是生成随机 浮点 值。 节点选择器面板中这两个脚本节点之间的差异是微妙的:
因此,如果意外选择错误的随机 | 范围脚本节点,脚本最终可能会在整数类型变量中无意中存储 Float 值,但错误 Float 值不会复制到任何其他客户端。
请记住这一点,作为你创建的共享变量似乎已停止共享的潜在原因。 将来版本的网格可视化脚本可能会在检测到此类脚本错误时发出警告。