最近在学习UE网络同步,发现了一个非常好的教程,并且附带了项目文件,这里从这个小项目入手,理解UE的网络同步
教程链接:https://www.youtube.com/watch?v=JOJP0CvpB8w
项目链接:https://github.com/awforsythe/Repsi/
文章目录
- 项目目标
- 网络模式
- Actor复制
- RPC
- 所有权
- Authority & Role
- 一些tips
项目目标
首先明确下这个项目要实现什么目标,多人游戏,射击命中场景中的小球,小球会变成玩家角色对应的颜色。
网络模式
单人游戏的只需要考虑客户端的实现,而多人游戏在UE中有两种网络模式,一种是ListenServer,玩家的主机作为监听服务器,接收来自其他玩家的连接,而另一种是DedicatedServer,单独的服务器供所有玩家连接。
这里我们采用 DedicatedServer
,其相较于ListenServer
的好处是我们可以指在服务器端处理逻辑,而在客户端只用渲染和接收玩家输入,可以防作弊,减少客户端的处理逻辑。
可以在编辑器的下方进行选项的配置
而服务端和客户端是通过如下方式连接起来的,服务端和客户端皆有
UGameEngine -> UNetDriver,而UNetDriver下对于服务端持有对多个对客户端的连接,而每个客户端持有对一个服务端的连接,
而对于一个客户端和服务端之间的连接,其持有多个Channel对应多个要同步的对象
Actor复制
设置 Actor 的 bReplicates = true
它会在对应的 Player 连接的 Channels 中加入对应的 UActorChannel
,那么客户端和服务端就会利用这个Channel来交换对应Actor的信息。
也可以在运行时设置
而 Actor 本身的生命周期由服务端控制,那么当服务端生成了一个要同步的Actor,该Actor会复制到客户端,如果服务端删除了一个 Actor,对应的客户端也要删除相应的 Actor。
而对于 Actor 属性来说,如果服务端修改了属性,那么修改后的属性会同步到客户端。C++中需要在 UPROPERTY 中加入 REPLICATED
同时在 GetLifetimeReplicateProps
增加对应实现
当然我们也可以有选择地去设置属性的复制,比如下面是跳过拥有该属性的所有权的客户端
有时候我们客户端同步属性的时候,可能不止是做简单的复制,我们可以使用 RepNotify
在复制的时候做一些其他的操作。
上面这种只会在客户端调用,我们想要在服务端也执行该函数的话,可以在服务端的执行流程中加入该函数的调用
对于 Actor 还有一个 Relevancy 属性,相关性决定了 Actor 会被复制到哪些客户端,
比如有一些 Actor(比如GameState、PlayerState)就是和所有客户端同步的,它们的 bAlwaysRelevant = true
RPC
远程过程调用(RPC) 是在本地调用但在其他机器上远程执行函数,在C++中我们可以在 UFUNCTION
中添加对应关键字就能将函数声明为RPC,可选项有:
- Server —— 表示在服务器上执行
- Client —— 表示在特定客户端上执行
- NetMulticast —— 表示在所有客户端上执行
C++中的函数定义后面要加上 _Implementation
后缀。
需要注意就算是 Multicast 也需要考虑相关性,Actor 都不被这些客户端持有,当然也不能在这些客户端上调用RPC
RPC可以设置为 可靠的和非可靠的,非可靠的可以减少带宽,但可能会导致RPC丢失或者数据到达顺序不被保证,而可靠的RPC保证到达,且在同一个Actor内RPC到达顺序与调用顺序一致
Server RPC 函数声明可以在UFUNCTION
中加上 WithValidation
,同时添加后缀为 _Validate
的实现,用于检测客户端传递过来的数据是否是合法的,可用于作弊检查,如果返回 false
会把客户端踢出游戏。
Server RPC 也是客户端向服务端传输数据的唯一方式。RPC会立即发送,所以适用于高优先级的网络代码,比如角色类中通过RPC传输位置。
所有权
客户端的连接拥有 Controller(每个客户端有其对应的一个Controller,也不会持有其他客户端Controller的副本),而Controller拥有PlayerState,我们可以看作连接也拥有 PlayerState,当然Controller也会拥有处理的Apawn,Apawn可能会持有Aweapon,通过所有权的设置,我们可以得到一个Actor是被哪个Player拥有(一个树形关系)
所有权(Ownership)也会影响 Relevance,比如下面这个 Actor 的所有者是 P0,且设置 bOnlyRelevantToOwner = true
那么该Actor不会被同步到未具有其所有权的客户端。
且可以通过 Owner 关系从拥有者传递相关性到被拥有者,要设置 bNetUseOwnerRelevancy=true
如果一个Actor没有设置拥有者,那么客户端未拥有这个Actor,那么默认的行为如下:
- 如果 Actor 被隐藏且其根组件的碰撞被关闭,它不会被认为是相关的
- 否则相关性基于与玩家的距离,如果小于设置的网络剔除距离,则被认为是与该Player相关的
当然我们也可以在 Actor 中自定义IsNetRelevantFor
的逻辑
Authority & Role
一个 Actor 在不同的机器上可能有不同的 Role,比如 玩家0 的机器上的角色对象本身的 Role 是 ROLE_AutonomousProxy
在服务端是 ROLE_Authority
而在其他玩家的机器上是 ROLE_SimulateProxy
我们可以根据 Role 来决定 Actor 在不同机器上的执行流程(如果是GameInstance是NM_Client且这个actor是由客户端产生,那么该Actor在产生它的客户端上的ROLE也是 Authority
)而如果 GameInstance是NM_Client而这个actor由服务端产生,那么actor在这个客户端上的ROLE是非 Authority
具体地一些Actor在不同机器上的情况可以看下表
不同的Pawn在不同机器上的 IsLocallyControlled()
是不同的
一些tips
我们引入了网络同步无疑会影响我们代码的复杂性
比如单机情况下 APawn
的代码组织结构如下
而在引入网络的情况下,服务端和客户端的代码结构是不一样的,要复杂了很多
不同的Actor会位于不同的机器上(服务端 or 客户端)
我们要根据不同对Actor的不同需求,赋予他们对应的所有权,以让他们可以在不同机器上实现