unreal游戏内同步机制_unreal fps 同步 - CSDN
  • UE4网络同步详解(一)——理解同步规则

    万次阅读 多人点赞 2019-12-05 20:19:49
    对于新手理解UE的同步机制非常有帮助,对于有一定的基础而没有深入的UE程序也或许有一些启发。如果想深入了解同步的实现原理,可以参考UE4网络同步(二)——深入同步细节 问题一:如何理解Actor与其所属连接? ...

        这篇文章主要以问题的形式,针对UE同步的各个方面的内容,做一个详细而充分的讨论。对于新手理解UE的同步机制非常有帮助,对于有一定的基础而没有深入的UE程序也或许有一些启发。如果想深入了解同步的实现原理,可以参考  UE4网络同步(二)——深入同步细节

            问题一:如何理解Actor与其所属连接?

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

            问题二:你真的会用RPC么?

      附加:1. 多播MultiCast RPC会发送给所有客户端么?

            问题三:COND_InitialOnly怎么用?

            问题四:客户端与服务器一致么?

            问题五:属性同步的基本规则是?

                附加:1.  结构体的属性同步有什么特别的?

     问题六:组件同步的基本规则是?

            Tips:同步注意的一些小细节

     

     

    问题一:如何理解Actor与其所属连接?

          https://api.unrealengine.com/CHN/Gameplay/Networking/Actors/OwningConnections/index.html。UE4官网关于网络链接这一块其实已经将的比较详细了,不过有一些内容没有经验的读者看起来可能还是比较吃力。

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

     主要步骤如下:

          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并且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写成下面的格式就可以了。

           voidAActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty > &OutLifetimeProps )const
         {
              DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement,COND_AutonomousOnly );
         }

     

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

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

    附加: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。还可以通过堆栈,找到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_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成员声明如下
         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的函数时,可能是从客户端调用,也可能是从服务器调用。虽然结果都是在服务器执行,但是过程可完全不同。从客户端调用的在实际运行时是通过网络来处理的,一定会有延迟。而从服务器调用的则会立刻执行。

     附加: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 seems weird.
    
    // Multicast reliables should probably never beused in gameplay code for actors that have relevancy checks. If they are, the
    
    // rpc will go through and the channel will be closedsoon after due to relevancy failing.

     

    问题三:COND_InitialOnly怎么用?

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

         声明一个属性并标记

         UPROPERTY(Replicated)
         uint8    bWeapon: 1;
         UPROPERTY(Replicated)
         uint8    bIsTargeting: 1;
         voidCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty > &OutLifetimeProps ) const
         {
              DOREPLIFETIME(Character,bWeapon );
              DOREPLIFETIME_CONDITION(Character, bIsTargeting,  COND_InitialOnly);
         }

     

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

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

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

     

    问题四:客户端与服务器一致么?

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

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

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

         voidAGate::BeginPlay()
         {
               Super::BeginPlay();
              //单纯的在当前位置创建粒子发射器
              GetWorld()->SpawnActor<AEmitter>(SpawnEmitter,GetActorLocation(),UVictory
              Core::RTransform(SpawnEmitterRotationOffset,GetActorRotation()));
         }

     

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

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

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

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

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

         bool AActor::TemplateAllowActorSpawn(UWorld* World,const FVector& AtLocation, const FRotator& AtRotation, const structFActorSpawnParameters& SpawnParameters)
         {
             return !bReplicates || SpawnParameters.bRemoteOwned||World->GetNetMode() != NM_Client;
         }

     

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

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

         voidGate::CreateParticle(int32 ID)
         {
              if(GateID!= ID)
              {
                    FActorSpawnParameters SpawnInfo;
                   GetWorld()->SpawnActor<AEmitter>(SpawnEmitter, GetActorLocation(),  GetActorRotation(), SpawnInfo);
             }
         }

     

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

     

    问题五:属性同步的基本规则是?

         单纯的非休眠状态Actor的属性同步比较简单,但是一旦涉及到休眠状态,回调函数的执行,还是值得总结一下的。

         非休眠状态下的Actor的属性同步:只在服务器属性值发生改变的情况下执行
         回调函数执行条件:服务器同步过来的数值与客户端不同

         休眠的ACtor:不同步

         首先要认识到,同步操作触发是由服务器决定的,所以不管客户端是什么值,服务器觉得该同步就会把数据同步到客户端。而回调操作是客户端执行,所以客户端会判断与当前的值是否相同来决定是否产生回调。
        然后是属性同步,属性同步的基本原理就是服务器在创建同步通道的时候给每一个Actor对象创建一个属性变化表(这里面涉及到FObjectReplicator,FRepLayout,FRepState,FRepChangedPropertyTracker相关的类,有兴趣可以进一步了解,我也会在另一个博客里面去讲解),里面会记录一个当前默认的Actor属性值。之后,每次属性发生变化的时候,服务器都会判断新的值与当前属性变化表里面的值是否相同,如果不同就把数据同步到客户端并修改属性变化表里的数据。对于一个非休眠且保持连接的Actor,他的属性变化表是一直存在的,所以他的表现出来的同步规则也很简单,只要服务器变化就同步。

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

     

    附加:结构体属性同步有什么特别的么?


         注意,UE里面UStruct类型的结构体在反射系统中对应的是UScriptStruct,他本身可以被标记Replicated并且结构体内的数据默认都会被同步,而且如果里面有还子结构体的话也仍然会递归的进行同步。如果不想同步的话,需要在对应的属性标记NotReplicated,而且这个标记只对UStruct有效,对UClass无效。另外,如果是Ustruct数组一定要在内部属性标记Uproperty,否在在数组同步的时候就会产生崩溃。
        有一点特别的是,Struct结构内的数据是不能标记Replicated的。如果你给Struct里面的属性标记replicated,UHT在编译的时候就会提醒你编译失败。

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

     

     

    问题六:组件同步的基本规则是?

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

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

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

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

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

    boolAActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)
    
    {
            ......
    
             boolWroteSomething = false;
    
             for(UActorComponent* ActorComp : ReplicatedComponents)
    
             {
    
                       if(ActorComp && ActorComp->GetIsReplicated())
    
                       {
    
    //Lets the component add subobjects before replicating its own properties.
    
                                WroteSomething|= ActorComp->ReplicateSubobjects(Channel, Bunch,RepFlags);        
    
    //(this makes those subobjects 'supported', and from here on those objects mayhave reference replicated)     子对象(包括子组件)的同步,其实是在ActorChannel里进行
    
                       WroteSomething |= Channel->ReplicateSubobject(ActorComp,*Bunch,*RepFlags);
    
                       }
    
             }
    
             returnWroteSomething;
    
    }
    
     
    
    对于C++默认的组件,需要放在构造函数里面构造并设置同步,UE给出了一个例子:
    
    ACharacter::ACharacter()
    
    {
    
       // Etc...
    
       CharacterMovement = CreateDefaultSubobject<UMovementComp_Character>(TEXT("CharMoveComp");
    
       if (CharacterMovement)
    
       {
    
           CharacterMovement->UpdatedComponent = CapsuleComponent;
    
           CharacterMovement->GetNavAgentProperties()->bCanJump = true;
    
           CharacterMovement->GetNavAgentProperties()->bCanWalk = true;
    
           CharacterMovement->SetJumpAllowed(true);
    
             //Make DSO components net addressable 实际上如果设置了Replicate之后,这句代码就没有必要执行了
    
           CharacterMovement->SetNetAddressable();
    
              // Enable replication by default
    
           CharacterMovement->SetIsReplicated(true);
    
              
    
       }
    
    }
     
    

     

        如果想进一步的深入网络同步的相关细节,我会在下一篇博客里面进一步分析讲解。

     

    Tips:同步的一些小细节?

    1.当前新版的Server RPC好像要求必须加 reliable/unreliable ,以及WithValidation

    一旦加上WithValidation,还必须要添加一个验证函数。像下面这样,

    UFUNCTION(Server, unreliable, WithValidation)
    void ServerSpawnTestActor();

    virtual bool ServerSpawnTestActor_Validate();

     

    2.有属性同步我们知道必须要添加GetLifetimeReplicatedProps,但是同时要在.cpp里面添加头文件#include "Net/UnrealNetwork.h",否则找不到FLifetimeProperty

    void ALevelTestCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>&OutLifetimeProps) const
    {
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(ALevelTestCharacter, TestRepActor);
    }

     

    3.看编译错误不要看VS的错误窗口,会看晕的,一定要看输出窗口的错误提示

     

    4.所有的Tick事件的注册都是在AActor::BeginPlay()里面完成的,所以重写各种Actor函数时一定别忘了加Super::XXXXX();

     

    原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78244492

     

    展开全文
  • Unreal中使用了一套非常Amazing的网络同步机制, 虽然由于种种原因,实际项目中把Atlas,同步等都弃之不用,完全自行处理网络模块, 但就其机制上,还是非常值得玩味的~ 通过UDN,及BeyondUnreal上的Wiki资料,...

     

    背景

    Unreal中使用了一套非常Amazing的网络同步机制,

    虽然由于种种原因,实际项目中把Atlas,同步等都弃之不用,完全自行处理网络模块,

    但就其机制上,还是非常值得玩味的~

    通过UDN,及BeyondUnreal上的Wiki资料,可以对此有一些初步的了解~

    (PS:

    想做些翻译,及Sample,但苦于时间实在不够,先在此挖个坑,日后来填~~

    --Zephyroal

    概要

    In Unreal Engine games replication is the concept of passing data between the server and clients in network games. Closely related is simulation, which is the concept of "guessing" server behavior on network clients using only the information already available to the client.

    Despite consisting of a relatively small set of rules, applying replication concepts usually scares programmers new to UnrealScript and makes even UnrealScript veterans scratch their head once in a while。。。

         在Unreal引擎中,replication 是一个用于在服务器与客户端同步数据的一个重要概念,

    现在还没有很好的翻译,这里先姑且称之为“副本”,当然此副本非彼游戏副本也,

          在UE的Dev代码库中,我们也可以看到很多simulation修饰符的函数,它们的作用既是为此而生,

    简而言之,replication 的作用便是写一份代码,自动地分离,同时维护服务器计算、客户端交互。

          当然,初入USript,这些突如其来的概念也许会让你抓耳饶腮,但是,who cares?!Just do it!

     

    Basics

     

    Replication concepts

     

    Replication examples

    函数

    Network modifiers

    These modifiers affect function replication in Unreal Engine 3. Earlier engine generations use the replication block to define these.

    Client3
    Specifies that this function should be replicated to the client owning the actor if it is called on the server. This modifier automatically makes the function simulated as well.
    DemoRecording3-x1
    Specifies that this function should be replicated to the demorecording driver. It will only be executed during demo playback. This modifier automatically makes the function simulated as well, which makes sense since demo playback essentially is a client environment.
    Reliable3
    Replicated functions will be replicated reliably when marked with this modifier.
    Server3
    Specifies that this function should be replicated to the server if it was called on a replicated actor that is owned by the local client.
    Unreliable3
    Replicated functions will be replicated unreliably when marked with this modifier, i.e. they may be dropped due to packet loss or bandwidth saturation.

    相关Topic

    《Everything you ever wanted to know about replication (but were afraid to ask)》

    Replication is a mighty beast lurking inside the Unreal Engine that even seasoned UnrealScript programmers treat with a lot of respect. With this article I'll try to explain how replication works and hopefully get rid of some myths and misunderstandings on that topic.

    I'm not that good at writing tutorials, so feedback on the discussion page is very welcome. —Wormbo

    Contents

    [hide]

     

     Things to keep in mind while reading

    We're on the Unreal Wiki, a site full of tutorials and reference documentation. I won't go into detail for every feature, because you can find that elsewhere. You really should have gathered some experience with the language itself and know how to use it properly before you take on scary features like replication. If you still have a question, try looking it up on the wiki first. If you can't find an answer, you can of course still ask on this article's discussion page. For more complex questions you might want to post on a forum instead, though.

    One fact people probably don't expect is that the demo recording feature of the UT series of games is internally handled much like a network game. When you record a demo you are something similar to a listen server and when you play back a demo you are a client. Even if you write an offline-only mod, as soon as you want to support demo recording, you will have to deal with replication. The difference between demos and network play is that when recording a demo, the "server" doesn't expect the client to respond (network traffic is just dumped to a file) and that the "client" in demo playback will discard any data that is supposed to be sent to the server.

    Another interesting case is UTV. A UTV server basically is a proxy server that looks like a special client on the game server, but acts as a server to its own clients. A UTV server's clients are spectators that can interact with each other via chat, but except for the primary client their data stays on the UTV proxy and doesn't reach the game server. Additionally the UTV proxy intentionally delays the game server's data so UTV clients cannot be used by the players to cheat in some way.

     

    Background - What you start with on the client side

    I won't explain how you should set up a server and connect to it with a game client, that part is covered in great detail elsewhere. This section is about what you start with after the game loaded a map on the client.

    To make it short: You start with most of what the mapper added to his level. Particularly all non-actor objects (sounds, textures, meshes) used in the map will be loaded. Some of the actors the mapper placed will be missing, though. To be precise, the engine deletes all actors that have neither bStatic nor bNoDelete set to True. Level geometry, most lights, movers, navigation points, keypoints, emitters, decorations and many other actors aren't affected by this. However, all Pawns (especially placed monsters, vehicles and turrets), Projectiles, in UE1/2 also Pickups, and triggers will be gone. All of the remaining actors will have their Role and RemoteRole values exchanged, except for ClientMovers and similar actors marked as bClientAuthoritative. Quite a lot of the static and non-deletable actors end up with Role set to ROLE_None here, but that doesn't mean they don't exist on the client. It only means they won't receive any property updates through replication.

    See What happens at map startup for what else happens before replication kicks in.

     

    Replication basis - Actor replication

    F6 network stats.

    So, how does the client get to know about them? They do show up when you play the game, right? The basic concept responsible here is actor replication. For each relevant actor, the engine creates an "actor channel" between the server and the target client. The number of active channels can be viewed via the stat net command, which is usually bound to the F6 key. Note that the number of channels listed there is the total number of channels. Most of these are actor channels, but the engine also has other channel types, e.g. for voice chat.

    There are actually two flavors of actor replication, one for static and non-deletable actors (those that aren't deleted at map load on the client) and one for any other actors that were either deleted at map load or spawned on the server at runtime. As mentioned above, static or non-deletable actors already exist in the client world, so their flavor of actor replication just establishes a channel between the corresponding server and client instances. It should be mentioned that static actors can only be subject to replication if they were already marked as bAlwaysRelevant before map load.

    The other version is for actors that are neither bStatic nor bNoDelete (let's call them "runtime actors" because they can be spawned and destroyed at runtime) and requires a bit more work, as the target actor does not exist on the client. The server basically tells the client to spawn an actor of the required type and it also tells where to spawn it. By spawning the actor on the client, all its properties start at the class default values. See What happens when an Actor is spawned for details on how actors are initialized. The most important part here is that the actor gets its Role and RemoteRole values exchanged before any UnrealScript code is executed.

    In case this isn't immediately obvious: Runtime actors placed by the mapper are deleted at map load and then possibly spawned again through actor replication. When they are replicated, all their properties are reset to class default values. Some of the change done by the mapper might later be reconstructed through other means, but for now they are gone.

     

     Network relevance - Which actors are replicated

    Most of what this article explained was from the network client's point of view. Let's switch to the server side for a while to discuss a very important thing, network bandwidth. Bandwidth is usually the most-restricted parameter in a network. Data is sent sequentially, so apart from the time it takes for data to travel anyway, some of the data needs to wait while other data is transmitted. This further increases response times ("ping"), which is undesirable in most games. Games can't reduce the actual travel time of the data, that's a fixed property of the underlying network architecture. They can, however, attempt to reduce the data's waiting time by reducing the overall amount of data to transmit. The Unreal Engine employs several tricks to reduce the overall amount of data, but the best way always is to not send any data at all.

    To figure out which actors need to be replicated to a client at all, the engine performs several checks to see if the actor is relevant to the client. These checks can be summed up in the following rules that will be tested roughly in the given order:

    1. If the actor is bAlwaysRelevant, it is relevant to the client.
    2. If the client or its view target owns the actor, it is relevant.
    3. If the client is a UTV see-all spectator, the actor is relevant.
    4. If the client can hear the actor's ambient sound, it is relevant.
    5. If the actor is based on (or attached to the bone of) another actor, it is relevant to the client if the other actor is.
    6. If the actor is bHidden or bOnlyOwnerSee and neither blocks other actors nor has an ambient sound, it is not relevant to the client.
    7. If the actor is in a zone with distance fog and is further away from the client's view location than the distance fog end, it is not relevant to the client.
    8. If there is BSP geometry between the client's view location and the actor's center (!), the actor is not relevant.
    9. The server may decide to check if the actor is behind some terrain and/or beyond its CullDistance if it needs to save bandwidth, which may result in the actor being not relevant to the client.
    10. The actor is relevant to the client.

    Note that, while they usually prevent an actor from being rendered, anti portals do not affect network relevance, probably because testing every actor against every single AntiPortalActor for every client may be far too expensive.

    Once an actor became relevant, it will continue to be considered relevant until the above rules fail for more than a few seconds. The duration is configured via RelevantTimeout=... under [IpDrv.TcpNetDrv] in the server's main configuration file for UE1/2 or the Engine.ini in UE3. The default value is 5 seconds and provides a good balance between getting rid of non-relevant actors and not having to restart replication too often for actors that often switch between being relevant and being not.

    If a previously net-relevant actor is really no longer relevant, its channel to the client is closed and the actor is destroyed on the client. (See What happens when an Actor is destroyed for details.) If the actor becomes relevant again later, it will be spawned again as a completely new actor. If an actor is destroyed on the server, its channel is closed as well, causing the corresponding actor instances on all clients to be destroyed.

    There are also two other ways to close an actor channel, which don't destroy the client instance. When that happens, the client takes over "simulation" of the actor behavior without any further help from the server. One way is the property bNetTemporary, which closes the actor channel immediately after the initial set of properties has been replicated (see below). This mode is used for most projectiles that don't change their movement after spawning, except for a potential influence of gravity. Projectiles that allow interaction other than the usual explode/bounce-on-impact logic usually don't use bNetTemporary. This includes projectiles that can be blown up (e.g. shock projectile, Redeemer or AVRiL rocket), that track down a target (e.g. seeking rockets or spider mines) or that simply stick to a target (e.g. bio goo or sticky grenades). bNetTemporary also has the advantage that the server doesn't need to remember which variable values it replicated to clients, but more on that later.

    The other way is the bTearOff property, which also closes the actor channel, but it also swaps the Role and RemoteRole properties of the actor again so the client side instance becomes an "authoritative" instance. Unlike bNetTemporary, which can only be set in the defaultproperties, bTearOff is set on the server at runtime to "tear off" replication to all clients at the same time. On the clients the actor was relevant to, the event TornOff() is called for the actor. Once an actor is "torn off", it will no longer be replicated to new clients it might become relevant to.

     

     Variable replication - Updating properties on the client

    Alright, now that you know how actors are brought to existence on clients, it's time to think about how to get modifications across the network. Remember, when a replicated actor is spawned on a client, it starts with its class defaults and the only information from the server is the actor's location and potentially its rotation, if it matters. Any other properties are sent separately through what is called variable replication.

    Replicated properties are always replicated from the server to all or a specific subset of the clients, but of course only to clients to which a channel for the actor exists. In Unreal Engine 1 there was also the possibility to replicate variables from the client owning the actor to the server, but that feature has been dropped in favor of sending the values via replicated function calls. (We'll see about that one later.) One left-over of that two-way replication is that almost all variable replication conditions in stock code contain the term Role == ROLE_Authority.

     

     Replication conditions

    Wait, what's a "replication condition"? Well, as mentioned before, variable replication can be restricted to a specific subset of the relevant clients. The subset is selected via a bool-type expression known as the replication condition. Replication conditions are specified in a special area of the source code, the replication block. Each class may only contain one replication block. Inside there may be one or more replication conditions, each applying to one or more variables or functions. Only one condition may be specified for a variable or function and you are not allowed to specify replication conditions for members inherited from a parent class.

    A typical replication block in UE2 might look as follows:

    
    {
        (bNetOwner)
        ThisVarOnlyConcernsTheOwner
     
        (bNetInitial)
        ThisVarIsOnlyReplicatedOnce
    }
    

    In UE3 it would be similar, except that the "reliable" or "unreliable" keyword is missing. That keyword doesn't have any effect on variable replication, it only exists because it affects the way function calls are replicated, but we'll get into that later.

    Technically the boolean expression between the parentheses after the "if" is standard UnrealScript code, so you could call functions there if you want. In practice, however, nobody will do that because the time and frequency at which replication conditions are evaluated is unpredictable. Also, this is deep inside network code and should be as quick as possible. Because of that, some classes have their replication conditions implemented in native code, which is specified in the class declaration via the NativeReplication modifier. These classes still have a replication block so you can figure out when exactly the various properties are replicated. Also, NativeReplication only applies to variable replication, not to replicated function calls.

    So, what kind of conditions can you use? Here are a few properties you may find useful:

    bNetInitial
    True only for the initial bunch of variables replicated in addition to the information for spawning the actor on the client.
    bNetDirty2,3
    True whenever variables changed on the actor. To be honest I'm not entirely sure why this exists as variables always only get replicated if they changed from what the server thinks the client's value is.
    bNetOwner
    True only if the actor is owned by the client.
    bDemoRecording
    True if replicating to the demo recording driver instead of a "real" network connection.
    bClientDemoRecording
    True if the demo is being recorded on a network client, false if recording offline or on a server or not recording a demo at all.
    bRepClientDemo
    True on the server if the actor is owned by a client that currently records a demo.
    Level.ReplicationViewer2
    The PlayerController of the client currently replicating to.
    Level.ReplicationViewTarget2
    The ReplicationViewer's current view target.
    WorldInfo.ReplicationViewers3
    A dynamic array with information about the PlayerController(s) on the target client, their view target, view location and view direction. It's an array because UE3 allows more than one player on a client if splitscreen mode is enabled.
    Role
    This actor's local network role. You only need to check it when replicating variables in UE1 or when replicating function calls in UE1/2.

    If you look around in the replication blocks of stock classes, you may find other variables being used. For example an actor's Mesh is only replicated if the DrawType is DT_Mesh.

     

     What is replicated and when?

    So, when exactly does variable replication happen? The short answer is "between world updates, if anything changed". But the server doesn't really check all actors after each tick. Each actor class has a NetUpdateFrequency, which tells how often per second the actor should be checked for changed replicated variables. The first check is of course done right when the actor becomes relevant to the client and bNetTemporary actors won't get any further updates after that. For all other relevant actors, the engine repeats checks for changed variables about every 1.0/NetUpdateFrequency. Usually there's only limited bandwidth available, so the engine needs to prioritize the various actors. This is done via the NetPriority property. The higher an actor's priority is, the more likely it will be updated. However, lower priority actors won't "starve", because the longer an actor has to wait for its update check, the more likely it will be updated during the next round of checks.

    In Unreal Engine 1 you can't control at which time an update check for replicated variables happens. In Unreal Engine 2 you can force (well, at least strongly suggest) updates earlier by setting the NetUpdateTime to a value in the past, e.g. Level.TimeSeconds - 1. Unreal Engine 3 provides the property bForceNetUpdate, which can be set to True for an immediate update.

    The server keeps track of what each client knows about the actors replicated to it and their replicated variable values. The initial assumption about what the client knows is built based on the serverside class defaults, which includes localized and configurable values read from the localization/config files. In other words, config/globalconfig properties might not initially get replicated because the server thinks the client already knows about them. It is really recommended you use separate properties for replicating configurable values. Similarly if you edit class defaults at runtime and then spawn a new replicated actor, the server will not know you have changed the defaults and just assume the client knows about it.

    Every time a variable is send to the client, the server will remember its value for that client. This may use a good amount of memory, but it helps the server save bandwidth by not having to replicate the same value again. Consider the following scenario: The server replicated a certain value to the client, then the variable is modified on the server multiple times, eventually ending up the same as it was when the server replicated it. None of the changes were replicated yet because they happened too quickly, but the server marked the actor as having changed properties. Now it's time again to check for properties to replicate. The server will look up what values it sent to the client last time and finds that the replicated property hasn't actually changed. To save bandwidth, the server won't send the property value again, because the client already knows about it.

     

     Value compression

    As mentioned in the section about relevance, the engine has a few tricks to reduce the amount of data it needs to send. One of these tricks is that it compresses certain value types for transfer and uncompressing them. This compression is not lossless, but actually changes the value that arrives at the client. This doesn't apply to basic types, but only to certain structs:

    Vector
    The components are rounded to the nearest integer and send as integer data. This way small vectors only require several bits up to a few bytes, while the original three uncompressed float values would have required 12 bytes. If you need more than integer precision, you should multiply the vector by a scalar value before assigning it to the replicated variable.
    Rotator
    Only bits 9 to 16 of the components are transfered, which corresponds to the operation C & 0xff00. That way the required data amount is reduced from 12 to about 3 bytes. (It seems zero components even only take up a single bit, reducing the minimum size to 3 bits for the zero rotator.) The compression restricts replicated rotator values to rotations and makes them useless for rotation rates. To replicate a rotation rate, you could copy the rotator components to the components of a vector variable. Note that you shouldn't use typecasting to vector because that results in a unit vectors, which not only discards the Roll component entirely, but also is heavily affected by vector compression.
    Quat
    Values are assumed to be unit quaternions, allowing the engine to drop the W component from replication entirely and calculating it from X, Y and Z on the client. As a result Quat values require only 12 instead of 16 bytes.
    CompressedPosition2
    The struct consists of vectors for location and velocity and a rotator for rotation. The vectors are replicated as usual, but because this struct is used to pack a player position, the Roll component of the rotation is not replicated at all, while the Pitch and Yaw components receive the usual compression to byte size.
    Plane
    Components are rounded to signed integers in the range [-32768,32767]. That corresponds to a data size reduction of 50%.

     

     Detecting replicated values on the client

    Most of the time you just let values replicate so they are available on the client. Sometimes, however, you will want to react to certain property changes immediately. Depending on the engine generation you have different options to react to replicated variables changing.

    In Unreal Engine 1 you're entirely on your own as there is no notification. You will have to keep a backup copy of the variable you are monitoring and frequently check the backup against the original, e.g. in Tick() or a Timer().

    Unreal Engine 2 at least tells you that it received replicated variables, but it doesn't tell you which variables were replicated. You need to set bNetNotify to True on the client to receive a PostNetReceive() call when a new bunch of replicated variable values arrived. It should be mentioned that if you only want to get a notification for a single, infrequent event, you can toggle the value of bClientTrigger. This will call the ClientTrigger() event as soon as the changed value arrives on the client.

    Finally in Unreal Engine 3 you don't have to figure out which variable was changed, because the engine tells you. To get replication notifications, simply declare the corresponding variable with the modifier RepNotify and the engine will call the ReplicatedEvent() function with the variable's name as the parameter whenever a value for that variable is received.

    Note that variables are not always replicated immediately when they are changed. Usually the engine makes sure there are at least 1/NetUpdateFrequency seconds between variable updates for a single actor. Also, actors with a higher NetPriority are usually preferred when there's not enough space to replicate changed properties in all relevant actors. Actors with a lower priority may have to wait longer for their variables to replicate.

    To get instant replication at the expense of the ability to pick more than one target client, you can use replicated function calls instead.

     

     Restrictions

    Not all types can be replicated, others may only replicate properly under certain conditions. For example dynamic arrays cannot be replicated at all. Any variable's value must at least fit into a single network packet to be replicated, but if multiple values from the same actor are small enough to fit into the same packet, then they will be transferred together, saving some overhead.

    Strings and structs can only be replicated as a whole, while the elements of a static array are treated as separate variables for replication. That means, a static array with hundreds of relatively small elements may replicate just fine, while a long string or a very complex struct may fail. Note that static arrays in structs are subject to the "structs are replicated as a unit" rule, while dynamic arrays in a struct will be excluded from the struct replication data.

    Actor or object references are another thing where you need to pay attention. Actor references can only be replicated if the referenced actor is either bStatic or bNoDelete or is currently relevant to the target client. Non-actor object references, such as classes, sounds, textures or meshes, will only reach the client if the object wasn't created at runtime. Non-Actor objects (not a reference, but the object itself) are generally not replicated, so you always need an actor if you want to establish a "connection" between the server and a client.

    It might be obvious from the article already, but just in case: There is no way to achieve direct replication between clients. Clients can only communicate with the server.

     

     Function call replication - Sending messages between server and client

    The word "messages" should be understood in a much wider range than just text messages. UnrealScript functions can have up to 16 parameters and each parameter can have one of many built-in and custom types. Replicated function calls can use almost the entire range of feature you can imagine. For parameters the same restrictions apply as for variable replication, with two additions: The entire function call with all parameter values must fit into a single network packet, and only the first element of a static array parameter is replicated, the others are set to their corresponding null value. If you hit the upper data size limit, you may have to find a way to break down the data into separate calls. If you need to replicate a static array, wrap it into a struct. This also makes passing it around in other cases much easier because structs can be copied as a whole, while static arrays cannot.

    Ok, that said, let's look at how to replicate a function call. This differs between UE1/2 and UE3. In engine generations 1 and 2 you use the replication block to specify when to replicate the function call to the remote end. Usually you will include Role == ROLE_Authority for functions you want to send from the server to the client and Role != ROLE_Authority for functions the client should send to the server other terms are extremely rare in the replication condition. Keep one thing in mind: Function replication always implies bNetOwner, i.e. function call are only replicated if the executing actor is owned by a client, and the call will only be replicated to/from that owning client.

    Unreal Engine 3 no longer uses the replication block to specify replicated function conditions. Instead it provides function modifiers to specify the replication direction. The modifier server means if the function is called on the client owning the actor, the call should be replicated to the server, while the modifier client means the server should replicate the function call to the client owning the actor. Because it makes sense, the client modifier also implies the modifier simulated to ensure the function can definitely be executed on the client when it arrives. Another modifier is demorecording, which means the function should be replicated to the demo recording driver.

     

     Calling replicated functions

    If a replicated function is called and its replication condition is met, the call and all parameter values passed to it will be sent to the remote side immediately. If the condition isn't met, the function will be called locally instead. That means, if the executing actor does not have any Owner or the owner does not belong to any clients, the function call is evaluated as if the function isn't defined to be replicated. For functions replicated from a client to the server this usually means the function call is ignored because it lacks the simulated keyword. Calls from the server that stay on the server don't have such a "failsafe switch" and will cause the function to be executed there.

    Note that while replicated functions are allowed to have a return type, the actual return value will be that type's null value if the function is replicated successfully. The code will not wait for the function to be executed and return a value, that's just not feasible for a game engine. If you want a replicated function to send by a value, you need to do that via a parameter of another replicated function that is sent in the other direction. Similarly if a replicated function has out parameters, their value will not change if the function is replicated. On the remote side, any out parameters or return values will be discarded when the function has finished.

    Note that the parameters of replicated functions are subject to the same variable compression strategies as mentioned in the section about variable replication. Additionally, any parameter that is a null value is omitted from the replication data to save bandwidth. This goes only for entire parameter values, not for individual members of a struct used as a parameter type.

     

     Reliability

    Function call replication can be either "reliable" or "unreliable", which is specified by the keywords of the same name either in the replication condition in UE1/2 or as optional function modifier in UE3. If a function is marked as "reliable", the engine makes sure it is processed in the correct order in relation to other reliable network events, especially other reliable function calls. But also opening and closing an actor channel is a reliable event. In other words, provided they are called after the actor channel is opened by the server, reliably replicated function calls are guaranteed to be processed on the client while the actor exists and they are guaranteed top be processed in the same order as they were called on the server.

    Why is the order important? I'll spare you the gory details, but we need to get a bit more technical to answer that. The Unreal Engine uses UDP to transmit its data. This protocol does not actually create a connection, but just sends packets to the target address. It doesn't even guarantee that the packets arrive, let alone that they arrive in the same order they were sent. Due to the way the internet works, different packets might takes different routes and overtake each other. They may get dropped somewhere or even get duplicated.

    Sounds like a nightmare, but the lack of checks also has a big advantage. The TCP protocol would implement guaranteed order and data integrity, but all of its checks cause a lot of overhead and slow down transfers. That might not be a problem for file transfers (HTTP, FTP and the various mail protocols are built on TCP), but for a game where low response times are crucial, this would be a catastrophe. Thus the engine swallows the bitter pill and performs its own checks for dropped, duplicated and out-of-order packets. These checks are only performed for important things like opening/closing actor channel or reliable replicated function calls. Note that even reliable function calls might get lost when there's packet loss, but the calls that do arrive are guaranteed to be executed in the correct order.

    Unreliable function calls on the other hand might not even get send if the connection is saturated. If they are sent, they are more likely to get lost, they could be duplicated or be called out of the correct order. If the ordering gets really bad, they may even arrive after their channel is already closed or before it was opened on the client, in which case they are dropped. In stock code, unreliable functions are used for things like replicating sounds, less important visual effects and (this may be surprising) player input. If one player input packet is lost, this usually isn't a great problem as the server extrapolates movement and the client has some freedom in correcting the server's extrapolation errors. Losing a jump or fire event may be a bit annoying, but the sheer amount of input packets causes unreliable replication to provide a huge advantage compared to reliable replication, including better response times. Duplicated and out-of-order packets are caught by a timestamp value in the function call, which allows the server to discard any obsolete updates.

    So when deciding whether to make a function reliable or unreliable ask yourself the following questions: Is it really that bad if the function call gets lost underway or isn't received in the correct order? And if so, would the advantages of making it reliable outweigh the response time penalty caused by the engine ensuring the correct order?

     

    Ok, what's with that "simulated" keyword?

    Ah yes, that weird function modifier. In fact, it can also be applied to states to affect state code in the same way. Remember the talk about Role and RemoteRole and how they are exchanged on the client up in the first few sections of this article? Well, the simulated keyword, or actually the lack of it, is related to the value of the Role property. Actor instances (as opposed to static functions and non-actor objects) will execute code in their functions and states only if the actor's Role is higher than ROLE_SimulatedProxy or if the function or state is marked as simulated.

    Offline and on a server all actors have ROLE_Authority as their Role value, and the same goes for "runtime actors" (remember? bStatic and bNoDelete both set to False) created on the client via the Spawn() function, i.e. not received through replication. Also bStatic or bNoDelete actors that are bClientAuthoritative don't get their roles exchanged on clients, and replicated actors that are "torn off" get their roles exchanged back to the original values, so these also have a Role of ROLE_Authority on the client.

    Now the rule says "either simulated or Role higher than ROLE_SimulatedProxy", but ROLE_Authority is not the only role satisfying that rule. There's also ROLE_AutonomousProxy, which is used by the local PlayerController and its Pawn on the client. That is actually set as the RemoteRole value on the server, but replication magic downgrades it to ROLE_SimulatedProxy on other clients so it really only applies to the owning client.

    On the other side, there are also ROLE_DumbProxy (at least in UE1/2) and ROLE_None. Remember how mapper-placed actors may end up with Role set to ROLE_None on clients? It just means you can't use replication on them, but nothing would prevent you from calling simulated functions on these actors, if they had any.

     

     

     

     

    转载于:https://www.cnblogs.com/Zephyroal/archive/2012/02/29/2372944.html

    展开全文
  • UE4网络同步(二)——深入同步细节

    万次阅读 热门讨论 2019-12-05 20:37:41
    我这里主要是从同步的流程分析,以同步机制为讲解核心,给大家描述里面是怎么同步的,会大量涉及UE同步模块的底层代码,稍微涉及一点计算机网络底层(Socket相关)相关的知识。 PS:如果只是想知道怎么使用同步,...

    前言

    UE同步是一块比较复杂而庞大的模块,里面设计到了很多设计思想,技巧,技术。我这里主要是从同步的流程分析,以同步的机制为讲解核心,给大家描述里面是怎么同步的,会大量涉及UE同步模块的底层代码,稍微涉及一点计算机网络底层(Socket相关)相关的知识。
    PS:如果只是想知道怎么使用同步,不建议阅读这篇文章,不过可以参考我另外一篇博客 UE4网络同步(一)——理解同步规则
    另外,博主参考的源码版本比较旧,有些细节略有差异,大家可以作为参考。有时间我会对此进行更新
    目录

    一.基本概念

    二.通信的基本流程

    三.连接的建立

      1. 服务器网络模块初始化流程
      1. 客户端网络模块初始化流程
      1. 服务器与客户端建立连接流程

    四.Actor的同步细节

    五.属性同步细节

      1. 属性同步概述
      1. 重要数据的初始化流程
      1. 发送同步数据流程分析
      1. 属性回调函数执行
      1. 关于动态数组与结构体的同步

    六.RPC执行细节

    七.其他网络特性

      1. Reliable与可靠数据传输

    一. 基本概念

    UE网络是一个相当复杂的模块,这篇文档主要是针对Actor同步,属性同步,RPC等大致的阐述一些流程以及关键的一些类。这里我尽可能将我的理解写下来。
    在UE里面有一些和同步相关的概念与类,这里逐个列举一下并做解释:

    底层通信:

    • Bunch
      一个Bunch里面主要记录了Channel信息,NGUID。同时包含其他的附属信息如是否是完整的Bunch,是否是可靠的等等,可以简单理解为一个数据包,该数据包的数据可能不完整,继承自FNetBitWriter
      InBunch:从Channel接收的数据流串
      OutBunch:从Channel产生的数据流串
    • FBitWriter
      字节流书写器,可以临时写入比特数据用于传输,存储等,继承自FArchive
    • FSocket
      所有平台Socket的基类。
      FSocketBSD:使用winSocket的Socket封装
    • Packet
      从Socket读出来/输出的数据
    • UPackageMap
      生成与维护Object与NGUID的映射,负责Object的序列化。每一个Connection对应一个UPackageMap
      (Packet与Bunch的区别:Packet里面可能不包含Bunch信息)

    基本网络通信:

    • NetDriver
      网络驱动,实际上我们创建使用的是他的子类IPNetDriver,里面封装了基本的同步Actor的操作,初始化客户端与服务器的连接,建立属性记录表,处理RPC函数,创建Socket,构建并管理当前Connection信息,接收数据包等等基本操作。NetDriver与World一一对应,在一个游戏世界里面只存在一个NetDriver。UE里面默认的都是基于UDPSocket进行通信的。
    • Connection
      表示一个网络连接。服务器上,一个客户端到一个服务器的一个连接叫一个ClientConnection。在客户端上,一个服务器到一个客户端的连接叫一个ServerConnection。
    • LocalPlayer
      本地玩家,一个客户端的窗口ViewportClient对应一个LocalPlayer,Localplayer在各个地图切换时不会改变。
    • Channel
      数据通道,每一个通道只负责交换某一个特定类型特定实例的数据信息。ControlChannel:客户端服务器之间发送控制信息,主要是发送接收连接与断开的相关消息。在一个Connection中只会在初始化连接的时候创建一个该通道实例。
      VoiceChannel:用于发送接收语音消息。在一个Connection中只会在初始化连接的时候创建一个该通道实例。
      ActorChannel:处理Actor本身相关信息的同步,包括自身的同步以及子组件,属性的同步,RPC调用等。每个Connection连接里的每个同步的Actor都对应着一个ActorChannel实例。
      常见的只有这3种:枚举里面还有FileChannel等类型,不过没有使用。
    • PlayerController
      玩家控制器,对应一个LocalPlayer,代替本地玩家控制游戏角色。同时对应一个Connection,记录了当前的连接信息,这和RPC以及条件属性复制都是密切相关的。另外,PlayerController记录他本身的ViewTarget(就是他控制额Character),通过与ViewTarget的距离(太远的Actor不会同步)来进行其他Actor的同步处理。
    • World
      游戏世界,任何游戏逻辑都是在World里面处理的,Actor的同步也受World控制,World知道哪些Actor应该同步,保存了网络通信的基础设施NetDriver。
    • Actor
      在世界存在的对象,没有坐标。UE4大部分的同步功能都是围绕Actor来实现的。
    • Dormant
      休眠,对于休眠的Actor不会进行网络同步

    属性同步相关:

    • FObjectReplicator
      属性同步的执行器,每个Actorchannel对应一个FObjectReplicator,每一个FObjectReplicator对应一个对象实例。设置ActorChannel通道的时候会创建出来。
    • FRepState
      针对每个连接同步的历史数据,记录同步前用于比较的Object对象信息,存在于FObjectReplicator里面。
    • FRepLayOut
      同步的属性布局表,记录所有当前类需要同步的属性,每个类或者RPC函数有一个。
    • FRepChangedPropertyTracker
      属性变化轨迹记录,一般在同步Actor前创建,Actor销毁的时候删掉。

    二. 通信的基本流程

    如果我们接触过网络通信,应该了解只要知道对方的IP地址以及端口号,服务器A上进程M_1_Server可以通过套接字向客户端B上的进程M_1_Client发送消息,大致的效果如下:
    这里写图片描述

    • 图2-1 远程进程通信图

    而对于UE4进程内部服务器Server与客户端Client1的通信,与上面的模型基本相似:
    这里写图片描述

    • 图2-2 UE4远程进程通信图

    那这个里面的Channel是什么意思呢?简单理解起来就是一个通信轨道。为了实现规范与通信效率,我们的一个服务器针对某个对象定义了Channel通道,这个通道只与客户端对应的Channel通道互相发送与接收消息。这个过程抽象起来与TCP/UDP套接字的传输过程很像,套接字是在消息发送到进程前就进行处理,来控制客户端进程A只会接收到服务器对应进程A的消息,而这里是在UnrealEditor.exe进程里面处理,让通道1只接收到另一端通道1发送的消息。
    上面的只是针对一个服务器到客户端的传输流程,那么如果是多个客户端呢?
    这里写图片描述

    • 图2-3 Channel通信图

    每一个客户端叫做一个Connection,如图,就是一个server连接到两个客户端的效果。对于每一个客户端,都会建立起一个Connection。在服务器上这个Connection叫做ClientConnection,对于客户端这个Connection叫做ServerConnection。每一个Channel都会归属于一个Connection,这样这个Channel才知道他对应的是哪个客户端上的对象。
    接下来我们继续细化,图中的Channel只标记了1,2,3,那么实际上都有哪些Channel?这些Channel对应的都是什么对象?其实,在第一部分的概念里我已经列举了常见的3中Channel,分别是ControlChannel,ActorChannel,以及VoiceChannel。一般来说,ControlChannel与VoiceChannel在游戏中只存在一个,而ActorChannel则对应每一个需要同步的Actor,所以我们再次细化上面的示意图:
    这里写图片描述

    • 图2-4 Connection下的Channel通信图

    到这里我们基本上就了解了UE4的基本通信架构了,下面我们进一步分析网络传输数据的流程。首先我们要知道,UE4的数据通信是建立在UDP-Socket的基础上的,与其他的通信程序一样,我们需要对Socket的信息进行封装发送以及接收解析。这里面主要涉及到Bunch,RawBunch,Packet等概念,建议参考第一部分的基本概念去理解,很多注释已经加在了流程图里面。如图所示:
    这里写图片描述

    • 图2-5 发送同步信息流程图

    这里写图片描述

    • 图2-6 接收同步信息流程图


    三. 连接的建立

    前面的内容已经提到过,UE的网通通信是基于Channel的,而ControlChannel就是负责
    控制客户端与服务器建立连接的通道,所以客户端与服务器的连接信息都是通过UControlChannel执行NotifyControlMessage函数处理的。下面首先从服务器与客户端的网络模块初始化说起,然后描述二者连接建立的详细流程:


    1.服务器网络模块初始化流程

    从创建GameInstance开始,首先创建NetDriver来驱动网络初始化,进而根据平台创建对应的Socket,之后在World里面监听客户端的消息。
    这里写图片描述

    • 图3-1 服务器网络模块初始化流程图


    2.客户端网络模块初始化流程

    客户端前面的初始化流程与服务器很相似,也是首先构建NetDriver,然后根据平台创建对应的Socket,同时他还会创建一个到服务器的ServerConnection。由于客户端没有World信息,所以要使用一个新的类来检测并处理连接信息,这个类就是UpendingNetGame。
    这里写图片描述

    • 图3-2 客户端网络模块初始化流程图

    3.服务器与客户端建立连接流程

    二者都完成初始化后,客户端就会开始发送一个Hello类型的ControlChannel消息给服务器(上面客户端初始化最后一步)。服务器接收到消息之后开始处理,然后会根据条件再给客户端发送对应的消息,如此来回处理几个回合,完成连接的建立,详细流程参考下图:
    (该流程是本地局域网的连接流程,与在线查找服务器列表并加入有差异)
    这里写图片描述

    • 图3-3 客户端服务器连接建立流程图

    四. Actor的同步细节

    Actor的同步可以说是UE4网络里面最大的一个模块了,里面包括属性同步,RPC调用等,这里为了方便我将他们拆成了3个部分来分别叙述。
    有了前面的描述,我们已经知道NetDiver负责整个网络的驱动,而ActorChannel就是专门用于Actor同步的通信通道。
    这里对Actor同步做一个比较细致的描述:服务器在NetDiver的TickFlush里面,每一帧都会去执行ServerReplicateActors来同步Actor的相关内容。在这里我们需要做以下处理:

    1. 获取到所有连接到服务器的ClientConnections,首先获取引擎每帧可以同步的最大Connection的数量,超过这个限制的忽略。然后对每个Connection几乎都要进行下面所有的操作
    2. 找到要同步的Actor,只有被放到World.NetworkActors里面的Actor才会被考虑,Actor在被Spawn时候就会添加到这个NetworkActors列表里面
    3. 找到客户端玩家控制的角色ViewTarget(ViewTaget与摄像机绑定在一起),这个角色的位置是决定其他Actor是否同步的关键
    4. 验证Actor,对于要销毁的以及所有权Role为ROLE_NONE的Actor不会同步
    5. 是否到达Actor同步时间,Actor的同步是有一定频率的,Actor身上有一个NetUpdateTime,每次同步前都会通过下面这个公式来计算下一次Actor的同步时间,如果没有到达这个时间就会放弃本次同步Actor->NetUpdateTime = World->TimeSeconds + FMath::SRand() * ServerTickTime + 1.f/Actor->NetUpdateFrequency;
    6. 如果这个Actor设置OnlyRelevantToOwner,那么就会放到一个特殊的列表里面OwnedConsiderList然后只同步给属于他的客户端。否则会把Actor放到ConsiderList里面
    7. 对于休眠状态的Actor不会进行同步,对于要进入休眠状态的Actor也要特殊处理关闭同步通道
    8. 查看当前的Actor是否有通道Channel,如果没有,还要看看Actor是否已经加在了场景,没有加载就跳过同步
    9. 接第8个条件——没有Channel的情况下,还会执行Actor::IsNetRelevantFor判断是否网络相关,对于不可见的或者太远的Actor会返回false,不会同步
    10. Actor的同步数量可能非常大,所以有必要对所有的Actor进行一个优先级的排列
      处理完上面的逻辑后会对优先级表里的所有Actor进行排序
    11. 排序后,如果连接没有加载此 actor 所在的关卡,则关闭通道(如果存在)并继续
      每 1 秒钟调用一次 AActor::IsNetRelevantFor,确定 actor 是否与连接相关,如果不相关的时间达到 5 秒钟,则关闭通道
      如果要同步的Actor没有ActorChannel就给其创建一个并绑定Actor,执行同步并更新NetUpdateTime = Actor->GetWorld()->TimeSeconds + 0.2f * FMath::FRand();
      如果此连接出现饱和剩下的 actor会根据连接相关时间判断是否在下一个时钟更新
    12. 执行UActorChannel::ReplicateActor执行真正的Actor同步以及内部数据的同步,这里会将Actor(PackageMap->SerializeNewActor),Actor子对象以及其属性序列化(ReplicateProperties)封装到OutBunch并发送给客户端
      (备注:我们当前版本下面的逻辑都是写在UNetDriver::ServerReplicateActors里面,4.12以后的UE4已经分别把Connection预处理,获取同步Actor列表,优先级处理等逻辑封装到单独的函数里了,详见ServerReplicateActors_BuildConsiderlist, ServerReplicateActors_PrioritizedActors, ServerReplicateActors_ProsessPrioritizedActors等函数
      优先级排序规则是什么?答案是按照是否有controller,距离以及是否在视野。通过FActorPriority构造代码可以定位到APawn::GetNetPriority,这里面会计算出当前Actor对应的优先级,优先级越高同步越靠前,是否有Controller的权重最大)
      总之,大体上Actor同步的逻辑就是在TickFlush里面去执行ServerReplicateActors,然后进行前面说的那些处理。最后对每个Actor执行ActorChannel::ReplicateActor将Actor本身的信息,子对象的信息,属性信息封装到Bunch并进一步封装到发送缓存中,最后通过Socket发送出去。

    下面是服务器的同步Actor的发送Bunch堆栈:(与UE默认的有些不同)
    这里写图片描述

    • 图4-1 服务器同步Actor堆栈图

    下面描述客户端是如何接收到服务器同步过来的Actor的。首先客户端TickDispatch检测服务器的消息,收到消息后通过Connection以及Channel进行解析(第二部分已经讲解),最后一步解析出完整数据的操作在UActorChannel::ProcessBunch执行,在这个函数里面:

    1. 如果发现当前的ActorChannel对应的Actor为NULL,就对当前的Bunch进行反序列化Connection->PackageMap->SerializeNewActor(Bunch, this, NewChannelActor);解析出Actor的内容并执行PostInitializeComponents。如果Actor不为NULL,跳过这一步(参考下面图一堆栈)
    2. 随后根据Bunch信息找到同步过来的属性值并对当前Actor对应的属性进行赋值
    3. 最后执行PostNetInit调用Actor的BeginPlay。(参考下面图二堆栈)

    下面截取了客户端接收到同步Actor并初始化的调用堆栈:
    在这里插入图片描述

    • 图4-2 客户端接收并序列化同步的Actor堆栈图

    • 图4-3 客户端初始化同步过来Actor堆栈图

    从上面的描述来看,基本上我们可以很容易的分析出当前的Actor是否被同步,比如在UActorChannel::ReceivedBunch里面打个断点,看看当前通道里有没有你要的Actor就可以了。
    至于里面更详细的内容,就建议大家去代码里面调试吧。


    五. 属性同步细节

    1.属性同步概述

    属性同步是一个很复杂的模块,我在另一个关于UE4网络的思考文档里面讲解了属性同步相关的使用逻辑以及注意事项。这里我尽可能的分析一下属性同步的实现原理。
    有一点需要先提前说明一下,服务器同步的核心操作就是比较当前的同步属性是否发生变化,如果发生就将这个数据通过到客户端。如果是普通逻辑处理,我们完全可以保存当前对象的一个拷贝对象,然后每帧去比较这个拷贝与真实的对象是否发生变化。不过,由于同步数据量巨大,我们不可能给每个需要同步的对象都创建一个新的拷贝,而且这个逻辑如果暴露到逻辑层的话会使代码异常复杂难懂,所以这个操作要统一在底层处理。那么,UE4的基本思路就是获取当前同步对象的空间大小,然后保存到一个buffer里面,然后根据属性的OffSet给每个需要同步的属性初始化。这样,就保存了一份简单的“拷贝”用于之后的比较。当然,我们能这么做的前提是存在UE的Object对象反射系统。
    下面开始进一步描述属性同步的基本思路:我们给一个Actor类的同步属性A做上标记Replicates(先不考虑其他的宏),然后UClass会将所有需要同步的属性保存到ClassReps列表里面,这样我们就可以通过这个Actor的UClass获取这个Actor上所有需要同步的属性,当这个Actor实例化一个可以同步的对象并开始创建对应的同步通道时,我们就需要准备属性同步了。
    首先,我们要有一个同步属性列表来记录当前这个类有哪些属性需要同步(FRepLayout,每个对象有一个,从UClass里面初始化);其次,我们需要针对每个对象保存一个缓存数据,来及时的与发生改变的Actor属性作比较,从而判断与上一次同步前是否发生变化(FRepState,里面有一个Staticbuff来保存);然后,我们要有一个属性变化跟踪器记录所有发生改变同步属性的序号(可能是因为节省内存开销等原因所以不是保存这个属性),便于发送同步数据时处理(FRepChangedPropertyTracker,对各个Connection可见,被各个Connection的Repstate保存一个共享指针)。最后,我们还需要针对每个连接的每个对象有一个控制前面这些数据的执行者(FObjectReplicator)。
    这四个类就是我们属性同步的关键所在,在同步前我们需要对这些数据做好初始化工作,然后在真正同步的时候去判断与处理。


    2.重要数据的初始化流程

    下面的两个图分别是属性同步的服务器发送堆栈以及客户端的接收堆栈。
    在这里插入图片描述

    • 图5-1服务器发送属性堆栈图

    这里写图片描述

    • 图5-2客户端接收属性堆栈图

    从发送堆栈中我们可以看到属性同步是在执行ReplicatActor的同时进行的,所以我们也可以猜到属性同步的准备工作应该与Actor的同步准备工作是密不可分的。前面Actor同步的讲解中我们已经知道,当Actor同步时如果发现当前的Actor没有对应的通道,就会给其创建一个通道并执行SetChannelActor。这个SetChannelActor所做的工作就是属性同步的关键所在,这个函数里面会对上面四个关键的类构造并做初始化,详细的内容参考下图:

    这里写图片描述

    • 图5-3 SetChannelActor流程解析图

    图中详细的展示了几个关键数据的初始化,不过第一次看可能对这个几个类的关系有点晕,下面给大家简单画了一个类图。
    这里写图片描述

    • 图5-4 属性同步相关类图

    具体来说,每个ActorChannel在创建的时候会创建一个FObjectReplicator用来处理所有属性同步相关的操作,同时会把当前对应通道Actor的同步的属性记录在FRepLayOut的Parents数组里面(Parents记录了每个属性的UProperty,复制条件,在Object里面的偏移等),同时把这个RepLayOut存储到RepState里面,该RepState指针也会被存储到FObjectReplicator里面,RepState会申请一个缓存空间用来存放当前的Object对象(并不是完整对象,只包含同步属性,但是占用空间大小是一样的)。FRepChangedPropertyTracker在创建RepState的同时也被创建,然后通过FRepLayOut的Parents数量来初始化他的记录表的大小,并记录对应的位置是否是条件复制属性,RepState里面保存一个指向他的指针。
    (关于Parents属性与CMD属性:Replayout里面,数组parents示当前类所有的需要同步的属性,而数组cmd会将同步的复杂类型属性【包括数组、结构体、结构体数组但不包括类类型的指针】进一步展开放到这里面。比如ClassA里面有一个StructB属性,这个属性被标记同步,StructB属性会被放到parents里面。由于StructB里面有一个Int类型C属性以及D属性,那么C和D就会被放到Cmd数组里面。有关结构体的属性同步第5部分还有详细描述)


    3.发送同步数据流程分析

    前面我们基本上已经做好了同步属性的基本工作,下面开始执行真正的同步流程。
    

    在这里插入图片描述

    • 图5-5服务器发送属性堆栈图

    再次拿出服务器同步属性的流程,我们可以看到属性同步是通过FObjectReplicator::
    ReplicateProperties函数执行的,进一步执行RepLayout->ReplicateProperties。这里面比较重要的细节就是服务器是如何判断当前属性发生变化的,我们在前面设置通道Actor的时候给FObjectReplicator设置了一个Object指针,这个指针保存的就是当前同步的对象,而在初始化RepState的同时我们还创建了一个Staticbuffer,并且把buffer设置和当前Object的大小相同,对buffer取OffSet把对应的同步属性值添加到buffer里面。所以,我们真正比较的就是这两个对象,一般来说,staticbuffer在创建通道的同时自己就不会改变了,只有当与Object比较发现不同的时候,才会在发送前把属性值置为改变后的。这对于长期同步的Actor没什么问题,但是对于休眠的Actor就会出现问题了,因为每次删除通道并再次同步强制同步的时候这里面的staticbuff都是Object默认的属性值,那比较的时候就可能出现0不同步这样奇怪的现象了。真正比较两个属性是否相同的函数是PropertiesAreIdentical(),他是一个static函数。
    这里写图片描述

    • 图5-6 服务器同步属性流程图

    4.属性回调函数执行

    虽然属性同步是由服务器执行的,但是FObjectReplicator,RepLayOut这些数据可并不是仅仅存在于服务器,客户端也是存在的,客户端也有Channel,也需要执行SetChannelACtor。不过这些数据在客户端上的作用可能就有一些变化,比如Staticbuffer,服务器是用它存储上次同步后的对象,然后与当前的Object比较看是否发生变化。在客户端上,他是用来临时存储当前同步前的对象,然后再把通过过来的属性复制给当前Object,Object再与Staticbuffer对象比较,看看属性是否发生变化,如果发生变化,就在Replicator的RepState里面添加一个函数回调通知RepNotifies。
    在随后的ProcessBunch处理中,会执行RepLayout->CallRepNotifies( RepState, Object );处理所有的函数回调,所以我们也知道了为什么接收到的属性发生变化才会执行函数回调了。
    在这里插入图片描述

    • 图5-7 客户端属性回调堆栈图

    5.关于动态数组与结构体的同步

    结构体:UE里面UStruct类型的结构体与C++的Struct不一样,在反射系统中对应的是UScriptStruct,他本身可以被标记Replicated并且结构体内的数据默认都会被同步,而且如果里面有还子结构体的话也也会递归的进行同步。如果不想同步的话,需要在对应的属性标记NotReplicated,而且这个标记只对UStruct有效,对UClass无效。这一段的逻辑在FRepLayout::InitFromObjectClass处理,ReplayOut首先会读取Class里面所有的同步属性并逐一的放到FRepLayOut的数组Parents里面,这个Parents里面存放的就是当前类的继承树里面所有的同步属性。随后对Parents里面的属性进一步解析(FRepLayout::InitFromProperty_r),如果发现当前同步属性是数组或者是结构体就会对其进行递归展开,将数组的每一个元素/UStruct里面的每一个属性逐个放到FRepLayOut的Cmds数组里面,这个过程中如果遇到标记了NotReplicate的UStruct内部属性,就跳过。所以Cmds里面存放的就是对数组或者结构体进一步展开的详细属性。
    在这里插入图片描述

    • 图5-8 Parents内部成员截图

    在这里插入图片描述

    • 图5-9 Cmds内部成员截图

    Struct结构内的数据是不能标记Replicated的。如果你给Struct里面的属性标记Replicated,UHT在编译的时候就会提醒你编译失败"Struct members cannot be replicated"。这个提示多多少少会让人产生误解,实际上这个只是表明UStruct内部属性不能标记Replicated而已。最后,UE里面的UStruct不可以以成员指针的方式在类中声明。
    数组:数组分为两种,静态数组与动态数组。静态数组的每一个元素都相当于一个单独的属性存放在Class的ClassReps里面,同步的时候也是会逐个添加到RepLayOut的Parents里面,参考上面的图5-8。UE里面的动态数组是TArray,他在网络中是可以正常同步的,在初始化RepLayOut的Cmds数组的时候,就会判断当前的属性类型是否是动态数组(UArrayProperty),并会给其cmd.type做上标记REPCMD_DynamicArray。后面在同步的时候,就会通过这个标记来对其做特殊处理。比如服务器上数组长度发生变化,客户端在接收同步过来的数组时,会执行FRepLayout::ReceiveProperties_DynamicArray_r来处理动态数组。这个函数里面会矫正当前对象同步数组的大小。
    (之前发生过休眠动态数组同步不正常的情况,但是现在无法重现,如果出现同步不正常的情况请告知我一下,以便进一步完善这个文档。)


    六. RPC执行细节

    RepLayOut参照表不止同步的对象有,函数也同样有,RPC的执行同样也是通过属性同步的这个框架。比如我们在代码里面写了一个Client的RPC函数ClientNotifyRespawned,那UHT会给我们生成一个.genenrate.cpp文件,里面会有这个函数的真正的定义如下:

    void APlayerController::ClientNotifyRespawned(class APawn* NewPawn, bool IsFirstSpawn)
    {
    	PlayerController_eventClientNotifyRespawned_Parms Parms;
    	Parms.NewPawn=NewPawn;
    	Parms.IsFirstSpawn=IsFirstSpawn ? true : false;
    	ProcessEvent(FindFunctionChecked(ENGINE_ClientNotifyRespawned),&Parms);
    }
    

    而我们在代码里的函数之所以必须要加上_Implementation,就是因为在调用端里面,实际执行的是.genenrate.cpp文件函数,而不是我们自己写的这个。同时结合下面的RPC执行堆栈,我们可以看到在Uobject这个对象系统里,我们可以通过反射系统查找到函数对应的UFuntion结构,同时利用ProcessEvent函数来处理UFuntion。通过识别UFunction里面的标记,可以知道这个函数是不是一个RPC函数,是否需要发送给其他的端。
    当我们开始调用CallRemoteFunction的时候,RPC相关的初始化就开始了。NetDiver会进行相关的初始化,并试着获取RPC函数的Replayout,那么问题是函数有属性么?正常来说,函数本身就是一个执行过程,函数名是一个起始的执行地址,他本身是没有内存空间,更不用说存储属性了。不过,在UE4的反射系统里面,函数可以被额外的定义为一个UFunction,从而保存自己相关的数据信息。RPC函数的参数就被保存在UFunction的基类Ustruct的属性链表PropertyLink里面,RepLayOut里面的属性信息就是从这里获取到的。
    一旦函数的RepLayOut被创建,也同样会放到NetDiver的RepLayoutMap里面。随后立刻调用FRepLayout::SendPropertiesForRPC将RPC的参数序列化封装与RPC函数一同发送。
    这里写图片描述

    • 图6-1 RPC函数的RepLayOut初始化堆栈图

    简单概括了RPC的发送,这里再说一下RPC的接收。当客户端收到上面的RPC发来的数据后,他需要一步一步的解析。首先,他会执行ReceivePropertiesForRPC来接收解析RPC函数传来的参数并做一些判断确定是否符合执行条件,如果符合就会通过ProcessEvent去处理传递过来的属性信息,找到对应的函数地址(或者说函数指针)等,最后调用该RPC函数。
    这里的ReplayOut里面的Parents负责记录当前Function的属性信息以及属性位置,在网络同步的过程中,客户端与服务器保存一个相同的ReplayOut,客户端才能在反序列化的时候通过OffSet位置信息正确的解析出服务器传来的RPC函数的N个参数。
    这里写图片描述

    • 图6-2 接收RPC函数的传递的参数堆栈图

    这里写

    • 图6-3 客户端执行RPC函数堆栈图

    最后客户端是怎样调用到带_Implementation的函数呢?这里又需要用到反射的机制。我们看到UHT其实会给函数生成一个.genenrate.h文件,这个文件就有下面这样的宏代码,把宏展开的话其实就是一个标准的C++文件,我们通过函数指针最后找到的就是这个宏里面标记的函数,进而执行我们自己定义的_Implementation函数。

    virtual void ClientNotifyRespawned_Implementation(class APawn* NewPawn, bool IsFirstSpawn);\ 
    DECLARE_FUNCTION(execClientNotifyRespawned) \
    { \
    	P_GET_OBJECT(APawn,NewPawn); \
    	P_GET_UBOOL(IsFirstSpawn); \
    	P_FINISH; \
    	this->ClientNotifyRespawned_Implementation(NewPawn,IsFirstSpawn); \
    } \
    


    七. 其他网络特性(待更新)

    1.Reliable与可靠数据传输
    我们知道RPC函数可以通过标记Reliable来保证远程执行,这个Reliable其实就是通过UE的上层机制来实现的。一般来说,如果使用TCP协议,我们在代码层面不需要做任何处理就可以实现可靠数据传输。但是,考虑到TCP协议的三次握手,保持连接,拥塞控制等等机制会影响传输效率,所以UE4仍然使用UDP作为运输层协议,通过上层机制来保证数据的可靠性。
    对于RPC函数:在执行UNetDriver::InternalProcessRemoteFunction会判断当前函数是否标记了Reliable
    if (Function->FunctionFlags & FUNC_NetReliable)
    {
    Bunch.bReliable = 1;
    }
    对于Actor及其属性:在执行UActorChannel::ReplicateActor 时只要Actor的bNetTemporary为false,那么Actor的同步就是可靠的。(bNetTemporary表示Actor只在创建时同步一次)
    Bunch.bReliable = !Actor->bNetTemporary;
    关于UDP实现可靠数据传输的思路与TCP相同,主要是解决丢包与包的顺序不对的问题。进一步来讲,就是发送端给数据包标号,接收端按照标号的顺序接收,如果接收到正常顺序的包就发送ACK应答给发送端,发送端如果没有收到ACK就表示丢包,需要重发。这里面的细节并没有去仔细研究,大家如果有兴趣可以参考下面几个函数。网上有篇博客对这部分有简单的讲解 —— UE4网络模块分析。
    UChannel::PreBunch,UNetConnection::ReceivePacket,UNetConnection::ReceiveNak,UChannel::ReceiveNak,UChannel::ReceivedAcks。

    最后再推荐几个博客
    http://www.jianshu.com/p/b4f1a5412cc9
    http://www.cnblogs.com/ghl_carmack/
    https://www.zhihu.com/people/fjz13/posts

    原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78384199

    展开全文
  • 而第二部分是关于移动组件同步解决方案的描述,里面有诸多细节来让移动的同步表现的更为流畅。关于移动网络同步这一块内容,博主还有一些地方还没有完全梳理清楚,会在之后的时间里慢慢完善。 四.移动同步解决...

    第一部分从移动相关架构以及单机情况下移动的处理细节讲起 UE4移动组件详解(一)——移动框架与实现原理
    而第二部分是关于移动组件同步解决方案的描述,里面有诸多细节来让移动的同步表现的更为流畅。关于移动网络同步这一块内容,博主还有一些地方还没有完全梳理清楚,会在之后的时间里慢慢完善。

    四.移动同步解决方案

    前面关于移动逻辑的细节处理都是在PerformMovement里面实现的,我们可以把函数PerformMovement当成一个完整的移动处理流程。这个流程无论是在客户端还是在服务器都必须要执行,或者作为一个单机游戏,这一个接口基本上可以满足我们的正常移动了。不过,在网络游戏中,为了让所有的玩家体验一个几乎相同的世界,需要保证一个具有绝对权威的服务器,这个服务器可以修正客户端的不正常移动行为,保证各个客户端的一致性。相关同步的操作都是基于UCharacterMovement组件实现的,所以我们的角色必须要使用这个移动组件。

    移动组件的同步全都是基于RPC不可靠传输的,你会在UCharacterMovement头文件里面看到多个以Server或者Client开头的RPC函数。

    关于移动组件的同步思路,建议选阅读一下官方文档的内容,https://docs.unrealengine.com/latest/CHN/Gameplay/Networking/CharacterMovementComponent/index.html 回头看可能更为清晰一点。现在我们把整个移动细节作为一个接口封装起来,宏观的研究移动组件的同步细节。

    另外,如果还没有完全搞清ROLE_Authority,ROLE_AutonomousProxy,ROLE_SimulatedProxy的概念,请参考 UE4网络同步详解(一)——理解同步规则。这里举个例子,一个服务器上有一个玩家ServerA和一个NPC ServerB,客户端上拥有从服务器复制过来的这个玩家ClientA与NPC ClientB。由于ServerA与ServerB都是在服务器上生成的,所以他们两在服务器上的所有权Role都是ROLE_Authority。ClientA在客户端上由于被玩家控制,他的Role是ROLE_AutonomousProxy。ClientB在客户端是完全通过服务器同步来控制的,他的Role就是ROLE_SimulatedProxy。

    4.1 服务器角色正常的移动流程

    第三章节里面的图3-1就是单机或者ListenServer服务器执行的移动流程。作为一个本地控制的角色,他只需要认真的执行正常的移动(PerformMovement)逻辑处理即可,所以ListenServer服务器移动不再赘述。

    但是对于DedicateServer,他的本地没有控制的角色,对移动的处理就有差异了。分为两种情况:

    1. 该角色在客户端是模拟(Simulate)角色,移动完全由服务器同步过去,如各类AI角色。这类移动一般是服务器上行为树主动触发的
    2. 该角色在客户端是拥有自治(Autonomous)权利的Character,如玩家控制的主角。这类移动一般是客户端接收玩家输入数据本地模拟后,再通过RPC发给服务器进行模拟的

    从下面的代码可以了解到这两种情况的处理(注意注释):

    // UCharacterMovementComponent:: TickComponent
    // simulate的角色在服务器执行IsLocallyControlled也会返回true
    // Allow root motion to move characters that have no controller.
    if( CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion()) )
    {
        {
            SCOPE_CYCLE_COUNTER(STAT_CharUpdateAcceleration);
    
            // We need to check the jump state before adjusting input acceleration, to minimize latency
            // and to make sure acceleration respects our potentially new falling state.
            CharacterOwner->CheckJumpInput(DeltaTime);
    
            // apply input to acceleration
            Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));
            AnalogInputModifier = ComputeAnalogInputModifier();
        }
    
        if (CharacterOwner->Role == ROLE_Authority)
        {
            // 单机或者DedicateServer控制simulate角色移动
            PerformMovement(DeltaTime);
        }
        else if (bIsClient)
        {
            ReplicateMoveToServer(DeltaTime, Acceleration);
        }
    }
    else if (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)
    {
        //DedicateServer控制自治客户端角色移动
        // Server ticking for remote client.
        // Between net updates from the client we need to update position if based on another object,
        // otherwise the object will move on intermediate frames and we won't follow it.
        MaybeUpdateBasedMovement(DeltaTime);
        MaybeSaveBaseLocation();
    
        // Smooth on listen server for local view of remote clients. We may receive updates at a rate different than our own tick rate.
        if (CharacterMovementCVars::NetEnableListenServerSmoothing && !bNetworkSmoothingComplete && IsNetMode(NM_ListenServer))
        {
            SmoothClientPosition(DeltaTime);
        }
    }

    这两种情况详细的流程我们在下面两个小结分析。

    4.2 Autonomous角色

    一个客户端的角色是完全通过服务器同步过来的,他身上的移动组件也一样是被同步过来的,所以游戏一开始客户端的角色与服务器的数据是完全相同的。对于Autonomous角色,大致的实现思路如下:

    客户端通过接收玩家的Input输入,开始进行本地的移动模拟流程,移动前首先创建一个移动预测数据结构FNetworkPredictionData_Client_Character,执行PerformMovement移动,随后保存当前的移动数据(速度,旋转,时间戳以及移动结束后的位置等信息)到前面的FNetworkPredictionData里面的SavedMoves列表里面,并通过RPC将当前的Move数据发送该数据到服务器。然后继续进行TickComponent操作,重复这个流程。

    客户端在发送给服务器RPC消息的同时,本地还会不断的执行移动模拟。SavedMoves列表里面的数据也就越来越多。如果这时候收到了一个ClientAckGoodMove调用,那么表示服务器接收了对应时间戳的客户端移动,客户端就将这个时间戳之前的SavedMoves全部移除。如果客户端收到了ClientAdjustPosition调用,那么表示对应这个时间戳的移动有问题,客户端需要修改成服务器传过来的位置,并重新播放那些还没被确认的SaveMoves列表里面的移动。
    这里写图片描述
    图4-1

    整个流程如下图所示:
    这里写图片描述
    图4-2 Autonomous角色移动流程图

    4.2.1 SavedMoves与移动合并

    仔细阅读源码的朋友对上面给出的流程可能并不是很满意,因为除了ServerMove你可能还看到了ServerMoveDual以及ServerMoveOld等函数接口。而且除了SavedMoves列表,还有PendingMove,FreeMove这些移动列表。他们都是做什么的?

    简单来讲,这属于移动带宽优化的一个方式,将没有意义的移动合并,减少消息的发送量。

    当客户端执行完本次移动后,都会把当前的移动数据以一个结构体保存到SavedMove列表,然后会判断当前的这个移动是否可以被延迟发送(CanDelaySendingMove(),默认为true),如果可以就会继续判断当前的客户端网络速度如何。如果当前的速度有一点慢或者上次更新的时间很短,移动组件就会将当前的移动赋值给PendingMove(表示将要执行的移动)并取消本次给服务器消息的发送。

    const bool bCanDelayMove = (CharacterMovementCVars::NetEnableMoveCombining != 0) && CanDelaySendingMove(NewMove);
    
    if (bCanDelayMove && ClientData->PendingMove.IsValid() == false)
    {
        // Decide whether to hold off on move
        // send moves more frequently in small games where server isn't likely to be saturated
        float NetMoveDelta;
        UPlayer* Player = (PC ? PC->Player : nullptr);
        AGameStateBase const* const GameState = GetWorld()->GetGameState();
    
        if (Player && (Player->CurrentNetSpeed > 10000) && (GameState != nullptr) && (GameState->PlayerArray.Num() <= 10))
        {
            NetMoveDelta = 0.011f;
        }
        else if (Player && CharacterOwner->GetWorldSettings()->GameNetworkManagerClass) 
        {
            //这里会根据网络管理的配置以及客户端网络速度来决定是否延迟发送
            NetMoveDelta = FMath::Max(0.0222f,2 * GetDefault<AGameNetworkManager>(CharacterOwner->GetWorldSettings()->GameNetworkManagerClass)->MoveRepSize/Player->CurrentNetSpeed);
        }
        else
        {
            NetMoveDelta = 0.011f;
        }
    
        if ((GetWorld()->TimeSeconds - ClientData->ClientUpdateTime) * CharacterOwner->GetWorldSettings()->GetEffectiveTimeDilation() < NetMoveDelta)
        {
            // Delay sending this move.
            ClientData->PendingMove = NewMove;
            return;
        }
    }

    当客户端进去下次Tick的时候,就会判断当前的新的移动是否能与上次保存的PendingMove合并。如果可以,就可以减少一次消息的发送。如果不能合并,那么在本次移动结束后给服务器发送一个两次移动(ServerMoveDual),就是单纯的执行两次ServerMove。

    服务器在受到两次移动的时候对第一次移动不进行任何校验,只对第二个移动进行正常的校验,判断是否是第一次的标准就是ClientPosition是不是FVector(1.f,2.f,3.f)。通过下面的代码就可以了解了

    void UCharacterMovementComponent::ServerMoveDual_Implementation(
        float TimeStamp0,
        FVector_NetQuantize10 InAccel0,
        uint8 PendingFlags,
        uint32 View0,
        float TimeStamp,
        FVector_NetQuantize10 InAccel,
        FVector_NetQuantize100 ClientLoc,
        uint8 NewFlags,
        uint8 ClientRoll,
        uint32 View,
        UPrimitiveComponent* ClientMovementBase,
        FName ClientBaseBone,
        uint8 ClientMovementMode)
    {
        ServerMove_Implementation(TimeStamp0, InAccel0, FVector(1.f,2.f,3.f), PendingFlags, ClientRoll, View0, ClientMovementBase, ClientBaseBone, ClientMovementMode);
        ServerMove_Implementation(TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBone, ClientMovementMode);
    }

    其实,UE的思想就是,将所有的移动的关键信息都数据化,这样移动就可以自由的存储和回放。为了节省带宽,提高效率,我们也就可以想出各种办法来减少发送不必要的消息,对于一个没有移动过的玩家,理论上我们甚至都可以不去同步他的移动信息。
    这里写图片描述
    图4-3 移动预测及保存的数据结构示意图

    4.3 Simulate角色

    首先看一下官方文档对Simulate角色移动的描述:

    对于那些不由人类控制的人物,其动作往往会通过正常的 PerformMovement() 代码在服务器(此时充当了主控者)上进行更新。Actor 的状态,如方位、旋转、速率和其他一些选定的人物特有状态(如跳跃)都会通过正常的复制机制复制到其他机器,因此,它们不必在每一帧都经由网络传送。为了在远程客户端上针对这些人物提供更流畅的视觉呈现,该客户端机器将在每一帧为模拟代理执行一次模拟更新,直到新的数据(由服务器主控)到来。本地客户端查看其他远程人类玩家时也是如此;远程玩家将其更新发送给服务器,后者为该玩家执行一次完整的动作更新,然后定期复制数据给所有其他玩家。
    这个更新的作用是根据复制的状态来模拟预期的动作结果,以便在下一次更新前“填补空缺”。所以,客户端并没有在新的位置放置由服务器发送的代理,然后将它们保留到下次更新到来(可能是几个后续帧),而是通过应用速率和移动规则,在每一帧模拟出一次更新。在另一次更新到来时,客户端将重置本地模拟并开始新一次模拟。

    简单来说,Simulate角色的在服务器上的移动就是正常的PerformMovement流程。而在客户端上,该角色的移动分成两个步骤来处理——收到服务器的同步数据时就直接进行设置。在没有收到服务器消息的时候根据上一次服务器传过来的数据(包括速度与旋转等)在本地执行Simulate模拟,等着下一个同步数据到来。Simulate角色采用这样的机制,本质上是为了减小同步带来的开销。下面代码展示了所有Character的同步属性

        void ACharacter::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
        {
            Super::GetLifetimeReplicatedProps( OutLifetimeProps );
            DOREPLIFETIME_CONDITION( ACharacter, RepRootMotion,COND_SimulatedOnlyNoReplay);
            DOREPLIFETIME_CONDITION( ACharacter, ReplicatedBasedMovement,   COND_SimulatedOnly );
            DOREPLIFETIME_CONDITION( ACharacter, ReplicatedServerLastTransformUpdateTimeStamp, COND_SimulatedOnlyNoReplay);
    
            DOREPLIFETIME_CONDITION( ACharacter, ReplicatedMovementMode,    COND_SimulatedOnly );
            DOREPLIFETIME_CONDITION( ACharacter, bIsCrouched,           COND_SimulatedOnly );
    
            // Change the condition of the replicated movement property to not replicate in replays since we handle this specifically via saving this out in external replay data
            DOREPLIFETIME_CHANGE_CONDITION(AActor,ReplicatedMovement,COND_SimulatedOrPhysicsNoReplay);
    
    
        }

    ReplicatedMovement记录了当前Character的位置旋转,速度等重要的移动数据,这个成员(包括其他属性)在Simulate或者开启物理模拟的客户端才执行(可以先忽略NoReplay,这个和回放功能有关)。同时,我们可以看到Character大部分的同步属性都是与移动同步有关,而且基本都是SimulatedOnly,这表示这些属性只在模拟客户端才会进行同步。除了ReplicatedMovement属性以外,ReplicatedMovementMode同步了当前的移动模式,ReplicatedBasedMovement同步了角色所站在的Component的相关数据,ReplicatedServerLastTransformUpdateTimeStamp同步了最新的服务器移动更新帧,也就相当于最后一次服务器更新移动的时间(在ACharacter::PreReplication里会将服务器当前的移动数据赋值给ReplicatedServerLastTransformUpdateTimeStamp然后进行同步)。

    了解了这些同步的数据后,我们开始分析其移动流程。流程如下图所示(RootMotion的情况我在上一章节已经描述,这里不再赘述)。其实其基本思路与普通的移动处理相似,只不过是调用SimulateTick去根据当前的速度等条件模拟客户端移动,但是有一点非常重要的差异就是Simulate的角色的胶囊体移动与Mesh移动是分开进行的。这么做的原因是什么呢?我们稍后再解释。
    这里写图片描述
    图4-4 Simulate角色移动流程图

    客户端的模拟我们大致了解了流程,那么接收服务器数据并修正是在哪里处理的呢?答案是AActor::OnRep_ReplicatedMovement。客户端在接收到服务器同步的ReplicatedMovement时,会产生回调函数触发SmoothCorrection的执行,从当前客户端的位置平滑的过度到服务器同步的位置。

    前面提到了胶囊体与Mesh的移动是分开处理的,其目的就是提高代理模拟的流畅度。其实在官方文档上有简单的例子,

    比如这种情况,一个 replicated 的状态显示当前的角色在时间为 t=0 的时刻以速度 (100, 0, 0) 移动,那么当时间更新到 t=1 的时候,这个模拟的代理将会在 X 方向移动 100 个单位,然后如果这时候服务端的角色在发送了那个 (100, 0, 0) 的 replcated 信息后立刻不动了,那么这个 replcated 信息则会使到服务端角色的位置和客户端的模拟位置处于不同的点上。

    为了避免这种“突变”情况,UE采用了Mesh网格的平滑操作。胶囊体的移动正常进行,但是其对应的Mesh网格不随胶囊体移动,而要通过SmoothClientPosition处理,在SmoothNetUpdateTime时间内完成移动,这样玩家在视觉上就不会觉得代理角色的位置突变。通过FScopedPreventAttachedComponentMove类可以限制某个组件暂时不跟随父类组件移动。

    对于Smooth平滑,UE定义了下面几种情况,默认我们采用Exponential(指数增长,越远移动越快):

        /** Smoothing approach used by network interpolation for Characters. */
        UENUM(BlueprintType)
    
         enum class ENetworkSmoothingMode : uint8
         {
           /** No smoothing, only change position as network position updates are received. */
           Disabled     UMETA(DisplayName="Disabled"),
    
           /** Linear interpolation from source to target. */
           Linear           UMETA(DisplayName="Linear"),
    
           /** Exponential. Faster as you are further from target. */
           Exponential      UMETA(DisplayName="Exponential"),
    
           /** Special linear interpolation designed specifically for replays. Not intended as a selectable mode in-editor. */
           Replay           UMETA(Hidden, DisplayName="Replay"),
         };
    

    4.4 关于物理托管后的移动

    一般情况下我们是通过移动组件来控制角色的移动,不过如果给玩家角色的胶囊体(一般Mesh也是)勾选了SimulatePhysics,那么角色就会进入物理托管而不受移动组件影响,组件的同步自然也是无效了,常见的应用就是玩家结合布娃娃系统,角色死亡后表现比较自然的摔倒效果。相关代码如下:

    // // UCharacterMovementComponent::TickComponent
    // We don't update if simulating physics (eg ragdolls).
    if (bIsSimulatingPhysics)
    {
        // Update camera to ensure client gets updates even when physics move him far away from point where simulation started
        if (CharacterOwner->Role == ROLE_AutonomousProxy && IsNetMode(NM_Client))
        {
            APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController());
            APlayerCameraManager* PlayerCameraManager = (PC ? PC->PlayerCameraManager : NULL);
            if (PlayerCameraManager != NULL && PlayerCameraManager->bUseClientSideCameraUpdates)
            {
                PlayerCameraManager->bShouldSendClientSideCameraUpdate = true;
            }
        }
        return;
    }

    对于开启物理的Character,Simulate的客户端也是采取移动数据靠服务器同步的机制,只不过移动的数据不是服务器PerformMovement算出来的,而是从根组件的物理对象BodyInstance获取的,代码如下。

    void AActor::GatherCurrentMovement()
    {
        AttachmentReplication.AttachParent = nullptr;
    
        UPrimitiveComponent* RootPrimComp = Cast<UPrimitiveComponent>(GetRootComponent());
        if (RootPrimComp && RootPrimComp->IsSimulatingPhysics())
        {
            FRigidBodyState RBState;
            RootPrimComp->GetRigidBodyState(RBState);
    
            ReplicatedMovement.FillFrom(RBState, this);
            ReplicatedMovement.bRepPhysics = true;
        }
    }

    原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78669947

    展开全文
  • 战斗同步机制

    千次阅读 2016-08-03 10:30:44
    dota 类游戏是如何解决网络延迟同步的? ...英雄联盟中,人物在很短的时间做的快速操作能很好的同步到其他客户端上显示出来,请问这是如何做到的呢?他们用了怎么样的方法实现的. 3 条评论
  • Unreal网络架构

    千次阅读 2017-03-11 16:44:19
    最初的多玩家游戏是双玩家的调制解调器游戏,以DOOM为代表,而现在多玩家游戏已经进化成为大型的、持久的、交互形式更加自由的游戏,如Quake2,Unreal和Ultima Online,共享现实背后的技术已经有了巨大的进步。...
  • 网络游戏同步问题

    千次阅读 2016-06-08 10:16:17
    介绍 作为一个程序,你想过网络多人对战游戏是怎么做出来的吗? 从外行的角度来看多人对战游戏是很神奇的:2个或者更多的玩家在同一个时间经历了...不同玩家间或多或少会存在不同步,程序员就是让这些不同步在玩家
  • UE4中的反射机制

    千次阅读 多人点赞 2016-12-27 20:22:49
    原文地址:Unreal Property System (Reflection) Reflection is the ability of a program to examine itself at runtime. This is hugely useful and is a foundational technology of the Unreal engine, ...
  • 同步(LockStep)该如何反外挂

    千次阅读 2018-03-29 14:41:38
    原文地址:帧同步(LockStep)该如何反外挂在中国的游戏环境下,反挂已经成为了游戏开发的重中之重,甚至能决定一款游戏的生死,吃鸡就是一个典型的案例。目前参与了了一款动作射击的MOBA类游戏的开发,同步方案上...
  • Unreal 使用 PySide 开发界面 | 利用 FBX Python SDK 操作 FBX 文件 前言   前段时间协助动画组制作将动画 FBX 文件导入 Unreal 引擎里面,遇到很麻烦的问题。  由于一些流程规范的问题...
  • AkAmbientSound类的实现 Unreal Engine提供了一个基本对象的构造器ObjectInitializer,一般来说用户创建的类总是拥有很多变量,因此 AkAmbientSound 首先覆写了 ObjectInitializer ,为该类的若干变量赋初始值,...
  • Unreal引擎术语表

    千次阅读 2015-11-21 08:39:08
    Unreal引擎术语表 转载自UDN: ‍Actor - 一个可以放置在世界中或者在世界中产生的对象。这包括类似于Players(玩家)、Weapons(武器)、 Trash.StaticMeshes(静态网格物体)、Emitters(编辑器)、 Infos以及 ...
  • 游戏开发入门(十一)游戏引擎架构

    万次阅读 多人点赞 2018-03-03 11:22:29
    链接:游戏开发入门(十一)游戏引擎架构(8节课 时常:约2小时40分钟) 该堂课是对游戏引擎内容的一个概括总结,同时也是对游戏开发技术的一个相当全面的总结。 正如我在开篇所提到的,游戏引擎架构的学习有助于...
  • 这是一篇超过万字读书笔记,总结了《游戏编程模式》一书中所有章节与内容的知识梗概。 我们知道,游戏行业其实一直很缺一本系统介绍游戏编程进阶技巧的书籍,而《游戏编程模式》得出现,正好弥补了这一点。在这篇...
  • 虚幻引擎游戏技能系统文档

    千次阅读 2020-06-09 19:01:22
    虚幻引擎游戏能力系统文档 通过一个简单的多人示例项目分享我对UE4中GAS插件的理解。 由于这不是官方文档,示例项目和我都不是来自Epic Games。因此我并不能保证描述的准备性。(译注:本人才疏学浅,还请大家多多...
  • UE4异步操作总结【转载】

    千次阅读 2018-06-05 16:40:25
    虚幻本身有提供一些对异步操作的封装,这里是对这段时间接触到的“非同步”的操作进行的总结。 当前使用的UE4版本为4.18.2。 在虚幻的游戏制作中,如果不是特殊情况一般不会有用到线程的时候。但是由于实际上虚幻...
1 2 3 4 5 ... 20
收藏数 532
精华内容 212
关键字:

unreal游戏内同步机制