UnrealEngine - 網絡同步入門

1 網絡同步機制

UE 提供了強大的網絡同步機制:

  • RPC :可以在本地調用,對端執行
  • 屬性同步:標記一個屬性爲 UPROPERTY(Replicated) 就可以自動將其修改後的值同步到客戶端
  • 移動複製:Actor 開啓了移動複製後會自動複製位置,旋轉和速度
  • 創建和銷燬:Server 創建 Actor 時根據其權限會在所有連接客戶端生成遠程代理
    UE 基本上都是基於 Actor 進行同步的。Actor 同步的前提需要標記 Actor 爲 bReplicated 。首先來了解下如何應用 UE 中的屬性同步。

2 Actor 同步

2.1 如何同步一個 Actor

首先思考一下,如何創建一個 Actor 然後讓他同步到各個客戶端?

  • 在哪裏創建?創建 Actor 的操作顯然需要在服務端執行,如果在客戶端執行,這個 Actor 只會在這個客戶端可見。

image.png|625

  • 如何讓 Actor 同步? 標記 Actor 的 bReplicated 爲 True。

2.2 如何同步 Actor 的屬性

創建並同步完 Actor 之後,下一步是能夠支持 Actor 的數據能夠正常同步到客戶端,首先在應用層如何支持這一操作?
假設我們有一把武器,需要同步武器的彈藥數量,那麼需要進行如下定義

/** weapon.h **/
class AWeapon : public  {
	UPROPERTY(replicatedUsing=OnRep_Ammo) // 可選屬性,當 Ammo 成功同步後會調用該函數
	int32 Ammo; // 彈藥數量
	UFUNCTION()  
	virtual void OnRep_Ammo();
	virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const override; // 屬性複製條件控制
}
/** weapon.cpp **/
void AWeapon::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const {
   Super::GetLifetimeReplicatedProps(OutLifetimeProps);  
   DOREPLIFETIME(AWeapon, Ammo); // 具體的複製屬性
}

上述定義主要有如下特點:

  • Actor 支持同步時,如果有自定義需要同步的屬性,需要重寫 GetLifetimeReplicatedProps 函數,並在其中標註要複製的具體屬性
  • 同步屬性時,可以通過 URPOPERTY 宏中 replicatedUsing 屬性來指定同步後要執行的回調函數

2.3 Actor 同步流程

現在我們需要考慮一下,Actor 的屬性在什麼情況下會被複制?通常來說我們只需要在 Actor 屬性被修改時就需要同步到客戶端,但是什麼時候會被修改我們並不知道,因此引擎中會根據 Actor 複製頻率 來做同步檢查。參考後文中 4.4 優先級和複製頻率 的內容。我們可以梳理出如下流程:

image.png|350

基本上每幀都需要檢查有哪些 Actor 需要同步,顯然這種檢查也是比較耗時的,由此 UE 也引入了 PushModel 技術,手動標記 Actor 哪些屬性已修改需要更新,從而節約檢查屬性的消耗。

2.4 小結

如何創建,同步一個 Actor 的應用層流程基本梳理完畢,但是顯然需要知道其後面的原理,由此引出如下問題在後續的文章中解決:

Actor 同步只能從 Server 同步到 Client,Client 唯一向 Server 發送請求的方式只有 RPC,屬性同步是單向的

3 RPC 使用分析

3.1 什麼是 RPC

RPC(Remote Procedure Call,遠程過程調用)是一種用於實現分佈式應用程序的技術。通過 RPC,可以使分佈式應用程序中的各個部分像本地代碼一樣交互,即使它們不在同一臺計算機或在不同的網絡上。
在 RPC 中,一個應用程序可以調用另一個應用程序中的函數或方法,就像調用本地函數一樣。這些函數和方法在不同的進程或計算機上執行,但對調用方來說,它們是透明的。調用方不需要了解遠程代碼的具體實現細節,只需要知道如何調用它們並處理返回值。

RPC 的使用有一些前提準則,必須滿足這些條件才能調用

  1. 它們必須從 Actor 上調用。
  2. Actor 必須被複制。
  3. 如果 RPC 是從服務器調用並在客戶端上執行,則只有實際擁有這個 Actor 的客戶端纔會執行函數。
  4. 如果 RPC 是從客戶端調用並在服務器上執行,客戶端就必須擁有調用 RPC 的 Actor。
  5. 多播 RPC 則是個例外:
  • 如果它們是從服務器調用,服務器將在本地和所有已連接的客戶端上執行它們。
  • 如果它們是從客戶端調用,則只在本地而非服務器上執行。
  • 現在,我們有了一個簡單的多播事件限制機制:在特定 Actor 的網絡更新期內,多播函數將不會複製兩次以上。按長期計劃,我們會對此進行改善,同時更好的支持跨通道流量管理與限制。

3.2 RPC 的種類

UE 中有 3 種 RPC :

  • Server : 僅在 Server 上調用
  • Client :僅在 Client 上調用
  • NetMulticast :在與服務器連接的所有客戶端及服務器本身上調用
    這三種 RPC 只需要在函數調用的聲明中加上對應的標記即可。

3.2.1 如何確定 RPC 在哪裏被執行

當 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 在哪裏被執行,主要根據如下三個條件:

  1. 調用端是誰(Client/Server)
  2. 調用的 Actor 屬於哪個連接
  3. RPC 的類型(Server/Client/NetMulticast)

舉一個例子,有兩個客戶端 c1 和 c2 各自有 Pawn p1 和 p2,c1 的客戶端上能夠獲取到 p2 這個對象,但是無法利用 p2 調用 RPC,因爲在 c1 上 p2 只是一個普通的 Pawn,其沒有對應的 c2 的 PlayerController(參考 [[總體框架#3. PlayerController|PlayerController 定義]])。也沒有對應的 Connection,因此無法執行 RPC。

  1. 實際上是否會調用到對端,主要根據 UObject::GetFunctionCallspace 這個接口返回的枚舉來判定的。
  2. 其次根據 Actor 所屬的 Connection,如果 Actor 不屬於任何一個 Connection(Owner 遞歸查找找不到 PlayerController),那麼也是無法調用 RPC 的。

3.3 RPC 的使用

UE 中,一個 RPC 函數的聲明和定義如下(以 Client 調用 Server 執行的 RPC 爲例):

/** weapon.h **/
class AWeapon : public  {
	UFUNCTION(Server)
	void Fire();
}

/** weapon.cpp **/
void AWeapon::Fire_Implementation() {
	/** do weapon fire **/ 
}

此時只需要在 Client 端使用如下操作:

AWeapon* Weapon = GetWeapon();
Weapon->Fire();

就能直接調用 Server 端的 Fire 接口了。關於其背後實現的原理,可以參考 [[原理#4. QA#4.5 RPC 函數如何執行的|RPC函數執行原理]]。
這裏需要注意一點,UE 的 RPC 是沒有返回值的,統一都是 void。個人如果需要獲取返回值,那麼就需要一個類似協程的概念,來獲取返回值,否則只能阻塞等待或者異步等待,後者顯然代碼可讀性也不是很好。

3.4 小結

RPC 與屬性同步有些不同,RPC 可以 Server To Client 也可以 Client To Server,是一種雙向的通信方式,而屬性同步只能 Server To Client,屬於單向同步。對於 RPC 的實現,有如下問題可以再進行深究:

4 Actor 同步概念

4.1 NetRole

每個 Actor 都有一個 LocalRole 和 RemoteRole 的概念,分別對應於 Actor 在本地和在對端的 Role,Role 主要分爲 3 種:

  • ROLE_SimulatedProxy
  • ROLE_AutonomousProxy
  • ROLE_Authority
    通常 LocalRole=Authority 只存在於服務器(但是客戶端也有可能存在,比如 Spawn 一個 Actor 但是不標記爲 Replicated)。關於各種 Role 常見的設置可以參考下圖:
    image.png|850

4.1.1 AutonomousProxy 和 SimulatedProxy 的區別

  • AutonomousProxy 和 SimulatedProxy 基本只存在於客戶端,ROLE_AutonomousProxy 用於處理本地玩家的輸入,並將這些輸入發送到服務器進行處理,而 ROLE_SimulatedProxy 用於處理其他玩家的輸入,並在客戶端上模擬 Actor 在服務器上的運行。因此通常 AutonomousProxy 只存在於 PlayerController 和其 Possess 的 Pawn。
  • SimulatedProxy 是標準的模擬途徑,通常是根據上次獲得的速率對移動進行推算。當服務器爲特定的 actor 發送更新時,客戶端將向着新的方位調整其位置,然後利用更新的間歇,根據由服務器發送的最近的速率值來繼續移動 actor。
  • AutonomousProxy 通常只用於 PlayerController 所擁有的 actor。這說明此 actor 會接收來自真人控制者的輸入,所以在我們進行推算時,我們會有更多一些的信息,而且能使用真人輸入內容來補足缺失的信息(而不是根據上次獲得的速率來進行推算)。

4.1.2 小結

那麼這個 Role 有什麼用呢?個人認爲有如下用處:

  • 在 C/S 模式下,基本可以認爲 LocalRole 爲 Authority 的 Actor 當前就是處於服務器環境下,用來區分服務器還是客戶端
  • 引擎對於 AutonomousProxy 和 SimulatedProxy 做了區分,用來更好的模擬玩家輸入

就目前而言,只有服務器能夠向已連接的客戶端同步 Actor (客戶端永遠都不能向服務器同步)。始終記住這一點, 只有 服務器才能看到 Role == ROLE_Authority 和 RemoteRole == ROLE_SimulatedProxy 或者 ROLE_AutonomousProxy

4.2 關聯連接

UE 中 Actor關聯連接的概念,即這個 Actor 屬於哪個連接。在傳統的 C/S 服務器中,每個客戶端和服務器會有一條連接,在 UE 中會爲每個連接創建一個 PlayerController,這樣這個 PlayerController 就歸這條連接所有。
而如果一個 Actor 的 Owner 爲 PlayerController 或者爲 Pawn 並且這個 Pawn 擁有一個 PlayerController,那麼這個 Actor 就歸屬於擁有這個 PlayerController 的連接。
這裏的關聯連接有什麼用呢?
考慮如下三種情況:

  • 需要確定哪個客戶端將執行運行於客戶端的 RPC
  • Actor 複製與連接相關性(比如 bOnlyRelevantToOwner 爲 True 的 Actor,只有擁有這個 Actor 的 Connection 纔會收到這個 Actor 的屬性更新,比如 PlayerController)
  • 涉及 Owner 的 Actor 屬性複製條件(比如 COND_OnlyOwner 只能複製給 Owner)

連接所有權

4.3 相關性

相關性是用於判斷 Actor 是否需要進行同步的重要依據。其主要判斷相關性的接口爲 AActor::IsNetRelevantFor 。個人認爲相關性最重要的一點是可以有效的節約帶寬和同步操作所帶來的 CPU 消耗
比如場景的規模可能比較大,玩家特定時刻只能看到關卡中部分 Actor。被服務器認爲可見或者能夠影響客戶端的 Actor 組會被是爲該客戶端的相關 Actor 組,服務器只會讓客戶端知道其相關組內的 Actor。

  1. 如果 Actor 是 bAlwaysRelevant、歸屬於 Pawn 或 PlayerController、本身爲 Pawn 或者 Pawn 是某些行爲(如噪音或傷害)的發起者,則其具有相關性。
  2. 如果 Actor 是 bNetUseOwnerRelevancy 且擁有一個所有者,則使用所有者的相關性。
  3. 如果 Actor 是 bOnlyRelevantToOwner 且沒有通過第一輪檢查,則不具有相關性。
  4. 如果 Actor 被附加到另一個 Actor 的骨架模型,它的相關性將取決於其所在基礎的相關性。
  5. 如果 Actor 是不可見的 (bHidden == true) 並且它的 Root Component 並沒有碰撞,那麼則不具有相關性,
    • 如果沒有 Root Component 的話,AActor::IsNetRelevantFor() 會記錄一條警告,提示是否要將它設置爲 bAlwaysRelevant=true
  6. 如果 AGameNetworkManager 被設置爲使用基於距離的相關性,則只要 Actor 低於淨剔除距離,即被視爲具有相關性。

Pawn 和 PlayerController 將覆蓋 AActor::IsNetRelevantFor() 並最終具有不同的相關性條件。

4.4 優先級和複製頻率

4.4.1 優先級

每個 Actor 都有一個名爲 NetPriority 的浮點變量。這個變量的數值越大,Actor 相對於其他"同伴"的帶寬就越多。和優先級爲 1.0 的 Actor 相比,優先級是 2.0 的 Actor 可以得到兩倍的更新頻度。唯一影響優先順序的就是它們的比值。
計算 Actor 的當前優先級時使用了函數 AActor::GetNetPriority。爲避免出現饑荒(starvation),AActor::GetNetPriority 使用 Actor 上次複製後經過的時間去乘以 NetPriority。同時,GetNetPriority 函數還考慮了 Actor 與觀察者的相對位置以及兩者之間的距離。

4.4.2 複製頻率

Actor 不是每一幀都進行復制的,每個 Actor 有個自己的每秒複製頻率 NetUpdateFrequency,每次檢查 Tick 的 DeltaTime > 1/NetUpdateFrequency,滿足條件纔可以進行下一步複製檢查。
比如默認 PlayerState 每秒更新 1 次,而 Pawn 每秒更新 100 次(默認情況下服務器 30 fps 運行,基本上每幀都會做複製檢查)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章