|
发表于 2022-12-2 15:33:00
|
显示全部楼层
前言
众所周知,目前国内游戏市场,不论是PC还是移动端,网络游戏占比都处于大头。作为一名想从事UE游戏开发的程序员,如果不了解如何使用UE制作多人游戏,那是十分不明智的。本文我们就探讨虚幻引擎的网络复制系统--Replication。首先介绍下网络游戏的概述,接着介绍了Replication的核心概念,最后给出了一些Tips。本文属于是UE5网络复制的入门理论文章,想要真正掌握网络复制,还需要了解底层同时进行大量的项目实践,参考部分给出了一些链接。希望此篇文章能是一个虚幻引擎多人游戏开发的好的入手点。
一、网络游戏概述
网络多人游戏起源于20世纪70年代的高校大型机系统,在20世纪90年代中后期互联网接入得到普及。
经历了从本地多人游戏 → 早期网络多人游戏(使用串口) → 多用户网络游戏(BBS) → 局域网游戏(LAN): Doom(1993)可以称为现代网络游戏的起源 → 在线游戏(WAN),Quake(1996)、Unreal(1998),主要问题是延迟 →大规模多人在线游戏(MMO),魔兽世界(2004) → 移动网络游戏, 炉石传说(2014)的过程
现代多人游戏体验需要需要在世界范围内的大量客户端间同步庞大数据,因此发送数据的类型和方式至关重要,其会极大影响项目的执行和质感。
1.1 套接字概念
同步数据就涉及到两个进程间的相互通信从而进行数据传输,本地进程间的通信有多种方式,如消息传递、同步、共享内存等,但这些都不是本文的主题。我们现在想实现互联网上身处不同地区的两台电脑上的两个游戏进程进行通信,而首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。而这个三元组我们称其为套接字,其是通信双方在通信时所使用的通信点,通信双方通过通信点来交换信息和数据。
现代使用TCP/IP协议的应用程序通常采用伯克利套接字应用程序接口(Berkeley Sockets API)来实现网络进程之间的通信。该API提供了进程与TCP/IP模型各个层之间通信的标准方法,此API已被移植到每一个主要的操作系统和流行的编程语言,它是网络编程中名副其实的标准。

套接字(socket)在TCP/IP协议中的位置
那么作为使用虚幻引擎开发网络游戏的工程师,我们是否需要使用套接字来进行数据同步呢?
答案是并不需要!为了给众多平台提供支持,UE从底层的套接字中抽象实现细节,称为ISocketSubsystem的接口类为Unreal支持的不同平台做了实现,进一步在此基础上实现了一个庞大的网络复制系统:Replication,其表示在客户端与服务器间同步数据和调用程序的过程。通过它,你可以轻松开发出一款多人游戏,而不需要自己去管理客户端连接、启用套接字或者发送数据包,更不用自己去处理序列化、编码和字节序等等事项。你只需要告诉虚幻引擎:”我想这个属性被复制“。
1.2 网络应用模型
网络应用模型指网络上计算机处理信息的方式,根据信息处理过程中主机之间的协作方式,有两种主要的网络模型:客户/服务器模型(C/S模型),对等网络模型(P2P)。
在正式介绍核心概念之前,我们简要介绍一下以下二者。

图来自王道计算机网络

网络应用模型思维导图
- 在虚幻引擎中,其采用的网络模型是C/S模型,网络中的一台计算机作为服务器主持多人游戏会话,而所有其他玩家的计算机作为客户端连接到该服务器。然后服务器与连接的客户端分享游戏状态信息,并提供一种客户端之间通信的方法。
- 当服务器上发生更改时候,这些更改根据需要向下传播到客户端,作为Replication过程的一部分。

- 主要的难点在于选择应复制的信息及方式,以向所有玩家提供一致的游戏体验,同时需最小化信息复制量,尽可能减少网络带宽占用率。
二、核心概念
2.1 网络模式(ENetMode)
网络模式描述了计算机与网络多人游戏会话的关系。

- 其是是UWorld的一个属性,UWorld代表的是一个地图或沙盒的顶级对象,Actor和Components将在其中存在并呈现。游戏实例可采用以下任意网络模式:
enum ENetMode
{
/** Standalone: a game without networking, with one or more local players. Still considered a server because it has all server functionality. */
NM_Standalone,
/** Dedicated server: server with no local players. */
NM_DedicatedServer,
/** Listen server: a server that also has a local player who is hosting the game, available to other players on the network. */
NM_ListenServer,
/**
* Network client: client connected to a remote server.
* Note that every mode less than this value is a kind of server, so checking NetMode < NM_Client is always some variety of server.
*/
NM_Client,
NM_MAX,
};
- 游戏是否可玩?我们的GameInstance是否有LocalPlayer,我们是否正在处理该玩家的输入并将世界渲染到视口中?
- 我们是服务器吗?换句话说我们的GameInstance是否具有World的规范、权威副本,其中包含GameMode 。
- 如果我们是服务器,我们是否对远程连接尝试开放?其它玩家可以加入并作为客户端玩吗。

通过三个问题区分四个网络模式
- UWorld中的NetMode的值就取决于你启动GameInstance的方式,如何打开地图。引擎何时生成的GameInstance可以参考之前的引擎运行流程文章
- 如果你的GameInstance已经连接到某个远程服务器,那么当前世界的NetMode是NM_Client。状态更新服从于服务器。

2. 如果你的GameInstance已经在本地加载了一张地图,那么ENetMode为NM_Standalone。此时GameInstance既是服务器又是客户端。但由于以单人模式运行,所以不对其它客户端开放连接。

3. 如果你在加载地图的时候添加了Listen参数,将以NM_ListenServer模式运行,其它GameInstance可以作为客户端连接进来

4. 最后是以专用服务器模式运行,该模式下GameInstance没有本地玩家,没有viewport

小结一下
- UWorld中有一个属性叫做UNetMode,它描述了计算机与网络多人游戏会话的关系。它有以下四种选择,区分的方式通过三个简单的问题。其值取决于你打开这个游戏世界的方式

2.2 三个核心类(UNetDriver、UNetConnection和UChannel)
对于网络游戏,UE只支持客户端-服务器模型,服务器可以以两种不同的模式进行,分别是专属服务器(NM_DedicatedServer)和聆听服务器(NM_ListenServer)。
当你选择上述两种多人游戏的网络模式开始游戏后,UE的Replication系统就会在幕后工作,从而确保多个UGameInstance能够保持同步,共同构建一个共享的UWorld。
Replication系统为了让它们对游戏世界发生的变化保持一致看法,依赖于三个主要的类:UNetDriver、UNetConnection和UChannel。

- 每个进程有一个UGameEngine对象,当服务器启动时,其会创建一个UNetDriver对象,其负责监听来自其它客户端的消息(InitListen)

- 当启动客户端时,也会创建自己的UNetDriver对象,它会向服务器发送连接请求(InitConnect)

- 一旦服务器和客户端UNetDriver对象建立了联系,各自UNetDriver内部就生成一个UNetConnection对象,服务器会针对每一个连接进来的远程玩家都创建一个对应的UNetConnection,而每个客户端只有一个UNetConncection,代表跟服务器的连接

- 每个UNetConnection都会拥有多个不同的UChannel对象。通常一个UNetConnection会有一个UControlChannel(C/S之间发送控制信息,主要是发送接收连接与断开的相关消息)和一个UVoiceChannel(用于发送接收语音消息,在Connection中初始化连接的时候创建一个该通道实例)
- 紧接着就是一系列的UActorChannel,每一个都对应通过当前UNetConnection进行网络复制的Actor

- 这说明了Replication系统的关键事实:网络复制是以Actor为基础的!
<hr/>
- 这里还有两个概念Packet与Bunch。Packet: 从Socket读出来/输出的数据,一个Packet里面可能有多个Bunch数据或者Ack数据; Bunch: 从逻辑上层分发下来的同步数据包,数据可能不完整,主要记录了Channel信息,NGUID。同时包含其他的附属信息如是否是完整的Bunch,是否是可靠等.
- UNetConnection通过Socket发出的数据块为Packet;UChannel发出的数据块为Bunch。
<hr/>
- 回归正文,如果你希望一个Actor对象在网络上保持同步,那你就需要将这个Actor的bReplicates设置为true,告诉引擎你需要复制这个Actor。(这里有一个注意点就是一个Actor参与复制,不代表它的所有部分都需要使用网络功能的代码。)

- 当一个Actor跟某个玩家相关时(相关性也是一个核心概念在下文介绍),那么服务器就会在对应这个玩家的UNetConnection中创建一个UActorChannel,然后服务器和客户端就会利用该UActorChannel来交换这个Actor的信息。(信息包装在哪里?)

小结一下
以下基础概念引用自南山搬砖道人:UE4网络同步-基础流程
- UNetDriver:使用其子类IPNetDriver,NetDriver与World一一对应。主要功能:创建管理Connection; 收发数据包; 初始化CS连接;Actor同步、RPC管理; Socket管理; 对于Server端的NetDriver对象管理多个NetConnection,对于Client端的NetDriver对象管理一个NetConnection
- UNetConnection:表示网络连接. 关联了NetDriver、PackageMap、UChannel数组和PlayerController。服务器上:一个客户端到服务器的一个连接叫一个ClientConnection;在客户端上:服务器到客户端的连接叫ServerConnection。
- UChannel: 每一个通道只负责交换某一个特定类型特定实例的数据信息.
UE网络通信没有使用TCP这种可靠性传输协议,而是使用UDP这种非可靠性传输协议来实现通信,在UDP基础上又实现了可靠与非可靠混合模式。
2.3 OwnerShip(所有权)
每个Actor都能指定另一个Actor作为自己的拥有者

<hr/>Owner和Outer的区别:
- Outer(父对象),所有的Object都拥有一个Outer,每个Object都至多有一个Outer(父UObject),且可以拥有任意数量的子Object。,比如Runtime模式下,放在场景里的Actor的Outer是ULevel。在编辑器模式在,在内容浏览器里的Actor蓝图资产的Outer是UPackage(UPackage又是啥?)
- Owner(拥有者),每个Actor都能指定另一个Actor作为自己的拥有者,一般在生成Actor的时候指定。或者在运行时调用SetOwner指定. 官方注释此属性主要用于复制(bNetUseOwnerRelevancy&bOnlyRelevantToOwner)和可见性(PrimitiveComponent bOwnerNoSee和bOnlyOwnerSee)
<hr/>

生成时指定

运行时指定
- 当涉及到OwnerShip(所有权),APlayerController是不得不提的重要的类。
- 基本上,每个UNetConnection都代表一个玩家

- 而且一旦某个玩家成功加入游戏就会有个PlayerController对象与之关联,从服务器的角度看,UNetConnection对象对PlayerController以及该PC拥有或间接拥有的Actor都具有拥有权

- APlayerController自动对它们所占有的Pawn对象具有拥有权。如果说每个APlayerController都有一个Pawn, 而每个Pawn都会生成并拥有一个Weapon对象。那么对某个Weapon对象沿着Owner引用链追溯到PlayerController,就能够知道这个Weapon对象属于哪个客户端。(这是所有权在网络复制中的作用之一)

- 所有权是以下情形的重要因素
- RPC 需要确定哪个客户端将执行运行于客户端的 RPC
- Actor 复制与连接相关性
- 在涉及所有者时的 Actor 属性复制条件
- 所有权对于 RPC 这样的机制至关重要,因为当您在 actor 上调用 RPC 函数时,除非 RPC 被标记为多播,否则就需要知道要在哪个客户端上执行该 RPC(RPC将在后文介绍)
- 提到所有权这个概念之后,那么不得不提的另一个与其密切相关的概念就是相关性(Relevancy)
2.4 Relevancy(相关性)
Actor的相关性决定哪些网络连接会产生,以及在什么时候产生
- 场景的规模可能非常大,在特定时刻某个玩家只能看到关卡中的一小部分 Actor。虚幻引擎的网络代码中包含一处重要的带宽优化:服务器只会让客户端知道其相关组内的 Actor,即只会同步这类Actor。
- 当一个Actor是可复制的,服务端的UNetDriver通常会将这个Actor同每个客户端的UNetConnection进行检查,来确定它跟这个客户端是否具有相关性。
- 一些Actor的bAlwaysRelevant属性是true,表示服务器会一直将其复制给所有客户端。比如PlayerState,GameState。

- OwnerShip和Relevancy两个概念密切相关。如果一个Actor对象被某个玩家拥有,那么它就被认为是跟这个客户端相关。即只要有所有权,那么一定是相关的。

- 一些Actor比如PlayerController都被设置为只与它们的拥有者相关,bOnlyRelevantToOwner=true ,所以它们永远不会被复制到没有拥有权的客户端。
- 如果一个Actor未设置相关性,并且客户端对这个Actor没有拥有权,那么会如下进行判断相关性
- Actor是隐藏的,并且根组件关闭了碰撞,那么它没有相关性
- 否则引擎基于这个Actor跟玩家的距离来决定其相关性。当Actor与玩家距离小于一个阈值的时候认为是相关的。你可以重载Actor的IsNetRelevantFor方法来自定义规则。

超过3k就无关,变为fasle

引擎默认的IsNetRelevantFor伪代码
- 判断是否相关性的规则没有写死,你可以重载Actor的IsNetRelevantFor方法来自定义自己的相关性判定规则。
2.5 NetUpdateFrequency和NetPriority

- 当Actor参与复制时,它的NetUpdateFrequency和NetPriority两个属性就会决定服务器向它所相关的客户端发送更新的频率
- NetUpdateFrequency: 服务器每秒钟检查Actor更新的次数,如果检查出来有变化就会将更新数据发送至客户端,所以重要的Actor一般值较高。
- 但是由于网络延迟和带宽的限制,即使你的Actor的NetUpdateFrequency设置的高,可能在另一个客户端上看起来也没有那么丝滑
- 服务器上的NetDriver采用了一些简单的负载均衡手段来缓解带宽饱和的问题:优先级
- 任何情况下,UNetDriver只会分配到有限的带宽,所以它需要将所有相关的Actor根据优先级排序,然后赋予其相应的带宽进行网络更新直到用完了所有可用的带宽。

https://www.zhihu.com/video/1571502187586531328
- 距离玩家更近的Actor拥有更高的优先级,而且一段时间未更新的Actor也会具有更高优先级,这可以从Actor的GetNetPriority方法中看出。根据上述规则最终会得到一个根据优先级排列的Actor列表。
- Actor的NetPriority属性的值用来在原有优先级的基础上相乘得到最终的优先级。所以可以给某个重要的Actor分配更高的NetPriority从而能分配更多的带宽,再配合上较高的NetUpdateFrequency,就可以让此Actor在客户端更近丝滑。
- UE在性能调整中分配的部分 NetPriority 值

越高得到的带宽越多
- 更多有关Replication的设置可以在蓝图的类默认值里找到

- 了解了以上这些概念后,我们来看一看一个Actor复制的流程
2.6 Actor Replication
- 在上面我们提到了网络复制是以Actor为基础的。Actor是实现复制的主要推动者。服务器将保留一份 Actor 列表并定期更新客户端,以便客户端保留每个 Actor (那些需要被同步的Actor)的近似复本。
- 要想让一个Actor参与网络复制,必须将这个Actor的bReplicates设置为true,告诉引擎你需要复制这个Actor。这相当于一个总开关。


- 当你设置一个Actor的bReplicates=true后,服务端会在UNetConnection中创建一个ActorChannel,从而将该Actor复制到对应的客户端。

- 那么Actor主要的更新方式是什么呢?
- 主要有两种:Property Replication(属性复制)和RPC(远程过程调用)
- 属性复制和 RPC 的主要区别在于,属性可以在发生变化时随时自动更新,而 RPC 只能在被执行时获得调用更新。
- 在介绍流程前有必要搞明白什么是属性复制、什么又是RPC
2.6.1 属性复制(Property Replication)
- 属性复制是虚幻网络复制系统的重要组成部分,之前提到的负载均衡和优先级功能让它更容易扩展
- RPC是即时的,PropertyReplication可能是间断发生的。

- 其与相关性、更新频率和带宽限制息息相关。无相关性,Actor的属性变化不会同步到客户端。
- 如何启用一个属性复制?
- 方法1:C++中你可以添加给UPROPERTY添加Replicated说明符,蓝图中在属性的细节面板设置Replicated。C++中还需要在Actor类的.cpp源文件中额外定义一个叫GetLifeTimeReplicatedProps的函数


我想复制此Actor里的Weapon和Color属性
- 此函数指定哪些属性应该参与复制,以及在什么条件下可以复制的地方,最简单的是使用DOREPLIFETIME宏将某个属性一直复制到所有的客户端,但是你也可以使用条件复制。比如你可能只需要将属性复制给有拥有权的客户端

2. 方法2:如果你想在属性更新的时候运行一些代码,那么RepNotify是你需要的。它拥有方法1的所有功能,同时还提供了一个函数,每当该函数关联的变量更新时,该函数就会调用并在服务器和客户端上执行

蓝图

C++
- 注意: 在蓝图中的RepNotify,当变量发生变化时,服务端和该值的客户端都可以调用指定的自定义函数。C++仅在客户端调用函数, 如果你想RepNotify逻辑在服务端和客户端上都运行,那么你需要在修改属性值之后手动在服务端调用RepNotify函数

最具代表性的一类应当复制的属性就是 Actor 的健康值。这种周期性的受带宽限制的网络更新大多使用属性复制(Property Replication)。但是如果你有一条较高优先级的消息想要立即通过网络发送出去,那就需要用到RPC了。
2.6.2 RPC(远程过程调用)
RPC (远程过程调用)是在本地调用但在其他机器(不同于执行调用的机器)上远程执行的函数。 RPC 函数非常有用,可允许客户端或服务器通过网络连接相互发送消息。
这些功能的主要作用是执行那些不可靠的暂时性/修饰性游戏事件。这其中包括播放声音、生成粒子或产生其他临时效果 之类的事件,它们对于 Actor 的正常运作并不重要。在此之前,这些类型的事件往往要通过 Actor 属性进行复制。
- 任何UFUNCTION都可以指定为Client,Server或者NetMulticast使得一个函数变成一个RPC。蓝图中通过设置函数的Replicates属性

- RPC同时可以声明为可靠的(Reliable)或者不可靠的(Unreliable),可靠RPC在带宽饱和的情况下不会被丢弃,保证发送到达同时对于单个Actor,保证按调用顺序发送到。不可靠的RPC不能保证被发送到,更不能保证按顺序发送到。

- 滥用可靠RPC会加剧带宽包含,导致丢包的问题。
- 前面介绍了所有权下概念,它对RPC非常重要,它决定了大多数 RPC 将在哪里运行。
- 如果你在服务器上调用客户端RPC,那么这个函数的实现将在具有拥有权的客户端上执行

- 如果在有此Actor拥有权的客户端上调用服务器RPC,那么这个RPC在服务器上运行。

- 如果你在服务器上调用了一个多播RPC,那么这个函数的实现会在所有地方执行。跟服务器RPC和客户端RPC不同的是,多播RPC受相关性影响。对于无拥有权的客户端,可能没有创建对应的ActorChannel,这样一来这个客户端根本收不到RPC,这意味着不应该使用多播RPC来复制持续的状态变化给其它客户端。

- 在C++中,RPC函数实现部分的函数名需要加”_Implementation”后缀,IDE一般自动帮你添加。带Implementation的函数是在远程进程上实际执行的地方,但是你本地调用的函数会自动替换成对应的消息进行网络传输。

- 服务端的RPC也可以使用WithValidation说明符来实现一个验证函数,是一种检测作弊的手段,验证客户端发送给服务端的数据是否正常可靠。要注意一点如果服务器RPC验证失败,那么发送该RPC的客户就会被踢出游戏!

- RPC在属性复制相对有局限性的情况下很有用
- 服务端RPC是唯一能够让服务器从相应客户端获得数据的方式,这使得它经常被用到。
- 总的来说,RPC适用于优先级较高、时间精度要求较高的网络代码。如:CharacterMovement就大量使用RPC来循环往复发送位置更新。
- 对于其它情况,能使用属性复制就尽量不用RPC。RPC是即时的,PropertyReplication可能是间断发生的。
小结一下
- UE有三种类型的RPC:服务器、客户端和多播RPC。
- 服务器RPC是在客户端上调用并在服务器上执行的函数。
- 服务器不会让任何客户端调用游戏世界中所有Actor上的服务器RPC,这很容易导致作弊等问题。只有这个Actor的OwnerShip是该客户端,客户端才可以在Actor上成功执行服务器RPC,否则丢弃。
- 在客户端上调用其它两类RPC都无意义,都只在自己本地运行该函数
- 比如如果你直接在客户端调用Server_OpenDoor,而这个门Actor的所有权是服务器的,那么此调用会被丢弃,无法实现开门的同步。解决的方法是使用此客户实际拥有的Actor来调用服务器RPC,如下图,利用PlayerController结合接口来实现开门,PC是客户端拥有的Actor可以成功调用这个服务器RPC

客户端调用服务器RPC的一个例子
- 客户端RPC与服务器RPC相反,当服务器调用客户端RPC时,这个过程调用被发送给拥有所讨论的Actor的客户端。
- 在服务器上调用服务器RPC只在服务器执行,在服务器上调用多播RPC在服务器和所有客户端上运行
- 多播RPC被发送到多个游戏实例,当且仅当是从服务器上调用这个多播RPC,否则只在调用的客户端本地运行。多播RPC用于通知每个客户端特定的事件,比如当服务器想让每个客户端本地生成一个粒子效果时,就使用多播函数。

结合此图思考:所有权、哪里调的、调的什么RPC

三个不同类型的RPC结合在一起具有很大的灵活性。这需要大量的实践去摸索!
一个Actor被复制到客户端,主要发生三件事(Lifetime、Property Replication和RPC):
- Actor对象的生命周期会在服务端和客户端之间保持同步。服务器生成了,客户端也生成,服务器Actor销毁了,客户端也销毁。
- 第二点就是属性复制(Property Replication)。上面提到的是整个Actor的生命周期的复制,那么每个Actor也有自身的属性,当服务器上Actor的某个属性值发生变化时,新的值也会被传播到客户端。这依赖于是Actor首先自身的bReplicates是true。

3. 最后是RemoteProceduralCall(远程过程调用),如果你将某个函数指定为多播RPC,那么当服务器调用这个函数时,服务器会发送消息到所有存在这个Actor副本的客户端,指示它们在自己Actor副本上调用这个函数。当然有多播RPC,也就有客户端RPC和服务端RPC。客户端RPC和服务端RPC让你能在服务端和真正拥有这个Actor的客户端之间单独进行消息通信。

2.7 Role & Authority
- 最后还有一个概念需要知道,关于Actor的网络角色。在 Actor 的复制过程中,有两个属性扮演了重要角色,分别是Role和RemoteRole。
- 注意:一个Actor的所有权和它的网络角色不是一回事,所有权是与这个Actor相关联的PlayerController(查询一个Actor最外部的Owner肯定是PC),而通过Actor的网络角色我们可以判断当前运行的游戏实例是否负责掌管此 Actor。
- 如果 Role 是ROLE_Authority,RemoteRole 是ROLE_SimulatedProxy或ROLE_AutonomousProxy,就说明这个引擎实例负责将此 Actor 复制到远程连接。
有了这两个属性,您可以知道:
- 谁拥有 Actor的主控权
- Actor是否被复制
- 复制模式
一个Actor可以是下图几种网络角色之一

- 大多数情况下你只需要关注一个问题:当前Actor的是否具有权威性(Authority)?

- 对于不同的数值观察者,它们的 Role 和 RemoteRole 值可能发生对调。比如下图,服务器和客户端对一个Actor的Role和RemoteRole是不同的.这种情况是正常的,因为服务器要负责掌管 actor 并将其复制到客户端。而客户端只是接收更新,并在更新的间歇模拟 actor。

- 可以在运行Actor类中的一些代码之前,先检查一下权威性,如果具有权威性,那么你便可以决定如何更新这个Actor的状态。

- 如果没有权威性说明当前的代码在客户端运行,且该Actor是从服务端复制过来的,此时,客户端的Actor副本是服务端上的权威版本的代理(Proxy)。

- 复制模式: 服务器会按照AActor::NetUpdateFrequency属性指定的频度来复制Actor,而不是每次更新时都复制,以节省带宽和CPU资源。为了弥补由此导致的可能存在的Actor断续,不连贯的移动等问题,客户端将在更新的间歇中模拟Actor,目前有两种类型的模拟:ROLE_SimulatedProxy和ROLE_AutonomousProxy
- 两种类型模拟,第一种是标准的模拟途径,通常是根据上次获得的速率对移动进行推算,第二种通常只用于PlayerController所拥有的Actor,会有更多的信息来进行推算。
- 如果Actor不具有权威性,那么它的网络角色一般都是模拟代理(SimulatedProxy)
- 注意:一般来讲,自主代理(AutonomousProxy)只会在涉及到玩家的时候才会出现!
- 客户端对于其拥有的一个PlayerController副本是一个自主代理,而且这个PlayerController所控制的Pawn也是一个自主代理。对于其它客户端,这个Pawn则是作为模拟代理进行复制的。
- 自主指的是客户端能够直接控制这个Actor的移动和行为,即便对这个Actor没有完整的权威性。

- 除非你在处理玩家角色,否则只需要面对一个问题:当前是否具有权威性?

- 如果你在处理玩家角色相关代码,你就需要关注一个问题:当前玩家是否是本地控制的?否就代表是远程玩家。

- 如果你有一个复制的参与者Actor,你不仅需要考虑该Actor负责什么以及它的每个成员函数做什么,你还必须考虑该代码在哪里执行,以及数据如何在网络中流动在不同的时间

- 这些额外的复杂性刚开始可能非常棘手,但随着经验的增加也会变得越来越容易
三、Tips
- 一个Actor参与复制,不代表它的所有部分都需要使用网络功能的代码。
- 你的大多数代码最终都会基于UE的GameplayFramework进行构建。当添加你的功能时,需要注意这个函数是否只在服务端运行,或者只在客户端,还是在所有地方运行。


引自UE4_Network_Compendium_by_Cedric_eXi_Neukirchen
3. 使用断言可以使得代码的运行位置变得显而易见

4. 针对辅助函数规范命名也很有用, 比如给那些只在权威性期刊下使用的函数添加”Auth”前缀,给客户端、服务器和多播RPC添加相应的”Client“、”Server“、 ”Multicast“前缀

5. 把Actor中的功能想象成一组因果流程图,一些过程的开始与结束发生在网络的不同位置,这就是需要添加网络复制代码的地方。如下图武器开火的功能

- 如果这个过程从客户端开始并且在服务端结束,那么你一般需要服务器RPC

- 如果过程从服务器开始并且在客户端产生某种效果而结束,那么你需要考虑在哪里将数据复制到客户端。具有权威性的游戏状态更新代码会先在服务器运行(否则玩家可能因为访问并修改客户端数据而得到不公平的游戏体验),在此之后,只需要将小部分数据复制出去来重建最后的结果,并且让客户端独立处理剩下的步骤

6. 当你的代码需要支持网络功能时,一般会遇到以下几种情形,在涉及底层实现细节之前,先思考一下你所处理的功能是否符合这四种情况之一

- 如果一些步骤只在远程服务器执行,在单人游戏不执行,就先使用!HasAuthority来检查一下,确保它不具有权威性。


- 如果你有纯粹的客户端功能,它通常与Pawn或Controller相关联,因此你可以检查玩家是否在本地控制

7. 技术上的细节,UNetDriver.h文件有特别有用的关于Replication系统工作方式的信息。
四、参考
|
|