UE4⽹络同步详解(⼀)——理解同步规则
这篇⽂章主要以问题的形式,针对UE同步的各个⽅⾯的内容,做⼀个详细⽽充分的讨论。对于新⼿理解UE的同步机制⾮常有帮助,对于有⼀定的基础⽽没有深⼊的UE程序也或许有⼀些启发。如果想深⼊了解同步的实现原理,可以参考
问题⼀:如何理解Actor与其所属连接?
附加:1. Actor的Role是ROLE_Authority就是服务端么?
问题⼆:你真的会⽤RPC么?
bounce
附加:1. 多播MultiCast RPC会发送给所有客户端么?
问题三:COND_InitialOnly怎么⽤?
独立主格问题四:客户端与服务器⼀致么?
问题五:属性同步的基本规则是?
附加:1. 结构体的属性同步有什么特别的?
问题六:组件同步的基本规则是?
Tips:同步注意的⼀些⼩细节
问题⼀:如何理解Actor与其所属连接?
按照官⽹的顺序,我⼀点点给出我的分析与理解。⾸先,⼤家要简单了解⼀些客户端的连接过程。
主要步骤如下:
1.客户端发送连接请求。
2.如果服务器接受连接,则发送当前地图。
呼叫保持
3.服务器等待客户端加载此地图。
4.加载之后,服务器将在本地调⽤ AGameMode::PreLogin。这样可以使 GameMode 有机会拒绝连接
5.如果接受连接,服务器将调⽤ AGameMode::Login该函数的作⽤是创建⼀个 PlayerController,可⽤于在今后复制到新连接
诚信在我身边
的客户端。成功接收后,这个 PlayerController 将替代客户端的临时PlayerController (之前被⽤作连接过程中的占位符)。
此时将调⽤ APlayerController::BeginPlay。应当注意的是,在此 actor 上调⽤RPC 函数尚存在安全风险。您应当等待
AGameMode::PostLogin 被调⽤完成。
6.如果⼀切顺利,AGameMode::PostLogin 将被调⽤。
这时,可以放⼼的让服务器在此 PlayerController 上开始调⽤RPC 函数。
那么这⾥⾯第5点需要重点强调⼀下。我们知道所谓连接,不过就是客户端连接到⼀个服务器,在维持着这个连接的条件下,我们才能真正的玩“⽹络游戏”。通常,如果我们想让服务器把某些特定的信息发送给特定的客户端,我们就需要找到服务器与客户端之间的这个连接。这个链接的信息就存储在PlayerController的⾥⾯,⽽这个PlayerController不能是随随便便创建的PlayerController,⼀定是客户端第⼀次链接到服务器,服务器同步过来的这个PlayerController(也就是上⾯的第五点,后⾯称其为拥有连接的PlayerController)。进⼀步来说,这个Controller⾥⾯包含着相关的NetDriver,Connection以及Session信息。
对于任何⼀个Actor(客户端上),他可以有连接,也可以⽆连接。⼀旦Actor有连接,他的Role(控制权限)就是
ROLE_AutonomousProxy,如果没有连接,他的Role(控制权限)就是ROLE_SimulatedProxy 。
那么对于⼀个Actor,他有三种⽅法来得到这个连接(或者说让⾃⼰属于这个连接):
1.设置⾃⼰的owner为拥有连接的PlayerController,或者⾃⼰owner的owner为拥有连接的PlayerController。也就说官⽅⽂档
说的查找他最外层的owner是否是PlayerController⽽且这个PlayerController拥有连接。
2.这个Actor必须是Pawn并且Posss了拥有连接的PlayerController。这个例⼦就是我们打开例⼦程序时,开始控制⼀个⾓⾊
的情况。我们控制的这个⾓⾊就拥有这个连接。
3.这个Actor设置⾃⼰的owner为拥有连接的Pawn。这个区别于第⼀点的就是,Pawn与Controller的绑定⽅式不是通过Owner这
个属性。⽽是Pawn本⾝就拥有Controller这个属性。所以Pawn的Owner可能为空。 (Owner这个属性在Actor⾥⾯,蓝图也可以通过GetOwner来获取)
对于组件来说,那就是先获取到他所归属的那个Actor,然后再通过上⾯的条件来判断。
我这⾥举⼏个例⼦,玩家PlayerState的owner就是拥有连接的PlayerController,Hud的owner是拥有连接的
PlayerController,CameraActor的owner也是拥有连接的PlayerController。⽽客户端上的其他NPC(⼀定是在服务器创建的)是都没有owner的Actor,所以这些NPC都是没有连接的,他们的Role就为ROLE_SimulatedProxy。
所以我们发现这些与客户端玩家控制息息相关的Actor才拥有所谓的连接。不过,进⼀步来讲,我们要这连接还有什么⽤?好吧,照搬官⽅⽂档。
连接所有权是以下情形中的重要因素:
1.RPC需要确定哪个客户端将执⾏运⾏于客户端的 RPC
2.Actor复制与连接相关性
3.在涉及所有者时的 Actor 属性复制条件
对于RPC,我们知道,UE4⾥⾯在Actor上调⽤RPC函数,可以实现类似在客户端与服务器之间发送可执⾏的函数的功能。最基本的,当我⼀个客户端拥有ROLE_AutonomousProxy权限的Actor在服务器代码⾥调⽤RPC函数(UFUNCTION(Reliable,Client))时,我怎么知道应该去众多的客户端的哪⼀个⾥⾯执⾏这个函数。(RPC的⽤法不细说,参考官⽅⽂档)答案就是通过这个Actor所包含的连接。关于RPC进⼀步的内容,下个问题⾥再详细描述。
第⼆点,Actor本⾝是可以同步的,他的属性当然也是。这与连接所有权也是息息相关。因为有的东西我们只需要同步给特定的客户端,其他的客户端不需要知道,(⽐如我当前的摄像机相关内容)。
对于第三点,其实就是Actor的属性是否同步可以进⼀步根据条件来做限制,有时候我们想限制某个属性只在拥有
ROLE_AutonomousProxy的Actor使⽤,那么我们对这个Actor的属性ReplicatedMovement写成下⾯的格式就可以了。
voidAActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty > &OutLifetimeProps )const
damain{
DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement,COND_AutonomousOnly );
visin}
⽽经过前⾯的讨论我们知道ROLE_AutonomousProxy与所属连接是密不可分的。
最后,这⾥留⼀个思考问题:如果我在客户端创建出⼀个Actor,然后把它的Owner设置为带连接的PlayerController,那么他也有连接么?这个问题在下⾯的⼀节中回答。
附加:Actor的Role是ROLE_Authority就是服务端么?
并不是,有了前⾯的讲述,我们已经可以理解,如果我在客户端创建⼀个独有的Actor(不能勾选bReplicate,参考第五条思考)。那么这个Actor的Role就是ROLE_Authority,所以这时候你就不能通过判断他的Role来确定当前调试的是客户端还是服务器。这时候最准确的办法是获取到NetDiver,然后通过NetDiver找到Connection。(事实上,GetNetMode()函数就是通过这个⽅法来判断当前是否是服务器的)对于服务器来说,他只有N个ClientConnections,对于客户端来说只有⼀个rverConnection。
如何找到NetDriver呢?可以参考下⾯的图⽚,从Outer获取到当前的Level,然后通过Level找到World。World⾥⾯就有⼀个NetDiver。当然,⽅法不⽌这⼀个了,如果有Playercontroller的话,Playe
rcontroller上⾯也有NetConnection,可以再通过NetConnection再获取到NetDiver。还可以通过堆栈,找到World。
问题⼆:你真的会⽤RPC么?
在看下⾯的图之前,先提出⼀个问题:
对于⼀个形如UFUNCTION(Reliable,Client)的RPC函数,我们知道这个函数应该在服务器调⽤,在客户端执⾏。可是如果我在Standalone的端上执⾏该函数的时候会发⽣什么呢?
答案是在服务器上执⾏。其实这个结果完全可以参考下⾯的这个官⽅图⽚。
刚接触RPC的朋友可能只是简单的记住这个函数应该从哪⾥调⽤,然后在哪⾥执⾏。不过要知道,即使我声明⼀个在服务器调⽤的RPC 我还是可以不按套路的在客户端去调⽤(有的时候并不是我们故意的,⽽是编写者没有理解透彻),其实这种不合理的情况UE早就帮我想到并且处理了。⽐如说你让⾃⼰客户端上的其他玩家去调⽤⼀个通知服务器来执⾏的RPC,这肯定是不合理的,因为这意味着你可以假装其他客户端随意给服务器发消息,这种操作与作弊没有区别~所以RPC机制就会果断丢弃这个操作。
所以⼤家可以仔细去看看上⾯的这个图⽚,对照着理解⼀下各个情况的执⾏结果,⽆⾮就是三个变量:1、在哪个端调⽤2、当前执⾏RPC的Actor归属于哪个连接3、RPC的类型是什么。
雅思考试写作技巧
不过看到这⾥,再结合上⼀节结尾提到的问题,如果我在客户端创建⼀个Actor。把这个Actor的Owner设置为⼀个带连接PlayerController会怎么样呢?如果在这⾥调⽤RPC呢?
我们确实可以通过下⾯这种⽅式在客户端给新⽣成的Actor指定⼀个Owner。
好吧,关键时候还是得搬出来官⽅⽂档的内容。
您必须满⾜⼀些要求才能充分发挥 RPC 的作⽤:
1. 它们必须从 Actor 上调⽤。
2. Actor必须被复制。
3. 如果 RPC 是从服务器调⽤并在客户端上执⾏,则只有实际拥有这个 Actor 的客户端才会执⾏函数。
4. 如果 RPC 是从客户端调⽤并在服务器上执⾏,客户端就必须拥有调⽤ RPC 的 Actor。
5. 多播 RPC 则是个例外:
o 如果它们是从服务器调⽤,服务器将在本地和所有已连接的客户端上执⾏它们。
o 如果它们是从客户端调⽤,则只在本地⽽⾮服务器上执⾏。
o 现在,我们有了⼀个简单的多播事件限制机制:在特定 Actor 的⽹络更新期内,多播函数将不会复制两次以上。按长期计划,我们会对此进⾏改善,同时更好的⽀持跨通道流量管理与限制。
看完第⼆条,其实你就能理解了,你的Actor必须要被复制,也就是说必须是bReplicate属性为true,Actor是从服务器创建并同步给客户端的(客户端如果勾选了bReplicate就⽆法在客户端上正常创建,参考第四条问题)。所以,这时候调⽤RPC是失效的。我们不妨去思考⼀下,连接存在的意义本⾝就是⼀个客户端到服务器的关联,这个关联的主要⽬的就是为了执⾏同步。如果我只是在客户端创建⼀个给⾃⼰看的Actor,根本就不需要⽹络的连接信息(当然你也没有权限把它同步给服务器),所以就算他符合连接的条件,仍然是⼀个没有意义的连接。同时,我们可以进⼀步观察这个Actor的属性,除了Role以外,Actor⾝上还有⼀个RemoteRole来表⽰他的对应端(如果当前端是客户端,对应端就是服务器,当前端是服务器,对应端就是客户端)。你会发现这个在客户端创建的Actor,他的Role是
ROLE_Authority(并不是ROLE_AutonomousProxy),⽽他的RemoteRole是ROLE_None。这也说明了,这个Actor只存在于当前的客户端内。
下⾯我们讨论⼀下RPC与同步直接的关系,这⾥先提出⼀个问题,
问题:服务器ActorA在创建⼀个新的ActorB的函数⾥同时执⾏⾃⾝的⼀个Client的RPC函数,RPC与ActorB的同步哪个先执⾏?
答案是RPC先执⾏。你可以这样理解,我在创建⼀个Actor的同时⽴刻执⾏了RPC,那么RPC相关的操作会先封装到⽹络传输的包中,当这个函数执⾏完毕后,服务器再去调⽤同步函数并将相关信息封装到⽹络包中。所以RPC的消息是靠前的。
在线英语翻译汉语那么这个问题会造成什么后果呢?
1. 当你创建⼀个新的Actor的同时(⽐如在⼀个函数内),你将这个Actor作为RPC的参数传到客户端去执⾏,这时候你会发现客户端的RPC函数的参数为NULL。
2. 你设置了⼀个bool类型属性A并⽤UProperty标记了⼀个回调函数OnRep_U。你先在服务器⾥⾯修改了A为true,同时你调⽤了⼀个RPC函数让客户端把A置为true。结果就导致你的OnRep_U函数没有执⾏。但实际上,这会导致你的OnRep_U函数⾥⾯还有其他的操作没有执⾏。
如果你觉得上⾯的情况从来没有出现过,那很好,说明暂时你的代码没有类似的问题,
但是我觉得有必要提醒⼀下⼤家,因为UE4代码⾥⾯本⾝就有这样的问题,你以后也很有可能遇到。下⾯举例说明实际可能出现的问题:
情况1:当我在服务器创建⼀个NPC的时候,我想让我的⾓⾊去骑在NPC上并控制这个NPC,所以我⽴刻就让我的Controller去Posss这个NPC。在这个过程中,PlayerController就会执⾏UFUNCTION(Reliable,Client) void ClientRestart
(APawn*NewPawn)函数。当客户端收到这个RPC函数回调的时候就发现我的APlayerController::ClientRestart_Implementation (APawn* NewPawn)⾥⾯的参数为空~原因就是因为这个NPC刚在服务器创建还没有同步过来。
情况2:对于Pawn⾥⾯的Controller成员声明如下
UPROPERTY(replicatedUsing = OnRep_Controller)
AController* Controller;
OnRep_Controller回调函数⾥⾯回去执⾏Controller->SetPawnFromRep(this);进⽽执⾏
Pawn = InPawn;
OnRep_Pawn();
下⾯重点来了,OnRep_Pawn函数⾥⾯会执⾏OldPawn->Controller=NULL;将客户端之前Controller控制的⾓⾊的Controller设置为空。到现在来看没有什么问题。那么现在结合上⾯第⼆个问题,如果⼀个RPC函数执⾏的时候在客户端的Controller同步前就修改为正确的Controller,那么OnRep_Controller回调函数就不会执⾏。所以客户端的原来Controller控制的OldPawn的Controller就不会置为空,导致的结果是客户端和服务器竟然不⼀样。
实际上,确实存在这么⼀个函数,这个RPC函数就是ClientRestart。这看起来就很奇怪,因为ClientRestart如果没有正常执⾏的
话,OnRep_Controller就会执⾏,进⽽导致客户端的oldPawn的Controller为空(与服务器不同,因为服务器并没有去设置OldPawn的Controller)。我不清楚这是不是UE4本⾝设计上的BUG。(不要妄想⽤AlwaysReplicate宏去解决,参考第⼋条有关AlwaysReplicate的使⽤)
不管怎么说,你需要清楚的是RPC的执⾏与同步的执⾏是有先后关系的,⽽这种关系会影响到代码的逻辑,所以之后的代码有必要考虑到这⼀点。
最后,对使⽤RPC的朋友做⼀个提醒,有些时候我们在使⽤UPROPERTY标记Server的函数时,可能是从客户端调⽤,也可能是从服务器调⽤。虽然结果都是在服务器执⾏,但是过程可完全不同。从客户端调⽤的在实际运⾏时是通过⽹络来处理的,⼀定会有延迟。⽽从服务器调⽤的则会⽴刻执⾏。
how to forget附加:1.多播MultiCast RPC会发送给所有客户端么?
看到这个问题,你可能想这还⽤说么?不发给所有客户端那要多播⼲什么?但事实上确实不⼀定。
考虑到服务器上的⼀个NPC,在地图的最北⾯,有两个客户端玩家。⼀个玩家A在这个NPC附近,另⼀个玩家B在最南边看不到这个新年快乐 英文
NPC(实际上就是由于距离太远,服务器没有把这个Actor同步到这个B玩家的客户端)。我们现在在这个NPC上调⽤多播RPC通知所有客户端上显⽰⼀个提⽰消失“NPC发现了宝藏”。这个消息会不会发送到B客户端上⾯?
情况⼀:会。多播顾名思义就是通知所有客户端,不需要考虑发送到哪⼀个客户端,直接遍历所有的连接发送即可。
情况⼆:不会。RPC本来就是基于Actor的,在客户端B上⾯连这个Actor都没有,我还可以使⽤RPC不会很奇怪?
第⼀种情况强化了多播的概念,淡化了RPC基于Actor的机制,情况⼆则相反。所以看起来都有道理。实际上,UE4⾥⾯更偏向第⼆种情况,处理如下:
如果⼀个多播标记为Reliable,那么他默认会给所有的客户端执⾏该多播事件,如果其标记的是unreliable,他就会检测该NPC与客户端B 的⽹络相关性(即在客户端B上是否同步)。但实际上,UE还是认为开发者不应该声明⼀个Reliable的多播函数。下⾯给出UE针对这个问题的相关注释:(相关的细节在另⼀篇进⼀步探索UE⽹络同步的⽂档⾥⾯去分析)
// Do relevancy check if unreliable.
// Reliables will always go out. This is oddbehavior. On one hand we wish to garuntee "reliables always getthere". On the other
// hand, replicating a reliable to something on theother side of the map that is non relevant ems weird.
// Multicast reliables should probably never beud in gameplay code for actors that have relevancy checks. If they are, the
// rpc will go through and the channel will be clodsoon after due to relevancy failing.