《Exploring in UE4》关于网络同步的理解与思考[概念理解]

2018-05-14作者:Jerish
https://zhuanlan.zhihu.com/c_164452593

这篇文章可以帮助你更全面的理解虚幻4引擎的网络模块~

目录

一.关于Actor与其所属连接

-1. Actor的Role是ROLE_Authority就是服务端么?

二.进一步理解RPC与同步

-1.  RPC函数应该在哪个端执行?

-2.  客户端创建的Actor能调用RPC么?

-3.  RPC与Actor同步谁先执行?

-4.  多播MultiCast RPC会发送给所有客户端么?

-5.  RPC参数与返回值

三.合理使用COND_InitialOnly

四.客户端与服务器一致么?

五.属性同步的基本规则与注意事项

-1.  结构体的属性同步

-2.  属性回调

-3.  UObject指针类型的属性同步

六.组件同步

一. 关于Actor与其所属连接

UE4官网关于网络链接这一块其实已经将的比较详细了,不过有一些内容没有经验的读者看起来可能还是比较吃力。

按照官网的顺序,我一点点给出我的分析与理解。首先,大家要简单了解一些客户端的连接过程。

主要步骤如下:

1.客户端发送连接请求

2.如果服务器接受连接,则发送当前地图

3.服务器等待客户端加载此地图

4.加载之后,服务器将在本地调用 AGameMode:reLogin。这样可以使 GameMode 有机会拒绝连接

5.如果接受连接,服务器将调用 AGameMode:ogin该函数的作用是创建一个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并且Possess了拥有连接的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写成下面的格式就可以了。

  1. void AActor::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
  2. {
  3.   DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement, COND_AutonomousOnly );
  4. }
复制代码

而经过前面的讨论我们知道ROLE_AutonomousProxy与所属连接是密不可分的。

最后,这里留一个思考问题:如果我在客户端创建出一个Actor,然后把它的Owner设置为带连接的PlayerController,那么他也有连接么?这个问题在下面的一节中回答。

1.Actor的Role是ROLE_Authority就是服务端么?

并不是,有了前面的讲述,我们已经可以理解,如果我在客户端创建一个独有的Actor(不能勾选bReplicate)。那么这个Actor的Role就是ROLE_Authority,所以这时候你就不能通过判断他的Role来确定当前调试的是客户端还是服务器。这时候最准确的办法是获取到NetDiver,然后通过NetDiver找到Connection。(事实上,GetNetMode()函数就是通过这个方法来判断当前是否是服务器的)对于服务器来说,他只有N个ClientConnections,对于客户端来说只有一个serverConnection。

如何找到NetDriver呢?可以参考下面的图片,从Outer获取到当前的Level,然后通过Level找到World。World里面就有一个NetDiver。当然,方法不止这一个了,如果有Playercontroller的话,Playercontroller上面也有NetConnection,可以再通过NetConnection再获取到NetDiver。


二. 进一步理解RPC与同步

1. RPC函数应该在哪个端执行?

对于一个形如UFUNCTION(Reliable, Client)的RPC函数,我们知道这个函数应该在服务器调用,在客户端执行。可是如果我在Standalone的端上执行该函数的时候会发生什么呢?

答案是在服务器上执行。其实这个结果完全可以参考下面的这个官方图片。

刚接触RPC的朋友可能只是简单的记住这个函数应该从哪里调用,然后在哪里执行。不过要知道,即使我声明一个在服务器调用的RPC我还是可以不按套路的在客户端去调用(有的时候并不是我们故意的,而是编写者没有理解透彻),其实这种不合理的情况UE早就帮我想到并且处理了。比如说你让自己客户端上的其他玩家去调用一个通知服务器来执行的RPC,这肯定是不合理的,因为这意味着你可以假装其他客户端随意给服务器发消息,这种操作与作弊没有区别~所以RPC机制就会果断丢弃这个操作。


所以大家可以仔细去看看上面的这个图片,对照着理解一下各个情况的执行结果,无非就是三个变量:

1.在哪个端调用

2.当前执行RPC的Actor归属于哪个连接

3.RPC的类型是什么。

2. 客户端创建的Actor能调用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就无法在客户端上正常创建,参考第4部分)。

所以,这时候调用RPC是失效的。我们不妨去思考一下,连接存在的意义本身就是一个客户端到服务器的关联,这个关联的主要目的就是为了执行同步。如果我只是在客户端创建一个给自己看的Actor,根本就不需要网络的连接信息(当然你也没有权限把它同步给服务器),所以就算他符合连接的条件,仍然是一个没有意义的连接。

同时,我们可以进一步观察这个Actor的属性,除了Role以外,Actor身上还有一个RemoteRole来表示他的对应端(如果当前端是客户端,对应端就是服务器,当前端是服务器,对应端就是客户端)。你会发现这个在客户端创建的Actor,他的Role是ROLE_Authority(并不是ROLE_AutonomousProxy),而他的RemoteRole是ROLE_None。这也说明了,这个Actor只存在于当前的客户端内。

3.  RPC与Actor同步谁先执行?

下面我们讨论一下RPC与同步直接的关系,这里提出一个这样的问题

问题:服务器ActorA在创建一个新的ActorB的函数里同时执行自身的一个Client的RPC函数,RPC与ActorB的同步哪个先执行?

答案是RPC先执行。你可以这样理解,我在创建一个Actor的同时立刻执行了RPC,那么RPC相关的操作会先封装到网络传输的包中,当这个函数执行完毕后,服务器再去调用同步函数并将相关信息封装到网络包中。所以RPC的消息是靠前的。

那么这个问题会造成什么后果呢?

1.当你创建一个新的Actor的同时(比如在一个函数内),你将这个Actor作为RPC的参数传到客户端去执行,这时候你会发现客户端的RPC函数的参数为NULL。

2.你设置了一个bool类型属性A并用UProperty标记了一个回调函数OnRep_Use。你先在服务器里面修改了A为true,同时你调用了一个RPC函数让客户端把A置为true。结果就导致你的OnRep_Use函数没有执行。但实际上,这会导致你的OnRep_Use函数里面还有其他的操作没有执行。

如果你觉得上面的情况从来没有出现过,那很好,说明暂时你的代码没有类似的问题,但是我觉得有必要提醒一下大家,因为UE4代码里面本身就有这样的问题,你以后也很有可能遇到。下面举例说明实际可能出现的问题:

情况1:当我在服务器创建一个NPC的时候,我想让我的角色去骑在NPC上并控制这个NPC,所以我立刻就让我的Controller去Possess这个NPC。在这个过程中,PlayerController就会执行UFUNCTION(Reliable, Client) void ClientRestart (APawn*

NewPawn)函数。当客户端收到这个RPC函数回调的时候就发现我的APlayerController::ClientRestart_Implementation (APawn* NewPawn)里面的参数为空~原因就是因为这个NPC刚在服务器创建还没有同步过来。

情况2:对于Pawn里面的Controller成员声明如下

  1. UPROPERTY(replicatedUsing=OnRep_Controller)
  2. AController*Controller;

  3. OnRep_Controller回调函数里面回去执行Controller->SetPawnFromRep(this);
  4. 进而执行
  5. Pawn = InPawn;
  6. 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的函数时,可能是从客户端调用,也可能是从服务器调用。虽然结果都是在服务器执行,但是过程可完全不同。从客户端调用的在实际运行时是通过网络来处理的,一定会有延迟。而从服务器调用的则会立刻执行。

4.  多播MultiCast RPC会发送给所有客户端么?

看到这个问题,你可能想这还用说么?不发给所有客户端那要多播干什么?但事实上确实不一定。

考虑到服务器上的一个NPC,在地图的最北面,有两个客户端玩家。一个玩家A在这个NPC附近,另一个玩家B在最南边看不到这个NPC(实际上就是由于距离太远,服务器没有把这个Actor同步到这个B玩家的客户端)。我们现在在这个NPC上调用多播RPC通知所有客户端上显示一个提示消失“NPC发现了宝藏”。这个消息会不会发送到B客户端上面?

1.情况一:会。多播顾名思义就是通知所有客户端,不需要考虑发送到哪一个客户端,直接遍历所有的连接发送即可。

2.情况二:不会。RPC本来就是基于Actor的,在客户端B上面连这个Actor都没有,我还可以使用RPC不会很奇怪?

第一种情况强化了多播的概念,淡化了RPC基于Actor的机制,情况二则相反。所以看起来都有道理。实际上,UE4里面更偏向第二种情况,处理如下:

如果一个多播标记为Reliable,那么他默认会给所有的客户端执行该多播事件,如果其标记的是unreliable,他就会检测该NPC与客户端B的网络相关性(即在客户端B上是否同步)。但实际上,UE还是认为开发者不应该声明一个Reliable的多播函数。下面给出UE针对这个问题的相关注释:(相关的细节在另一篇进一步深入UE网络同步的文章里面去分析)

  1. <p>// Do relevancy check if unreliable.</p><p>
  2. </p><p>// Reliables will always go out. This is odd behavior. On one hand we wish to garuntee "reliables always getthere". On the other</p><p>
  3. </p><p>// hand, replicating a reliable to something on theother side of the map that is non relevant seems weird.</p><p>
  4. </p><p>// Multicast reliables should probably never beused in gameplay code for actors that have relevancy checks. If they are, the</p><p>
  5. </p><p>// rpc will go through and the channel will be closedsoon after due to relevancy failing.</p>
复制代码

5.  RPC参数与返回值

参数:RPC函数除了UObject类型的指针以及constFString&的字符串外,其他类型的指针或者引用都不可以作为RPC的参数。对于UObject指针类型我们可以在另一端通过GUID识别(后面第五部分有讲解),但是其他类型的指针传过去是什么呢?我们根本就无法还原其地址,所以不允许传输其指针或者引用。

而对于FString,传const原因我认为是为了不想让发送方与接收方两边对字符串进行修改,而传引用只是为了减少复制构造带来的开销。在FString发送与接收的处理细节里面并不在意其是否是const&,他只在意他的类型以及相对Object的偏移。

返回值:一个RPC函数是不能有返回值的,因为其本身的执行就是一次消息的传递。假如一个客户端执行一个Server RPC,如果有返回值的话,那么岂不是服务器执行后还要再发送一个消息给客户端?这个消息怎么处理?再发一次RPC?如果还有返回值那么不就无限循环了?因此RPC函数不可以添加返回值。

三. 合理使用COND_InitialOnly

前面提到过,Actor的属性同步可以通过这种方式来实现。

声明一个属性并标记

  1. UPROPERTY(Replicated)
  2. uint8 bWeapon: 1;

  3. UPROPERTY(Replicated)
  4. uint8 bIsTargeting: 1;

  5. void Character::GetLifetimeReplicatedProps(TArray<FLifetimeProperty > & OutLifetimeProps ) const
  6. {
  7.     DOREPLIFETIME(Character,bWeapon );
  8.     DOREPLIFETIME_CONDITION(Character, bIsTargeting, COND_InitialOnly
  9. );
复制代码

这里面的第一个属性一般的属性复制,第二个就是条件属性复制。条件属性复制无非就是告诉引擎,这个属性在哪些情况下同步,哪些情况下不同步。这些条件都是引擎事先提供好的。

这里我想着重的提一下COND_InitialOnly这个条件宏,汉语的官方文档是这样描述的:该属性仅在初始数据组尝试发送。而英文是这样描述的:This property will only attempt to send on the initial bunch。对比一下,果然还是英文看起来更直观一点。

经过测试,这个条件的效果就是这个宏声明的属性只会在Actor初始化的时候同步一次,接下来的游戏过程中不会再同步。所以,我们大概能想到这个东西在有些时候确实用的到,比如同步玩家的姓名,是男还是女等,这些游戏开始到结束一般都不会改变的属性。也就是说,上限一般调整的次数很少,如果真的有调整并需要同步,他会手动调用函数去同步该属性。这样就可以减少同步带来的压力。 然而,一旦你声明为COND_InitialOnly。你就要清楚,同步只会执行一次,客户端的OnRep回调函数就会执行一次。所以,当你在服务器创建了一个新的Actor的时候你需要第一时间把需要改变的值修改好,一旦你在下一帧(或是下一秒)去执行那么这个属性就无法正确的同步到客户端了。

四.客户端与服务器一致么?

我们已经知道UE4的客户端与服务器公用一套代码,那么我们在每次写代码的时候就有必要提醒一下自己。这段代码在哪个端执行,客户端与服务器执行与表现是否一致?

虽然,我很早之前就知道这个问题,但是写代码的时候还是总是忽略这个问题,而且程序功能经常看起来运行的没什么问题。不过看起来正常不代表逻辑正常,有的时候同步机制帮你同步一些东西,有时候会删除一些东西,有时候又会生成一些东西,然而你可能一点都没发现。

举个例子,我在一个ActorBeginPlay的时候给他创建一个粒子Emiter。代码大概如下:

  1. void AGate::BeginPlay()
  2. {
  3.    Super::BeginPlay();
  4.   //单纯的在当前位置创建粒子发射器
  5.    GetWorld()->SpawnActor<AEmitter>(SpawnEmitter,GetActorLocation(), UVictoryCore::RTransform(SpawnEmitterRotationOffset,GetActorRotation()));
  6. }
复制代码

代码很简单,不过也值得我们分析一下。

首先,服务器下,当Actor创建的时候就会执行BeginPlay,然后在服务器创建了一个粒子发射器。这一步在服务器(DedicateServer)创建的粒子其实就是不需要的,所以一般来说,这种纯客户端表现的内容我们不需要在专用服务器上创建。

再来看一下客户端,当创建一个Gate的时候,服务器会同步到客户端一个Gate,然后客户端的Gate执行BeginPlay,创建粒子。这时候我们已经发现二者执行BeginPlay的时机不一样了。进一步测试,发现当玩家远离Gate的时候,由于UE的同步机制(只会同步一定范围内的Actor),客户端的Gate会被销毁,而粒子发射器也会销毁。而当玩家再次靠近的时候,Gate又被同步过来了,原来的粒子发射器也被同步过来。而因为客户端再次执行了BeginPlay,又创建了一个新的粒子,这样就会导致不断的创建新的粒子。

你觉得上面的描述准确么?

并不准确,因为上述逻辑的执行还需要一个前置条件——这个粒子的bReplicate属性是为false的。有的时候,我们可能一不小心就写出来上面这种代码,但是表现上确实正常的,为什么?因为SpawnActor是否成功是有条件限制的,在生成过程中有一个函数

  1. bool AActor::TemplateAllowActorSpawn(UWorld* World,const FVector& AtLocation, const FRotator& AtRotation, const struct
  2. FActorSpawnParameters& SpawnParameters)
  3. {
  4.     return !bReplicates || SpawnParameters.bRemoteOwned||World->GetNetMode() != NM_Client;
  5. }
复制代码

如果你是在客户端,且这个Actor勾选了bReplicate的话,TemplateAllowActorSpawn就会返回false,创建Actor就会失败。如果这个Actor没有勾选bReplicate的话,那么服务器只会创建一个,客户端就可能不断的创建,而且服务器上的这个Actor与客户端的Actor没有任何关系。

另外,还有一种常见的错误。就是我们的代码执行是有条件的,然而这个条件在客户端与服务器是不一样的(没同步)。如

  1. void Gate::CreateParticle(int32 ID)
  2. {
  3.    if(GateID!= ID)
  4.    {
  5.       FActorSpawnParameters SpawnInfo;
  6.       GetWorld()->SpawnActor<AEmitter>(SpawnEmitter, GetActorLocation(),GetActorRotation(), SpawnInfo);
  7.    }
  8. }
复制代码

这个GateID是我们在GateBeginPlay的时候随机初始化的,然而这个GateID只在服务器与客户端是不同的。所以需要服务器同步到客户端,才能按照我们理想的逻辑去执行。

五. 属性同步的基本规则与注意事项

非休眠状态下的Actor的属性同步:只在服务器属性值发生改变的情况下执行

回调函数执行条件:服务器同步过来的数值与客户端不同

休眠的Actor:不同步

首先要认识到,同步操作触发是由服务器决定的,所以不管客户端是什么值,服务器觉得该同步就会把数据同步到客户端。而回调操作是客户端执行,所以客户端会判断与当前的值是否相同来决定是否产生回调。

然后是属性同步,属性同步的基本原理就是服务器在创建同步通道的时候给每一个Actor对象创建一个属性变化表(这里面涉及到FObjectReplicator,FRepLayout,FRepState,FRepChangedPropertyTracker相关的类,有兴趣可以进一步了解,在另一深入UE网络同步文章里有讲解),里面会记录一个当前默认的Actor属性值。之后,每次属性发生变化的时候,服务器都会判断新的值与当前属性变化表里面的值是否相同,如果不同就把数据同步到客户端并修改属性变化表里的数据。对于一个非休眠且保持连接的Actor,他的属性变化表是一直存在的,所以他的表现出来的同步规则也很简单,只要服务器变化就同步。

动态数组TArray在网络中是可以正常同步的,系统会检测到你的数组长度是否发生了变化,并通知客户端改变。

1.  结构体的属性同步

注意,UE里面UStruct类型的结构体在反射系统中对应的是UScriptStruct,他本身可以被标记Replicated并且结构体内的数据默认都会被同步,而且如果里面有还子结构体的话也仍然会递归的进行同步。如果不想同步的话,需要在对应的属性标记NotReplicated,而且这个标记只对UStruct有效,对UClass无效。

有一点特别的是,Struct结构内的数据是不能标记Replicated的。如果你给Struct里面的属性标记replicated,UHT在编译的时候就会提醒你编译失败。

最后,UE里面的UStruct不可以以成员指针的方式在类中声明。

2.  属性回调

问题:属性回调与RPC在使用结果上的差异?

属性回调理论上一定会执行,而RPC函数有可能由于错过执行时机而不再会执行。例如:我在服务器上面有一个宝箱,第一个玩家过去后,宝箱会自动开启。如果使用RPC函数,当第一个玩家过去后,箱子执行多播RPC函数触发开箱子操作。但是由于其他的玩家离这个箱子很远,所有这个箱子没有同步给其他玩家,其他玩家收不到这个RPC消息。(如果对结果有疑问参考第二节的第四个问题)当这些玩家之后再过去之后,会发现箱子还是关闭的。如果采用属性回调,但第一个玩家过去后,设置箱子的属性bOpen为true,然后同步到所有客户端,通过属性回调执行开箱子操作。这时候其他玩家靠近箱子时,箱子会同步到靠近的玩家,然后玩家在客户端上会收到属性bOpen,同时执行属性回调,这时候可以实现所有靠近的玩家都会发现箱子已经被别人开过了。

问题:服务器上生成一个Actor,他在客户端上的UObject类型指针的属性回调与他的Beginplay谁先执行?

这个问题这么看有点奇怪,我进一步描述一下。有一个类MyActor,他有一个指针属性PropertyB指向一个同步的MyActorB,同时这个指针属性有一个回调函数。现在我在服务器创建一个新的MyActor A,并设置A的PropertyB为MyActorB。那么在客户端上,是A的BeginPlay先执行,还是PropertyB的属性回调先执行?

答案是不确定,一开始的时候,我一直认为是属性回调在Actor的BeginPlay之前执行,测试了很多次也是这样的。但是某种情况下, BeginPlay会先执行。这个问题的意义就在于,一个Actor同步过去执行BeginPlay的时候,你发现他的属性还没有同步过来(而且只发现指针可能没有同步过来,其他内置类型都会在BeginPlay 前同步过来)。为什么指针没有同步过来?因为这个指针同步过来的时候,他指向的对象在客户端还不存在,他在客户端上也没有对应的GUID缓存 。由于找不到对应的对象,他只能先暂时记录下这个指针指向对象的GUID,然后在其他的Tick时间再回来检测这个对象是否存在。这种情况一般来说很难重现,不过这个问题有助于我们进一步加深对网络的理解。

3. UObject指针类型的属性同步

属性同步也好,RPC参数也好。我们都需要思考一下,我在传递一个UObject类型的指针时,这个UObject在客户端存在么?如果存在,我如何能通过服务器的一个指针找到客户端上相同UObject的指针?

答案是通过FNetworkGUID。服务器在同步一个对象引用(指针)的时候,会给其分配专门的FNetworkGUID并通过网络进行发送。客户端上通过识别这个ID,就可以找到对应UObject。

那么如此说来,是不是只有标记Replicate的对象才能同步其引用或指针呢?

也不是。对于直接从数据包加载出来的对象(如地图里面实现搭建好的建筑地形),我们可以直接认为服务器上的该地形对象与客户端上对应的地形对象就是一个对象,那么在服务器上指向该地形的指针发送到客户端也应该就是指向对应地形的指针。所以总结来说一个UObject对象是否可以通过网络发送他的引用有如下条件(参考官方文档):

您通常可以按照以下原则来确定是否可以通过网络引用一个对象:

任何复制的 actor 都可以复制为一个引用

任何未复制的 actor 都必须有可靠命名(直接从数据包加载)

任何复制的组件都可以复制为一个引用

任何未复制的组件都必须有可靠命名。

其他所有 UObject(非actor 或组件)必须由加载的数据包直接提供

什么是拥有可靠命名的对象?

拥有可靠命名的对象指的是存在于服务器和客户端上的同名对象。

1.如果Actor 是从数据包直接加载(并非在游戏期间生成),它们就被认为是拥有可靠命名。

2.满足以下条件的组件即拥有可靠命名:

● 从数据包直接加载

● 通过construction scripts脚本添加

● 采用手动标记(通过 UActorComponent::SetNetAddressable 设置)

● 只有当您知道要手动命名组件以便其在服务器和客户端上具有相同名称时,才应当使用这种方法(最好的例子就是 AActor C++ 构造函数中添加的组件)

最后总结一下就是有四种情况下UObject对象的引用可以在网络上传递成功

1.标记replicate

2.从数据包直接Load

3.通过Construction scripts添加或者C++构造函数里面添加

4.使用UActorComponent::SetNetAddressable标记(这个只针对组件,其实蓝图里面创建的组件默认就会执行这个操作)

六.组件同步

组件在同步上分为两大类:静态组件与动态组件。

对于静态组件:一旦一个Actor被标记为同步,那么这个Actor身上默认所挂载的组件也会随Actor一起同步到客户端(也需要序列化发送)。什么是默认挂载的组件?就是C++构造函数里面创建的默认组件或者在蓝图里面添加构建的组件。所以,这个过程与该组件是否标记为Replicate是没有关系的。

对于动态组件:就是我们在游戏运行的时候,服务器创建或者删除的组件。比如,当玩家走进一个洞穴时,给洞穴里面的火把生成一个粒子特效组件,然后同步到客户端上,当玩家离开的时候再删除这个组件,玩家的客户端上也随之删除这个组件。

对于动态组件,我们必须要设置他的Replicate属性为true,即通过函数 AActorComponent ::SetIsReplicated(true)来操作。而对于静态组件,如果我们不想同步组件上面的属性,我们就没有必要设置Replicate属性。

一旦我们执行了SetIsReplicated(true)。那么组件在属性同步以及RPC上与Actor的同步几乎没有区别,组件上也需要设置GetLifetimeReplicatedProps来执行属性同步,Actor同步的时候会遍历他的子组件查看是否标记Replicate以及是否有属性要同步。

  1. bool AActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)
  2. {
  3.      check(Channel);
  4.      check(Bunch);
  5.      check(RepFlags);

  6.      bool  WroteSomething = false;
  7.      for(UActorComponent* ActorComp : ReplicatedComponents)
  8.      {
  9.          if(ActorComp && ActorComp->GetIsReplicated())
  10.          {
  11.            //Lets the component add subobjects before replicating its own properties.
  12.             WroteSomething|= ActorComp->ReplicateSubobjects(Channel, Bunch,RepFlags);
  13.            //(this makes those subobjects 'supported', and from here on those objects mayhave reference replicated)  子对象(包括子组件)的同步,其实是在ActorChannel里进行
  14.             WroteSomething |= Channel->ReplicateSubobject(ActorComp,*Bunch,*RepFlags);
  15.          }
  16.       }
  17.      return  WroteSomething;
  18. }
复制代码

对于C++默认的组件,需要放在构造函数里面构造并设置同步,UE给出了一个例子:

  1. ACharacter::ACharacter()
  2. {
  3.    // Etc...
  4.    CharacterMovement = CreateDefaultSubobject<UMovementComp_Character>(TEXT("CharMoveComp");
  5.    if (CharacterMovement)
  6.    {
  7.      CharacterMovement->UpdatedComponent = CapsuleComponent;
  8.      CharacterMovement->GetNavAgentProperties()->bCanJump = true;
  9.      CharacterMovement->GetNavAgentProperties()->bCanWalk = true;
  10.      CharacterMovement->SetJumpAllowed(true);
  11.      //Make DSO components net addressable 实际上如果设置了Replicate之后,这句代码就没有必要执行了
  12.      CharacterMovement->SetNetAddressable();
  13.      // Enable replication by default
  14.      CharacterMovement->SetIsReplicated(true);
  15.     }
  16. }
复制代码