蓝图中的多人游戏

本页面的内容:

虚幻引擎 4 提供了很多即购即用的多人游戏功能,而且可以轻松的设置基本的蓝图连网游戏。您可以很轻松的上手 并开始玩多人游戏。大多数基本的多人游戏逻辑都依赖于 Character 类及其 CharacterMovementComponent(供 Third Person 模板项目使用)中内置的网络连接支持。

游戏架构回顾

要在您的游戏中加入多人游戏功能,就一定要了解引擎提供的主要游戏类的作用,以及它们的协作方式 - 尤其是它们如何在多人游戏背景下工作:

可以参见 Gameplay 框架 文档了解更多信息。此外,在设计您的多人游戏时,至少要记住以下这些提示:

Actor 复制

replicates.png

UE4 网络连接技术的核心就是 actor 复制。一个将 “Replicates” 标志设为 true 的 actor 将自动由服务器同步至与该服务器连接的客户端。必须说明的是,actor 只能从服务器复制到客户端 - 反过来就不行。当然,客户端仍需要能够向服务器发送数据,而且将通过复制的 “Run on server” (运行于服务器)事件实现这一点。

请参见这份 如何同步 Actor 指引 了解具体示例, 同时查看 Actor 的复制 文档。

主控权

对于世界中的每个 actor,都会有一个连接的玩家作为这个 actor 的主控者。对于服务器上存在的每个 actor,服务器均对其拥有主控权 - 包括所有的 actor 复本。因此,当 “Has Authority” 函数运行于客户端且目标是一个复制给它的 actor 时,该函数将返回 false。您也可以使用 “Switch Has Authority” 宏,从而快速便捷的在 actor 复本中划分不同的服务器和客户端行为。

switch_has_authority.png

变量

在 actor 的变量详情面板中有一个 Replication 下拉菜单,可用于控制变量的复制方式(如果可行)。

variable_replication.png

选项 说明
None 这是新变量的默认选项,表示不通过网络向客户端发送值。
Replicated 当服务器复制此 actor 时,会向客户端发送这个变量。接收客户端上面的变量值会自动更新,以便在下次访问时能够反映服务器上的值。当然,在通过真实网络运行游戏时,由于网络延迟的存在,更新也会有一段时间的延迟。切记,复制的变量只能单向传送,也就是从服务器到客户端!要将数据从客户端发送至服务器,请参见“事件”部分。
RepNotify 变量将按照 replicated 选项的方式进行复制,但除此之外,还会在您的蓝图中创建一个 OnRep_ 函数。当这个变量的值发生变化时,该函数将在客户端和服务器上被引擎自动调用。您可以根据游戏的需要来随意实施此函数。 onrep.png

在引擎的内置类中,很多变量已经启用了复制,所以在多人游戏情况下,很多功能会自动运行。

请参见这份 操作指南 了解关于变量复制过程的具体示例,同时查看 属性复制之完整文档

生成与销毁

当复制的 actor 被生成到服务器时,客户端也会得到通知,然后自动生成此 actor 的复本。但在这之后,一般就不会再从客户端向服务器进行复制,如果复制的 actor 是在客户端上生成,这个 actor 就只在生成它的客户端上存在。服务器和其他任何客户端都不会收到 actor 复本。而执行生成操作的客户端将作为 actor 的主控者。这对于那些对游戏没有实际影响的修饰性 actor 来说仍然适用,但如果 actor 确实会对游戏产生影响而且应当被复制,则最好是确保它们能在服务器上生成。

销毁 actor 复本的过程与此类似:如果服务器销毁了一个 actor,所有客户端都将销毁其各自的复本。客户端可以任意销毁其管辖的 actor(也就是它们本身生成的 actor),因为这些 actor 没有复制给其他玩家,也不会对其产生任何影响。如果客户端尝试销毁一个不归其管辖的 actor,销毁请求将被忽略。此时的情况与生成 actor 时相同:如果您要销毁一个复制的 actor,就要在服务器上销毁它。

事件复制

在蓝图中,除了复制 actor 及其变量之外,您还可以在客户端和服务器上运行事件。

请参见这份 远程调用函数(Replicating Functions) 了解具体示例,同时查看 RPC

您也可以参阅 RPC(“远程过程调用”)术语说明。这时需要注意的是,蓝图中的事件复本将最终编译成引擎中的 RPC - 这在 C++ 中会经常调用。

所有权

在多人游戏方面(尤其是使用复制事件时)需要明确一个重要问题,那就是哪个连接 被认为是特定 actor 或组件的所有者 。具体到现在的情况,我们需要知道 “Run on server”(运行于服务器)事件只能从客户端拥有的 actor(或其组件)上调用。通常来讲,这意味着您只能从以下的 actor 或其中某个 actor 的一个组件上发送 “Run on server” 事件:

同样,对于发送“Run on owning client”(运行于所属客户端)事件的服务器来讲,这些事件也应当在上述某个 actor 上调用。如果不这样做,服务器就无法知道要将事件发送给哪个客户端,只能让它在服务器上运行!

事件

在自定义事件的详情面板中,您可以设置事件的复制方式(如果可行)。

event_replication.png

选项 说明
Not Replicated 这个是默认选项,表示该事件不会进行复制。如果它是在客户端上调用,就只能在这个客户端上运行,如果在服务器上调用,就只能在服务器上运行。
Multicast 如果一个多播事件是在服务器上调用,它将不顾哪条连接拥有目标对象,而被复制到所有已连接的客户端。如果客户端调用了一个多播事件,它将被视为没有经过复制,而且只能在调用它的客户端上运行。
Run on Server 如果该事件是从服务器调用,它就只能运行在服务器上。如果是从客户端调用且拥有一个归客户端所有的目标,它将被复制到服务器并在上面运行。“Run on Server” 事件是客户端向服务器发送数据的主要途径。
Run on Owning Client 如果从服务器调用,该事件将运行于拥有目标 actor 的客户端。由于服务器可以拥有 actor,“Run on Owning Client” 事件可能实际运行在服务器上(尽管从名称上看不是这样)。如果是从客户端调用,该事件将被视为没有经过复制,而且只能在调用它的客户端上运行。

下列表格介绍了在不同的调用方式下,不同的复制模式如何影响事件的运行位置。

如果事件是从服务器调用,那么在目标符合左侧列所指的特征时,它将运行于……

未复制 多播 运行于服务器 运行于所属客户端
Client-owned target 服务器 服务器和所有客户端 服务器 目标的所属客户端
Server-owned target 服务器 服务器和所有客户端 服务器 服务器
Unowned target 服务器 服务器和所有客户端 服务器 服务器

如果事件是从客户端调用,那么在目标符合左侧列所指的特征时,它将运行于……

未复制 多播 运行于服务器 运行于所属客户端
Target owned by invoking client 执行调用的客户端 执行调用的客户端 服务器 执行调用的客户端
Target owned by a different client 执行调用的客户端 执行调用的客户端 丢弃 执行调用的客户端
Server-owned target 执行调用的客户端 执行调用的客户端 丢弃 执行调用的客户端
Unowned target 执行调用的客户端 执行调用的客户端 丢弃 执行调用的客户端

如上面的表格所示,任何从客户端调用且未设置成 “Run on Server” 的事件都会被视为没有复制。

将复制的事件从客户端发送到服务器,是客户端向服务器传达信息的唯一途径,因为一般的 actor 复制过程仅支持从服务器指向客户端。

另外还要注意的是,多播事件只能从服务器发送。由于虚幻引擎采用了客户端-服务器模式,客户端无法直接与任何其他的客户端进行连接,而只能与服务器相连。因此,客户端无法将多播事件直接发送给其他客户端,必须且只能与服务器进行通信。但是,您可以用两个复制的事件模拟这一行为,即 “Run on server”(运行于服务器)事件和 “Multicast”(多播)事件。通过实施 “Run on server” 事件,您可以在需要时执行验证,然后调用多播事件。通过实施多播事件,您可以为所有已连接的玩家执行逻辑处理。下面是一个没有执行任何验证的例子:

forward_multicast.png

关于“中途加入”方面的注意事项

在使用事件复本来传递游戏状态的更改时,需要留意其如何与支持“中途加入”(在游戏过程中加入游戏)的游戏进行交互。如果玩家加入了一个正在进行中的游戏,则任何在加入前所发生事件的复本都不会针对这个新玩家执行。顺便一提的是,如果您想让游戏很好的支持中途加入,最好的做法往往是通过复制的变量来同步重要游戏数据。这时,您经常会看到这样的过程:客户端在世界中执行某种操作,通过 “Run on server” 事件向服务器告知这一操作,然后在该事件实施时,服务器将根据此操作更新一些复制的变量。而其他没有执行此操作的客户端仍然会通过复制的变量看到操作的结果。此外,在这一操作后中途加入的客户端也能看到世界的正确状态,因为它们会从服务器接收最近期的变量复本值。如果服务器只发送了一个事件,那么中途加入的玩家就不会知道之前执行的操作!

可靠性

对于任何复制的事件,您都可以选择它的类型,即 reliableunreliable

Reliable 事件一定会抵达目的地(假设遵守了上述所有权规则),但为了确保可靠性,它们会占用更多的带宽,而且有可能造成延迟。您应当避免太过频繁的发送可靠事件,例如每个时钟单位发送一次,因为引擎内部的可靠事件缓冲区可能发生溢出 - 这时,相关的玩家就会断开连接!

Unreliable 事件同样名符其实:一旦出现某些状况,如网络 数据包丢失,或者引擎确信要发送较多的高优先级流量,它们也许就无法抵达目的地。因此,不可靠事件比可靠事件使用的带宽更小,也可以更经常被安全地调用。