深入浅出UE4网络

您所在的位置:网站首页 owned和owning的区别 深入浅出UE4网络

深入浅出UE4网络

2023-09-20 20:03| 来源: 网络整理| 查看: 265

UE4中的官方文档结构比较混乱,且有部分错误,不方便学习。笔者试图通过本文,整理出一篇关于UE4网络的文章,方便朋友们对UE4中的网络同步部分的认识,并有进一步理解。如有讲得不清楚明白的地方,还望批评指正。如需转载请注明出处,http://www.cnblogs.com/Leonhard-/p/6511821.html,这是对作者最起码的尊重,谢谢大家。

本文的结构如下:

一、UE4网络综述 二、UE4中的几种同步方式 1.Actor Replication 2.Property Replication 3.Function Call Replication 4.Actor Component Replication 5.Generic Subobject Replication 三、UE4中网络高级用法 1.复制对象引用 2.Role的深层次解读 3.对象归属性 四、UE4中的网络实例分析--Character Movement 五、Further More 1.Detailed Actor Replication 2.网络性能优化 一、UE4中的网络综述

  UE4中的网络模型和一般网络游戏的一样,是C/S模型。但是有些不同的是,UE4最初是用来做FPS的引擎,考虑到FPS的游戏性质,在网络的设计部分,考虑了两种服务器,一种是方便进行局域网本地游戏,在本地机器上搭建服务器,此时本地机器既是服务器又是客户端,即Listen Server。而另外一种则是更为专业的独立服务器,在独立服务器上则不执行渲染任务,只承担服务器的相关职责,即Dedicated Server,默认情况下独立服务器连界面都没有(除非在启动参数中加入-log)。

 

  而说起服务器和客户端,服务器对游戏状态拥有主控权力,机器之间出现数据的差异,都以服务器的为准。而此时的客户端只有服务器的近似值,不难理解,越好的网络环境和网络模型,客户端的游戏状态会接近服务器。但需要注意的是,客户端之间是没有直接连接的,必须通过服务器来进行客户端之间的交互,换句话说,如果没有服务器告知,客户端之间是不知道互相之间的存在的。而且有一个原则,游戏信息只准从服务器向客户端同步,客户端不能向服务器同步,就算客户端发信息给服务器,服务器也当成垃圾丢掉,客户端向服务器发信息的方式只有调用RPC中的Server函数一种形式,这个后面具体会讲。

  如果对服务器和客户端建立连接的过程感兴趣的话,可以参看这幅图(官网上的建立连接过程在顺序上有问题,以这幅图为准)

二、UE4中的几种同步方式 1.Actor Replication

  Actor是UE4中场景中可以显示的核心,在多人网络环境下,Actor也是网络传输的核心元素,甚至可以说Actor是网络同步中的基本单位,后面即将讲到的RPC和属性复制都是在Actor复制中进行的。当Actor的状态发生变换时,引擎会在预设的时间范围内(可以设置同步间隔时间)对该Actor进行网络同步,使得该Actor在应该存在的电脑上(条件复制)得到与服务器的版本。但是默认情况下,UE4不知道是否该对一个Actor执行复制操作,我们需要将Actor::bReplicates变量设置为true。既然Actor是网络同步的基本单位,如果这个变量的Actor::bReplicates为false,那么Actor下的Property Replication,RPC等等,都是白搭。Actor::bReplicates有两种设置方式,一种是在蓝图里,一种是在C++里。

在C++中:

 

或者,在蓝图中:

 

  OK。那么这个时候新问题来了,如果这个Actor服务器只想让Actor的所有者得到同步信息,或者说,不想让整个地图的人都知道Actor的信息,只想让距离Actor的一定范围的人得到同步,又或者说,只想让某些付费玩家得到同步信息,怎么办?这时有一个叫“network relevant”的概念,也就是说,只有当一个Actor对于服务器是net relevant的时候,服务器才会同步信息给他。比如说:只想让Actor的所有者得到信息,我们可以设置Actor::bOnlyRelevantToOwner变量为true。

 

  比如:不想让整个地图的人都知道Actor的信息,只想让距离Actor的一定范围的人得到同步,我们可以设置Actor::NetCullDistanceSquared。当然,这个又会引出新的问题,具体可参看视频,本文为了保持文章的条理性,不进一步讨论这个问题。

  再比如:如果只想让收费玩家得到同步信息,那么此时引擎里没有预设变量来完成这个功能,我们可以Override在Actor::IsNetRelevantFor函数。

2.Property Replication

  当我们将Actor设置为bReplicates = true后,在进行同步时,并不是将所有的属性进行同步,只有将属性标记为需要复制后,同步时才会同步属性。同步属性有两种方式,第一种是当服务器发生变化时,同步到对应客户端,成为Replicated。第二种是当属性发生变化时,同步属性并调用OnRep_函数()。第一种同步方式的例子。蓝图和C++中都可以使用这种方法,而且效果并无差异。

第一种同步方式的C++声明方式如下:

 

第一种同步方式的蓝图声明方式如下:

 

  第二种同步的方式需要特别说明一下。虽然C++和蓝图都可以使用这种方法,但是如果用C++声明此种同步属性,当属性在服务器发生变化时,对应的客户端自动调用OnRep函数,在服务器端,需要手动调用OnRep函数,称作ReplicatedUsing。而在蓝图里声明此种属性时,当属性在服务器发生变化时,服务器和客户端都会自动调用OnRep函数,不需要单独在服务器手动调用,称作RepNotify。

第二种同步方式的C++声明、定义方式如下:

 

 

  蓝图中的除了将Replication中选择RepNotify外,还需要定义如下的函数:

  在C++中实现同步属性,无论是第一种同步方式还是第二种同步方式还需要为属性设置Lifetime。设置的方式就是在类里Override Actor::GetLifetimeReplicatedProps函数。这个函数会在第一个类实例被创建时调用,这里需要注意的是,当有多个实例被创建时,GetLifetimeReplicatedProps函数并不会多次执行。这也就是说,Lifetime的设置是基于类本身还不是基于类实例的,如果属性的同步由某一个状态值来定的话,那么所有的实例都会用第一个实例的状态来定,而不是根据自己实例的状态来定。

 

  那么这个Lifetime有什么用呢。可以用来使用条件复制(Conditional Replication)就可以实现属性只同步给部分客户端的功能,具体的几种类型可以查看链接。那么有的朋友可能会问,那全部属性设成Replicated或者ReplicatedUsing不就行了。那么这样做虽然也可以,但是会有两个问题,一个是实际中的网络流量有限,必须节省带宽。另一个就是为了防止玩家作弊,不该让玩家知道的信息绝对不能让玩家知道。还有个问题,如果Actor的Net Relevant的作用范围小于属性的条件复制的作用范围怎么办,也就是说,如果Actor设置只同步给Owner,还属性设置同步给全部人会怎么样,前面说了,Actor是网络传输的最小单位,属性同步是放在Actor同步包里的,所以,以Actor的限制为准,也就是说这种情况下该属性只能传递给Owner。

 

  那么根据上面说法,条件复制属于“静态”生存期,不能跟随程序进行实时的调整,那么如果我们想根据程序的实时状态来动态调整属性的生存期,是可以的吗?答案是可以的,可以重写Actor::PreReplication函数使用DOREPLIFETIME_ACTIVE_OVERRIDE来实现。需要注意的是,这个限制它是基于Actor的,还不是基于连接的。

3.Function Call Replication

  Function Call Replication也叫作Remote Procedure Calls(RPCs),在蓝图中也称为Event Replication,是一种利用网络手段,将函数调用和执行分开的方式。在正式开始讨论RPC之前,我们先得来认识一下主控(Authority)。我们前面说到“服务器对游戏状态拥有主控权力,机器之间出现数据的差异,都以服务器的为准。”,所以可以说服务器对游戏具有Authority,任何与游戏规则,游戏状态有关的变量以及参与复制的对象(replicated object),都以服务器的为准,客户端的只是一个复制品。但是不是服务器就对所有的Actor拥有Authority呢?答案是:不是。比如说:只出现在客户端的UI,或者说一些本地产生的特效效果。此时的客户端就是这些Actor的Authority,要看谁是这个Actor的Authority,就看服务器是否有这个对象,如果没有,客户端就对这个Actor拥有Authority。但是一般情况下,我们可以把拥有Authority的对象看作是服务器,相反如果没有的话就看作客户端。

  以下是蓝图中区分客户端和服务器的方法。

 

或者在C++中。

1 void AShooterCharacter::FlyDown() 2 { 3 if (this->Role IsMovingOnGround() && !GetCharacterMovement()->IsFalling()) 8 { 9 GetCharacterMovement()->SetMovementMode(MOVE_Falling); 10 } 11 }

  我们看到C++代码中,使用一个Actor::Role与一个枚举值进行比较。我们进一步看一下枚举值还有什么。

UENUM() enum ENetRole { /** No role at all. */ ROLE_None, /** Locally simulated proxy of this actor. */ ROLE_SimulatedProxy, /** Locally autonomous proxy of this actor. */ ROLE_AutonomousProxy, /** Authoritative control over the actor. */ ROLE_Authority, ROLE_MAX, };

  其中的ROLE_NONE表示这个对象不扮演网络角色,不参与同步。ROLE_SimulatedProxy表示它是一个远程机器上的一个复制品,它没有权利来改变一个对象的状态,也不能调用RPC(现在先把RPC理解为下命令让别人去执行)。ROLE_AutonomousProxy表示它既可以完成ROLE_SimulatedProxy的工作(做一个复制品),又可以通过RPC来修改真正Actor的状态。简单来说,两种角色都可以同步服务器角色的信息,并在自己的电脑显示。但是ROLE_AutonomousProxy则可以通过调用RPC,让服务器来执行对应的命令。(这也是前面说的,客户端向服务器发送信息的唯一一种方式,这是RPC中的其中一种,称作Server函数,具体细节后面会介绍。)

  正如前面说说,RPC是一种利用网络手段,将函数调用和执行分开的方式,一共有三种RPCs,分别为Server函数,Client函数,Multicast函数。Server函数是客户端调用,服务器执行。什么意思呢?在网络环境下,一般情况下,所有客户端和服务器都同时拥有一个Actor的实例,服务器的网络角色为ROLE_Authority,而客户端有两种,一种为ROLE_SimulatedProxy,这种角色只能用来接收服务器给它同步的信息,而不能向服务器发送信息。而ROLE_AutonomousProxy角色,不仅可以用来接收服务器给它同步的信息,还可以利用调用Server函数,让服务器自行执行一段预设的代码,这个过程就是Server函数。相反,Client函数就是ROLE_Authority的角色调用,ROLE_AutonomousProxy的角色执行。Multicast函数则是服务器调用,服务器和所有客户端执行。一些特殊情况可以查看下表。下表描述的所有权中:Client-owned actor指的是服务器和客户端的Actor都同属于同一个UNetConnection家族的Actor,而Server-owned actor指的是只存在于Server端的Actor,Unowned actor则指其余Actor(这个说起来比较绕口,但是看完如下的代码可能会好一些)。具体的Client-owned actor可以参考如下的代码。

1 void UActorChannel::ProcessBunch( FInBunch & Bunch ) 2 { 3 //...... 4 // Owned by connection's player? 5 UNetConnection* ActorConnection = Actor->GetNetConnection(); 6 if (ActorConnection == Connection || (ActorConnection != NULL && ActorConnection->IsA(UChildConnection::StaticClass()) && ((UChildConnection*)ActorConnection)->Parent == Connection)) 7 { 8 RepFlags.bNetOwner = true; 9 } 10 //...... 11 }

 

  从服务器调用RPC:

  Actor 所有权未复制NetMulticastServerClient Client-owned actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在 actor 的所属客户端上运行 Server-owned actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在服务器上运行 Unowned actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在服务器上运行

  从客户端调用RPC:

  Actor 所有权未复制NetMulticastServerClient Owned by invoking client 在执行调用的客户端上运行 在执行调用的客户端上运行 在服务器上运行 在执行调用的客户端上运行 Owned by a different client 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行 Server-owned actor 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行 Unowned actor 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行  

  在蓝图中使用RPC时,只需要对函数进行如下设置即可。

  在C++中要将一个函数声明为 RPC,您只需将 Server、Client 或 NetMulticast 关键字添加到 UFUNCTION 声明。

  例如,若要将某个函数声明为一个要在服务器上调用、但需要在客户端上执行的 RPC,您可以这样做:

1 UFUNCTION( Client ); 2 void ClientRPCFunction();

  如果要将某个函数声明为一个要在客户端上调用、但需要在服务器上执行的 RPC,您可以采取类似的方法,但需要使用 Server 关键字:

1 UFUNCTION(reliable, server, WithValidation) 2 void ServerFlyUp();

  此外,还有一种叫做多播(Multicast)的特殊类型的 RPC 函数。多播 RPC 可以从服务器调用,然后在服务器和当前连接的所有客户端上执行。 要声明一个多播函数,您只需使用 NetMulticast 关键字:

1 UFUNCTION( NetMulticast,unreliable ); 2 void MulticastRPCFunction();

  接下来定义我们的RPC函数。此时需要注意的是,RPC函数的定义需要在函数末尾添加_Impementation,这是跟引擎的具体调用有关,这里不深入探讨,有兴趣的朋友可以参考.generate.h文件。我们直接看例子:

1 void AShooterCharacter::ServerFlyUp_Implementation() 2 { 3 FlyUp(); 4 } 1 void AShooterCharacter::FlyUp() 2 { 3 if (this->Role UpdatedComponent = CapsuleComponent; 9 CharacterMovement->GetNavAgentProperties()->bCanJump = true; 10 CharacterMovement->GetNavAgentProperties()->bCanWalk = true; 11 CharacterMovement->SetJumpAllowed(true); 12 CharacterMovement->SetNetAddressable(); // Make DSO components net addressable 13 CharacterMovement->SetIsReplicated(true); // Enable replication by default 14 15 } 16 }

  或者是在蓝图中设置:要进行静态蓝图组件复制,只需在组件默认设置中切换 Replicates 布尔变量。同样,只有当组件中拥有需要复制的属性或事件时,才需要 进行此操作。静态组件需要在客户端和服务器上隐式创建。

components_checkbox.png

需要注意的是,并非所有组件都会如此显示,必须要支持某种复制形式才会显示。要通过动态生成的组件来实现这一点,可以调用 SetIsReplicated 函数:

components_function.png

5.Generic Subobject Replication

  其实,UE4还可以复制任意的UObject对象,UObject对象还可以嵌套其他的UObject对象,而且UObject还可以设置RPC和同步属性,功能大致和Actor一致。但是需要注意的是,UObject必须最终被包含在Actor中。只有Actor进行同步时,SubObject才能被同步。首先应该Override UObject::IsSupportedForNetorking。

1 //ReplicatedSubobject.h 2 UCLASS() 3 class UReplicatedSubobject : public UObject 4 { 5 GENERATED_UCLASS_BODY() 6 7 public: 8 9 UPROPERTY(Replicated) 10 uint32 bReplicatedFlag:1; 11 12 virtual bool IsSupportedForNetworking() const override 13 { 14 return true; 15 } 16 };

然后,再Override UObject::UReplicatedSubobject。

1 //ReplicatedSubobject.cpp 2 UReplicatedSubobject::UReplicatedSubobject(const class FPostConstructInitializeProperties& PCIP) 3 : Super(PCIP) 4 { 5 } 6 7 void UReplicatedSubobject::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const 8 { 9 DOREPLIFETIME(UReplicatedSubobject, bReplicatedFlag); 10 }

最后,在同步的Actor里,我们需要实现AActor::ReplicateSubobjects() 

1 UCLASS() 2 class AReplicatedActor : public AActor 3 { 4 GENERATED_UCLASS_BODY() 5 6 public: 7 8 virtual void PostInitializeComponents() override; 9 virtual bool ReplicateSubobjects(class UActorChannel *Channel, class FOutBunch *Bunch, FReplicationFlags *RepFlags) override; 10 11 /** A Replicated Subobject */ 12 UPROPERTY(Replicated) 13 UReplicatedSubobject* Subobject; 14 15 private: 16 }; 1 #include "ReplicatedActor.h" 2 #include "UnrealNetwork.h" 3 #include "Engine/ActorChannel.h" 4 AReplicatedActor::AReplicatedActor(const class FPostConstructInitializeProperties& PCIP) 5 : Super(PCIP) 6 { 7 bReplicates = true; 8 } 9 10 void AReplicatedActor::PostInitializeComponents() 11 { 12 Super::PostInitializeComponents() 13 14 if (HasAuthority()) 15 { 16 Subobject = NewObject(this); 17 } 18 } 19 20 bool AReplicatedActor::ReplicateSubobjects(class UActorChannel *Channel, class FOutBunch *Bunch, FReplicationFlags *RepFlags) 21 { 22 bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags); 23 24 if (Subobject != nullptr) 25 { 26 WroteSomething |= Channel->ReplicateSubobject(Subobject, *Bunch, *RepFlags); 27 } 28 29 return WroteSomething; 30 } 31 32 void AReplicatedActor::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const 33 { 34 DOREPLIFETIME(AReplicatedActor, Subobject); 35 } 三、UE4中网络高级用法 1.复制对象引用

  在UE4中,C++的指针同步是一个不得不处理的对象。如果服务器只是单纯把对象的地址作为信息传给客户端的话,那么客户端会引用无效内存而崩溃。在UE4中,使用FNetworkGUID来作为对象在服务器上的唯一ID。有两种情况服务器会为UObject对象分配GUID,第一种是,第二章里谈及到的所有类型都可以被服务器分配UObject,第二种是具有Stably Named 的UObjects。什么是Stably Named Objects?就是客户端和服务器上名字完全相同的对象(即使这个对象不会被复制,也拥有GUID)。那么什么情况下会存在Stably Named Objects呢?1.从包里直接load出来的对象,比如关卡里放的静态对象。2.通过construction scripts添加的对象。3.通过UActorComponent::SetNetAddressable手动标记的对象。

2.Role的深层次解读

  前面说过,有三种网络角色,分别是ROLE_Autonomous、ROLE_Simulated和ROLE_Authority。其实在UE4的底层代码中,这三个角色不存在实权,三种网络角色其实跟调用RPC函数之间是没有直接关系的。客户端调用RPC Server函数时,服务器端仅仅判断这个客户端Pawn的GetNetConnection是否与这个服务器端的Pawn同属相同的UNetConnection家族,如果相同,则允许Pawn调用RPC Server函数,UE4根本不会判断这个Pawn是什么网络角色。可以做个有趣的实验,我们可以将Actor的Role设为Simulated,发现它还是可以调用RPC,反之如果把Simuate置为Autonomous,是不能调用RPC的。那Role还有什么意义呢?Role是UE4中帮助我们理解和认识UE4网络框架的一个概念,可以反映UE4的网络架构。在一般情况下,我们就可以认为具有Autonomous角色的Pawn就可以调用RPC Server函数。如果只是作为了解,那么这一节就不用再往下读了,如果你是一个有强迫症的人,想要弄清楚他们之间的关系。OK。接下来的图,可以帮助你了解PlayerController、UNetConnection、Role和RPC之间的关系。

 

  此时,我们知道了,Role只是一个“巧合”,它与是否调用RPC没有直接关系。

3.对象归属性

  官网抛出了Client-owned actor和Server-Owned actor两个概念。根据上下文,Client-owned actor指的是服务器和客户端的Actor都同属于同一个UNetConnection家族的Actor,而Server-owned actor指的是存在于Server端的Actor。其实,这两个概念叫connection-owned和Server-only actor更加贴切。每个连接都有一个专门为其创建的PlayerController。每个出于此原因创建的PlayerController都归这个连接所有。要确定一个 actor 是否归某一连接所有,可以查询这个 actor 最外围的所有者,如果所有者是一个 PlayerController,则这个 actor 同样归属于拥有 PlayerController 的那个连接。类似的例子包括 Pawn actor 归 PlayerController 所有的情形。它们的所有者将是其所属的 PlayerController。在此期间,它们归属于 PlayerController 的连接。在连接归属于 PlayerController 期间,Pawn 只能由该连接所有。所以,一旦 PlayerController 不再拥有这个 Pawn,后者就不再归连接所有。另一个例子就是道具栏物品归 Pawn 所有的情况。这些道具栏物品归属于可能拥有该 Pawn 的同一连接(如存在)。在确定所属连接方面,组件有一些特殊之处。这时,我们要首先确定组件所有者,方法是遍历组件的“外链”,直到找出所属的 actor,然后确定这个 actor 的所属连接,像上面那样继续下去。

连接所有权是以下情形中的重要因素:

RPC 需要确定哪个客户端将执行运行于客户端的 RPC

Actor 复制与连接相关性

在涉及所有者时的 Actor 属性复制条件

  连接所有权对于 RPC 这样的机制至关重要,因为当您在 actor 上调用 RPC 函数时,除非 RPC 被标记为多播,否则就需要知道要在哪个客户端上执行该 RPC。它可以查找所属连接来确定将 RPC 发送到哪条连接。连接所有权会在 actor 复制期间使用,用于确定各个 actor 上有哪些连接获得了更新。对于那些将bOnlyRelevantToOwner设置为true的actor,只有拥有此actor的连接才会接收这个 actor 的属性更新。默认情况下,所有 PlayerController 都设置了此标志,正因如此,客户端才只会收到其拥有的 PlayerController的更新。这样做是出于多种原因,其中最主要的是防止玩家作弊和提高效率。对于那些要用到所有者的需要复制属性的情形来说,连接所有权具有重要意义。例如,当使用 COND_OnlyOwner 时,只有此 actor 的所有者才会收到这些属性更新。最后,所属连接对那些作为自治代理的 actor(角色为 ROLE_AutonomousProxy)来说也很重要。这些 actor 的角色会降级为 ROLE_SimulatedProxy,其属性则被复制到不拥有这些 actor 的连接中。

四、UE4中的网络实例分析--Character Movement

  设想在游戏中,角色在向前走,当把这个角色同步到其他机器上时,由于网络传输的延迟和网络带宽的限制,我们不可能实时的同步每一个角色每一瞬间的位置和动作,这样的话,我们的角色在其他电脑上的移动并不是很平滑,看起来是一直在瞬移的,这是一个问题;还有,UE4中是采用RPC同步的方式将客户端玩家的输入传给服务器,让服务器进行处理,那么,如果我们移动函数的参数设置为具体的速度值或者位移值,那么这样的话,玩家就可以通过给服务器传假的包来作弊;即使我们不传输具体数值,这样的话,我们传递命令给服务器,让服务器自行移动,仔细一想,这样也会有问题,那么通过加快客户端运行速度来实现客户端频繁的向服务器发生移动命令,也可以实现作弊……幸运的是,UE4已经帮我们解决了角色的移动问题。就是CharacterMovementComponent组件。这一节分析一下UE4对于角色移动的所做的考虑,作为一次网络实例的分析。

(1)UE4解决了玩家的瞬移问题

  为了解决玩家在两次同步之间的缝隙造成的瞬移,UE4采取本地对角色的移动进行模拟的策略,在UCharacterMovementComponent::SimulateMovement和MoveSmooth中,UE4实现了对SimulatedProxy的移动模拟,而且后者更加的节约性能。

(2)处理Speed Hack问题

  在以前的CS里,可以利用加速齿轮加快本地机器的运行速度,使得调用RPC的频率增加,进而让自己的角色移动速度和开枪速度加快。这个就是Speed Hack问题。那么为了不让玩家作弊,UE4加入了一种Speed Hack Protection的方法。在每次客户端调用移动的RPC Server函数时,加入一个TimeStamp的变量,服务器会根据这个变量,计算客户端本次调用RPC函数与上一次调用的时间差,如果这个差值过于小的话,则RPC函数调用失败。此时,客户端因为先执行了移动的函数,造成了客户端和服务器角色的位置不同步。此时服务器会重新发包给Actor所在客户端进行数据的更新,如果客户端的Actor的位置与服务器发来的位置偏差较大时,客户端会在调用PerformMovement进行移动之前,先调用ClientUpdatePostion进行位置的调整与更新。其中,两次RPC函数之间的调用阈值可以在GameNetworkManager中设置。

(3)RPC Server函数是设计为reliable还是unreliable

  我们想一下,涉及玩家移动的RPC Server函数,是设计为reliable好还是unreliable妙呢?如果是reliable的话,函数必然会执行,但是,这样执行速度太慢,会造成玩家体验感不好。而且如果网速卡的情况下,还会出现新的问题,比如:玩家看到了boss,点击了必杀,结果网络卡了半天,boss已经被消灭,此时玩家丢出去的必杀收不回来,又原地放了个必杀,这就很尴尬了。所以玩家行动的RPC函数,设计成unreliable会比较好一些。但是unreliable也会有问题,比如说:玩家点击了一下瞬移,网络卡了,包没发过去,此时客户端进行了移动,但是等服务器的同步包过来时,客户端又被拉到了移动前,看起来就像按键失灵了一样。一个比较好的办法是在客户端保持一个行动队列,每一次客户端的动作都加到队列里,当服务器确认了一个动作之后,就把动作从行动队列里移除,如果服务器没有确认该动作,则客户端重新发包,客户端这边就不会有“按键失灵”的感觉。

五、Further More 1.Detailed Actor Replication

  具体的细节,官网上有比较详细的文档。大多数 actor 复制操作都发生在 UNetDriver::ServerReplicateActors 内。在这里,服务器将收集所有被认定与各个客户端相关的 actor,并发送那些自上次(已连接的)客户端更新后出现变化的所有属性。

这里还定义了一个专门流程,指定了 actor 的更新方式、要调用的特定框架回调,以及在此过程中使用的特定属性。其中最重要的包括:

AActor::NetUpdateFrequency - 用于确定 actor 的复制频度

AActor::PreReplication - 在复制发生前调用

AActor::bOnlyRelevantToOwner - 如果此 actor 仅复制到所有者,则值为 true

AActor::IsRelevancyOwnerFor - 用于确定 bOnlyRelevantToOwner 为 true 时的相关性

AActor::IsNetRelevantFor - 用于确定 bOnlyRelevantToOwner 为 false 时的相关性

相应的高级流程如下:

循环每一个主动复制的 actor(AActor::SetReplicates( true ))

确定这个 actor 是否在一开始出现休眠(DORM_Initial),如果是这样,则立即跳过。

通过检查 NetUpdateFrequency 的值来确定 actor 是否需要更新,如果不需要就跳过

如果 AActor::bOnlyRelevantToOwner 为 true,则检查此 actor 的所属连接以寻找相关性(对所属连接的观察者调用 AActor::IsRelevancyOwnerFor)。如果相关,则添加到此连接的已有相关列表。

此时,这个 actor 只会发送到单个连接。

对于任何通过这些初始检查的 actor,都将调用 AActor::PreReplication。

PreReplication 可以让您决定是否针对连接来复制属性。这时要使用 DOREPLIFETIME_ACTIVE_OVERRIDE。

如果同过了以上步骤,则添加到所考虑的列表

对于每个连接:

对于每个所考虑的上述 actor

确定是否休眠

是否还没有通道

确定客户端是否加载了 actor 所处的场景

如未加载则跳过

针对连接调用 AActor::IsNetRelevantFor,以确定 actor 是否相关

如不相关则跳过

在归连接所有的相关列表上添加上述任意 actor

这时,我们拥有了一个针对此连接的相关 actor 列表

按照优先级对 actor 排序

对于每个排序的 actor:

如果连接没有加载此 actor 所在的关卡,则关闭通道(如存在)并继续

每 1 秒钟调用一次 AActor::IsNetRelevantFor,确定 actor 是否与连接相关

如果不相关的时间达到 5 秒钟,则关闭通道

如果相关且没有通道打开,则立即打开一个通道

如果此连接出现饱和

对于剩下的 actor

如果保持相关的时间不到 1 秒,则强制在下一时钟单位进行更新

如果保持相关的时间超过 1 秒,则调用 AActor::IsNetRelevantFor 以确定是否应当在下一时钟单位更新

对于通过了以上这几点的 actor,将调用 UChannel::ReplicateActor 将其复制到连接

UChannel::ReplicateActor 将负责把 actor 及其所有组件复制到连接中。其大致流程如下:

确定这是不是此 actor 通道打开后的第一次更新

如果是,则将所需的特定信息(初始方位、旋转等)序列化

确定该连接是否拥有这个 actor

如果没有,而且这个 actor 的角色是 ROLE_AutonomousProxy,则降级为 ROLE_SimulatedProxy

复制这个 actor 中已更改的属性

复制每个组件中已更改的属性

对于已经删除的组件,发送专门的删除命令

2.网络性能优化

  一般的网络优化手段基本都是基于上述的某些手段,官网已经将这些方法总结成了一篇短文,具体可以参看官网。复制 actor 是一件耗费时间的工作。引擎会尽量让这个过程变得更有效率,但您也可以做一些额外的工作来简化这个过程。在收集 actor 用于复制时,服务器将检查一些事项,如相关性、更新频度、休眠情况等。您可以调整这些检查项以改善性能。要最大限度提升这一过程的效率,最好是遵循以下优先顺序:

关闭复制(AActor::SetReplicates( false ))

当 actor 未进行复制时,它最初不会出现在列表中,我们可以充分利用这一点,确保那些无需复制的 actor 会有相应标记。

减少 NetUpdateFrequency 值

actor 的更新次数越少,更新所用的时间就越短。最好是尽量压低这个数值。该数值代表了这个 actor 每秒复制到客户端的频度。

休眠情况

相关性

NetClientTicksPerSecond

能在客户端生成的就不要在服务器生成(爆炸,特效等统一用一个函数来让客户端生成)

  如果属性并非是绝对必需,则不要将其标记为复制。如果可以,最好能尝试从现有的已复制属性中派生状态。尝试利用已有的量化函数,如 FVector_NetQuantize。这样能大大减少向客户端复制此状态时所需的大小,如果使用得当,就不会导致任何明显的偏差。FName 一般不会被压缩,所以在使用它们作为 RPC 的参数时,请记住它们通常会向字符串发送所有调用。这会产生很大的资源消耗。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3