深入淺出UE4網絡


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 < ROLE_Authority)
 4     {
 5         ServerFlyDown();
 6     }
 7     if (!GetCharacterMovement()->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 所有權 未復制 NetMulticast Server Client
Client-owned actor 在服務器上運行 在服務器和所有客戶端上運行 在服務器上運行 在 actor 的所屬客戶端上運行
Server-owned actor 在服務器上運行 在服務器和所有客戶端上運行 在服務器上運行 在服務器上運行
Unowned actor 在服務器上運行 在服務器和所有客戶端上運行 在服務器上運行 在服務器上運行

  從客戶端調用RPC:

 
Actor 所有權 未復制 NetMulticast Server Client
Owned by invoking client 在執行調用的客戶端上運行 在執行調用的客戶端上運行 在服務器上運行 在執行調用的客戶端上運行
Owned by a different client 在執行調用的客戶端上運行 在執行調用的客戶端上運行 丟棄 在執行調用的客戶端上運行
Server-owned actor 在執行調用的客戶端上運行 在執行調用的客戶端上運行 丟棄 在執行調用的客戶端上運行
Unowned actor 在執行調用的客戶端上運行 在執行調用的客戶端上運行 丟棄 在執行調用的客戶端上運行

 

  在藍圖中使用RPC時,只需要對函數進行如下設置即可。

  在C++中要將一個函數聲明為 RPC,您只需將 ServerClient 或 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 < ROLE_Authority)
4      {
5          ServerFlyUp();
6      }
7       //implement character fly up
8        //.......  
9  }    

  上面兩段代碼中,第一段是Server函數的具體實現,而下一段是Server函數在客戶端的調用和具體飛行的邏輯。這兩段代碼有點繞,具體含義是這樣的:客戶端的AutonomousProxy角色響應鍵盤的輸入,本地的Actor執行FlyUp()函數,實現角色的本地飛行,然后客戶端調用RPC Server函數,RPC函數在客戶端得到調用,因為它是Server函數,所以在Server端進行執行,執行的代碼就寫在ServerFlyUp_Impementation()里,而執行的代碼與客戶端本地的FlyUp()相同,只不過,因為服務器Role == Role_Authority,所以,不再調用ServerFlyUp。那么有的朋友可能會有問題,能不能給RPC函數添加參數呢,答案是可以的。UE4會自動幫你同步參數,前提是你的參數類型必須是Replicates的。那么指針參數會不會導致指針失效呢?答案是不會的。具體原因后面會討論。還有個小問題,能不能把飛行的最終位置作為參數傳給Server函數呢?答案可以,但是不好。為什么呢?技術上要實現是可以的,但是因為客戶端的數據是不可靠的,為了防止玩家作弊,RPC函數的使用最好以命令的形式傳給服務器比較好。

  注意到Server函數的例子中,添加了WithValidation的字段。這是為了防止玩家作弊,添加的一個驗證函數,我們可以自行編寫驗證條件,如果客戶端的數據不滿足驗證條件,則客戶端這邊會直接放棄RPC的調用,到了服務器那邊,服務器在執行Server函數之前還會進行一次判斷,防止客戶端發送假的包過來,如果判斷通過了,才會繼續執行。實驗環境下,我們默認驗證條件都返回true。

1 bool AShooterCharacter::ServerFlyUp_Validate()
2 {
3     return true;
4 }

  上述的一個例子中,還有兩個字段是reliable和unreliable。reliable相當於TCP傳輸,可靠,必達,順序一定,但是速度慢。unreliable則相當於UDP,不可靠,網絡不好時有可能出現不可達,但是好處就是速度快。

  總結一下得到如下的表格。

選項 說明
Not Replicated 這個是默認選項,表示該事件不會進行復制。如果它是在客戶端上調用,就只能在這個客戶端上運行,如果在服務器上調用,就只能在服務器上運行。
Multicast 如果一個多播事件是在服務器上調用,它將不顧哪條連接擁有目標對象,而被復制到所有已連接的客戶端。如果客戶端調用了一個多播事件,它將被視為沒有經過復制,而且只能在調用它的客戶端上運行。
Run on Server 如果該事件是從服務器調用,它就只能運行在服務器上。如果是從客戶端調用且擁有一個歸客戶端所有的目標,它將被復制到服務器並在上面運行。“Run on Server” 事件是客戶端向服務器發送數據的主要途徑。
Run on Owning Client 如果從服務器調用,該事件將運行於擁有目標 actor 的客戶端。由於服務器可以擁有 actor,“Run on Owning Client” 事件可能實際運行在服務器上(盡管從名稱上看不是這樣)。如果是從客戶端調用,該事件將被視為沒有經過復制,而且只能在調用它的客戶端上運行。


4.Actor Component Replication

  (此部分官網已經講得較為清楚,可點擊此處進入鏈接)UE4支持Actor的組件復制,但是在UE4中運用的卻不多,組件的屬性修改基本都是通過組件的Server函數來實現,很少情況下才需要在邏輯代碼中直接修改Component的屬性,或者動態增/刪組件。Component作為Actor的一部分進行復制,Actor 仍然掌管角色、優先級、相關性、剔除等方面的工作。一旦復制了 Actor,它就可以復制自身的Component。這些組件可以按 Actor 的方式復制屬性和RPC(至於Component為什么能調用RPC函數,后面會有解釋)。Component必須以 Actor 的方式來實施::GetLifetimeReplicatedProps函數。

  組件復制中涉及兩大類組件。一種是隨 Actor一起創建的靜態組件。也就是說,在客戶端或服務器上生成所屬Actor時,這些組件也會同時生成,與組件是否被復制無關。服務器不會告知客戶端顯式生成這些組件。 在此背景下,靜態組件是作為默認子對象在 C++ 構造函數中創建,或是在藍圖編輯器的組件模式中創建。靜態組件無需通過復制存在於客戶端;它們將默認存在。只有在屬性或事件需要在服務器和客戶端之間自動同步時,才需要進行復制。動態組件是在運行時在服務器上生成的組件種,其創建和刪除操作也將被復制到客戶端。它們的運行方式與 Actor 極為一致。與靜態組件不同, 動態組件需通過復制的方式存在於所有客戶端。另外,客戶端可以生成自己的本地非復制組件。這適合於很多種情形。只有當那些在服務器上觸發的屬性或事件需要自動同步到客戶端時,才會出現復制行為。

  在組件上設置屬性和 RPC 的過程與 Actor 並無區別。將一個類設置為具有復本后,這些組件的實際實例也必須經過設置后才能復制。在C++中進行組件復制,只需調用 AActorComponent::SetIsReplicated(true) 即可。如果需要復制的組件是一個默認子對象,就應當在生成組件之后通過類構造函數來完成此調用。例如:(注意:真正的ACharacter不是這樣實現的)

 1 ACharacter::ACharacter()
 2 {
 3     // Etc...
 4 
 5     CharacterMovement = CreateDefaultSubobject<UMovementComp_Character>(TEXT("CharMoveComp"));
 6     if (CharacterMovement)
 7     {
 8         CharacterMovement->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<UReplicatedObject>(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 actorServer-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 的參數時,請記住它們通常會向字符串發送所有調用。這會產生很大的資源消耗。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM