《Exploring in UE4》關於網路同步的理解與思考[概念理解]

這篇文章可以幫助你更全面的理解虛幻4引擎的網路模塊~

目錄

一.關於Actor與其所屬連接

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

二.進一步理解RPC與同步

-1. RPC函數應該在哪個端執行?

-2. 客戶端創建的Actor能調用RPC么?

-3. RPC與Actor同步誰先執行?

-4. 多播MultiCast RPC會發送給所有客戶端么?

-5. RPC參數與返回值

三.合理使用COND_InitialOnly

四.客戶端與伺服器一致么?

五.屬性同步的基本規則與注意事項

-1. 結構體的屬性同步

-2. 屬性回調

-3. UObject指針類型的屬性同步

六.組件同步


一. 關於Actor與其所屬連接

UE4官網關於網路鏈接這一塊其實已經將的比較詳細了,不過有一些內容沒有經驗的讀者看起來可能還是比較吃力。

按照官網的順序,我一點點給出我的分析與理解。首先,大家要簡單了解一些客戶端的連接過程。

主要步驟如下:

1.客戶端發送連接請求

2.如果伺服器接受連接,則發送當前地圖

3.伺服器等待客戶端載入此地圖

4.載入之後,伺服器將在本地調用 AGameMode::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寫成下面的格式就可以了。

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

而經過前面的討論我們知道ROLE_AutonomousProxy與所屬連接是密不可分的。

最後,這裡留一個思考問題:如果我在客戶端創建出一個Actor,然後把它的Owner設置為帶連接的PlayerController,那麼他也有連接么?這個問題在下面的一節中回答。

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

並不是,有了前面的講述,我們已經可以理解,如果我在客戶端創建一個獨有的Actor(不能勾選bReplicate)。那麼這個Actor的Role就是ROLE_Authority,所以這時候你就不能通過判斷他的Role來確定當前調試的是客戶端還是伺服器。這時候最準確的辦法是獲取到NetDiver,然後通過NetDiver找到Connection。(事實上,GetNetMode()函數就是通過這個方法來判斷當前是否是伺服器的)對於伺服器來說,他只有N個ClientConnections,對於客戶端來說只有一個serverConnection。

如何找到NetDriver呢?可以參考下面的圖片,從Outer獲取到當前的Level,然後通過Level找到World。World裡面就有一個NetDiver。當然,方法不止這一個了,如果有Playercontroller的話,Playercontroller上面也有NetConnection,可以再通過NetConnection再獲取到NetDiver。


二. 進一步理解RPC與同步

1. RPC函數應該在哪個端執行?

對於一個形如UFUNCTION(Reliable, Client)的RPC函數,我們知道這個函數應該在伺服器調用,在客戶端執行。可是如果我在Standalone的端上執行該函數的時候會發生什麼呢?

答案是在伺服器上執行。其實這個結果完全可以參考下面的這個官方圖片。

剛接觸RPC的朋友可能只是簡單的記住這個函數應該從哪裡調用,然後在哪裡執行。不過要知道,即使我聲明一個在伺服器調用的RPC我還是可以不按套路的在客戶端去調用(有的時候並不是我們故意的,而是編寫者沒有理解透徹),其實這種不合理的情況UE早就幫我想到並且處理了。比如說你讓自己客戶端上的其他玩家去調用一個通知伺服器來執行的RPC,這肯定是不合理的,因為這意味著你可以假裝其他客戶端隨意給伺服器發消息,這種操作與作弊沒有區別~所以RPC機制就會果斷丟棄這個操作。

所以大家可以仔細去看看上面的這個圖片,對照著理解一下各個情況的執行結果,無非就是三個變數:

  1. 在哪個端調用
  2. 當前執行RPC的Actor歸屬於哪個連接
  3. RPC的類型是什麼。

2. 客戶端創建的Actor能調用RPC么?

不過看到這裡,再結合上一節結尾提到的問題,如果我在客戶端創建一個Actor。把這個Actor的Owner設置為一個帶連接PlayerController會怎麼樣呢?如果在這裡調用RPC呢?

我們確實可以通過下面這種方式在客戶端給新生成的Actor指定一個Owner。

好吧,關鍵時候還是得搬出來官方文檔的內容。

您必須滿足一些要求才能充分發揮 RPC 的作用: 1. 它們必須從 Actor 上調用。 2. Actor 必須被複制。 3. 如果 RPC 是從伺服器調用並在客戶端上執行,則只有實際擁有這個 Actor 的客戶端才會執行函數。 4. 如果 RPC 是從客戶端調用並在伺服器上執行,客戶端就必須擁有調用 RPC 的 Actor。 5. 多播 RPC 則是個例外:

o 如果它們是從伺服器調用,伺服器將在本地和所有已連接的客戶端上執行它們。

o 如果它們是從客戶端調用,則只在本地而非伺服器上執行。

o 現在,我們有了一個簡單的多播事件限制機制:在特定 Actor 的網路更新期內,多播函數將不會複製兩次以上。按長期計劃,我們會對此進行改善,同時更好的支持跨通道流量管理與限制。

看完第二條,其實你就能理解了,你的Actor必須要被複制,也就是說必須是bReplicate屬性為true, Actor是從伺服器創建並同步給客戶端的(客戶端如果勾選了bReplicate就無法在客戶端上正常創建,參考第4部分)。

所以,這時候調用RPC是失效的。我們不妨去思考一下,連接存在的意義本身就是一個客戶端到伺服器的關聯,這個關聯的主要目的就是為了執行同步。如果我只是在客戶端創建一個給自己看的Actor,根本就不需要網路的連接信息(當然你也沒有許可權把它同步給伺服器),所以就算他符合連接的條件,仍然是一個沒有意義的連接

同時,我們可以進一步觀察這個Actor的屬性,除了Role以外,Actor身上還有一個RemoteRole來表示他的對應端(如果當前端是客戶端,對應端就是伺服器,當前端是伺服器,對應端就是客戶端)。你會發現這個在客戶端創建的Actor,他的Role是ROLE_Authority(並不是ROLE_AutonomousProxy),而他的RemoteRole是ROLE_None。這也說明了,這個Actor只存在於當前的客戶端內。

3. RPC與Actor同步誰先執行?

下面我們討論一下RPC與同步直接的關係,這裡提出一個這樣的問題

問題伺服器ActorA在創建一個新的ActorB的函數里同時執行自身的一個Client的RPC函數,RPC與ActorB的同步哪個先執行?

答案是RPC先執行。你可以這樣理解,我在創建一個Actor的同時立刻執行了RPC,那麼RPC相關的操作會先封裝到網路傳輸的包中,當這個函數執行完畢後,伺服器再去調用同步函數並將相關信息封裝到網路包中。所以RPC的消息是靠前的。

那麼這個問題會造成什麼後果呢?

  1. 當你創建一個新的Actor的同時(比如在一個函數內),你將這個Actor作為RPC的參數傳到客戶端去執行,這時候你會發現客戶端的RPC函數的參數為NULL。
  2. 你設置了一個bool類型屬性A並用UProperty標記了一個回調函數OnRep_Use。你先在伺服器裡面修改了A為true,同時你調用了一個RPC函數讓客戶端把A置為true。結果就導致你的OnRep_Use函數沒有執行。但實際上,這會導致你的OnRep_Use函數裡面還有其他的操作沒有執行。

如果你覺得上面的情況從來沒有出現過,那很好,說明暫時你的代碼沒有類似的問題,但是我覺得有必要提醒一下大家,因為UE4代碼裡面本身就有這樣的問題,你以後也很有可能遇到。下面舉例說明實際可能出現的問題:

情況1:當我在伺服器創建一個NPC的時候,我想讓我的角色去騎在NPC上並控制這個NPC,所以我立刻就讓我的Controller去Possess這個NPC。在這個過程中,PlayerController就會執行UFUNCTION(Reliable, Client) void ClientRestart (APawn*

NewPawn)函數。當客戶端收到這個RPC函數回調的時候就發現我的APlayerController::ClientRestart_Implementation (APawn* NewPawn)裡面的參數為空~原因就是因為這個NPC剛在伺服器創建還沒有同步過來。

情況2:對於Pawn裡面的Controller成員聲明如下

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的函數時,可能是從客戶端調用,也可能是從伺服器調用。雖然結果都是在伺服器執行,但是過程可完全不同。從客戶端調用的在實際運行時是通過網路來處理的,一定會有延遲。而從伺服器調用的則會立刻執行。

4. 多播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 odd behavior. 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.

5. RPC參數與返回值

參數:RPC函數除了UObject類型的指針以及constFString&的字元串外,其他類型的指針或者引用都不可以作為RPC的參數。對於UObject指針類型我們可以在另一端通過GUID識別(後面第五部分有講解),但是其他類型的指針傳過去是什麼呢?我們根本就無法還原其地址,所以不允許傳輸其指針或者引用。

而對於FString,傳const原因我認為是為了不想讓發送方與接收方兩邊對字元串進行修改,而傳引用只是為了減少複製構造帶來的開銷。在FString發送與接收的處理細節裡面並不在意其是否是const&,他只在意他的類型以及相對Object的偏移。

返回值:一個RPC函數是不能有返回值的,因為其本身的執行就是一次消息的傳遞。假如一個客戶端執行一個Server RPC,如果有返回值的話,那麼豈不是伺服器執行後還要再發送一個消息給客戶端?這個消息怎麼處理?再發一次RPC?如果還有返回值那麼不就無限循環了?因此RPC函數不可以添加返回值。


三. 合理使用COND_InitialOnly

前面提到過,Actor的屬性同步可以通過這種方式來實現。

聲明一個屬性並標記

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

這裡面的第一個屬性一般的屬性複製,第二個就是條件屬性複製。條件屬性複製無非就是告訴引擎,這個屬性在哪些情況下同步,哪些情況下不同步。這些條件都是引擎事先提供好的。

這裡我想著重的提一下COND_InitialOnly這個條件宏,漢語的官方文檔是這樣描述的:該屬性僅在初始數據組嘗試發送。而英文是這樣描述的:This property will only attempt to send on the initial bunch。對比一下,果然還是英文看起來更直觀一點。

經過測試,這個條件的效果就是這個宏聲明的屬性只會在Actor初始化的時候同步一次,接下來的遊戲過程中不會再同步。所以,我們大概能想到這個東西在有些時候確實用的到,比如同步玩家的姓名,是男還是女等,這些遊戲開始到結束一般都不會改變的屬性。也就是說,上限一般調整的次數很少,如果真的有調整並需要同步,他會手動調用函數去同步該屬性。這樣就可以減少同步帶來的壓力。 然而,一旦你聲明為COND_InitialOnly。你就要清楚,同步只會執行一次,客戶端的OnRep回調函數就會執行一次。所以,當你在伺服器創建了一個新的Actor的時候你需要第一時間把需要改變的值修改好,一旦你在下一幀(或是下一秒)去執行那麼這個屬性就無法正確的同步到客戶端了。


四.客戶端與伺服器一致么?

我們已經知道UE4的客戶端與伺服器公用一套代碼,那麼我們在每次寫代碼的時候就有必要提醒一下自己。這段代碼在哪個端執行,客戶端與伺服器執行與表現是否一致?

雖然,我很早之前就知道這個問題,但是寫代碼的時候還是總是忽略這個問題,而且程序功能經常看起來運行的沒什麼問題。不過看起來正常不代表邏輯正常,有的時候同步機制幫你同步一些東西,有時候會刪除一些東西,有時候又會生成一些東西,然而你可能一點都沒發現。

舉個例子,我在一個ActorBeginPlay的時候給他創建一個粒子Emiter。代碼大概如下:

void AGate::BeginPlay(){ Super::BeginPlay(); //單純的在當前位置創建粒子發射器 GetWorld()->SpawnActor<AEmitter>(SpawnEmitter,GetActorLocation(), UVictoryCore::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沒有任何關係。

另外,還有一種常見的錯誤。就是我們的代碼執行是有條件的,然而這個條件在客戶端與伺服器是不一樣的(沒同步)。如

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

這個GateID是我們在GateBeginPlay的時候隨機初始化的,然而這個GateID只在伺服器與客戶端是不同的。所以需要伺服器同步到客戶端,才能按照我們理想的邏輯去執行。


五. 屬性同步的基本規則與注意事項

非休眠狀態下的Actor的屬性同步:只在伺服器屬性值發生改變的情況下執行

回調函數執行條件:伺服器同步過來的數值與客戶端不同

休眠的Actor:不同步

首先要認識到,同步操作觸發是由伺服器決定的,所以不管客戶端是什麼值,伺服器覺得該同步就會把數據同步到客戶端。而回調操作是客戶端執行,所以客戶端會判斷與當前的值是否相同來決定是否產生回調。

然後是屬性同步,屬性同步的基本原理就是伺服器在創建同步通道的時候給每一個Actor對象創建一個屬性變化表(這裡面涉及到FObjectReplicator,FRepLayout,FRepState,FRepChangedPropertyTracker相關的類,有興趣可以進一步了解,在另一深入UE網路同步文章里有講解),裡面會記錄一個當前默認的Actor屬性值。之後,每次屬性發生變化的時候,伺服器都會判斷新的值與當前屬性變化表裡面的值是否相同,如果不同就把數據同步到客戶端並修改屬性變化表裡的數據。對於一個非休眠且保持連接的Actor,他的屬性變化表是一直存在的,所以他的表現出來的同步規則也很簡單,只要伺服器變化就同步。

動態數組TArray在網路中是可以正常同步的,系統會檢測到你的數組長度是否發生了變化,並通知客戶端改變。

1. 結構體的屬性同步

注意,UE裡面UStruct類型的結構體在反射系統中對應的是UScriptStruct,他本身可以被標記Replicated並且結構體內的數據默認都會被同步,而且如果裡面有還子結構體的話也仍然會遞歸的進行同步。如果不想同步的話,需要在對應的屬性標記NotReplicated,而且這個標記只對UStruct有效,對UClass無效。

有一點特別的是,Struct結構內的數據是不能標記Replicated的。如果你給Struct裡面的屬性標記replicated,UHT在編譯的時候就會提醒你編譯失敗。

最後,UE裡面的UStruct不可以以成員指針的方式在類中聲明。

2. 屬性回調

問題:屬性回調與RPC在使用結果上的差異?

屬性回調理論上一定會執行,而RPC函數有可能由於錯過執行時機而不再會執行。例如:我在伺服器上面有一個寶箱,第一個玩家過去後,寶箱會自動開啟。如果使用RPC函數,當第一個玩家過去後,箱子執行多播RPC函數觸發開箱子操作。但是由於其他的玩家離這個箱子很遠,所有這個箱子沒有同步給其他玩家,其他玩家收不到這個RPC消息。(如果對結果有疑問參考第二節的第四個問題)當這些玩家之後再過去之後,會發現箱子還是關閉的。如果採用屬性回調,但第一個玩家過去後,設置箱子的屬性bOpen為true,然後同步到所有客戶端,通過屬性回調執行開箱子操作。這時候其他玩家靠近箱子時,箱子會同步到靠近的玩家,然後玩家在客戶端上會收到屬性bOpen,同時執行屬性回調,這時候可以實現所有靠近的玩家都會發現箱子已經被別人開過了。

問題:伺服器上生成一個Actor,他在客戶端上的UObject類型指針的屬性回調與他的Beginplay誰先執行?

這個問題這麼看有點奇怪,我進一步描述一下。有一個類MyActor,他有一個指針屬性PropertyB指向一個同步的MyActorB,同時這個指針屬性有一個回調函數。現在我在伺服器創建一個新的MyActor A,並設置A的PropertyB為MyActorB。那麼在客戶端上,是A的BeginPlay先執行,還是PropertyB的屬性回調先執行?

答案是不確定,一開始的時候,我一直認為是屬性回調在Actor的BeginPlay之前執行,測試了很多次也是這樣的。但是某種情況下, BeginPlay會先執行。這個問題的意義就在於,一個Actor同步過去執行BeginPlay的時候,你發現他的屬性還沒有同步過來(而且只發現指針可能沒有同步過來,其他內置類型都會在BeginPlay 前同步過來)。為什麼指針沒有同步過來?因為這個指針同步過來的時候,他指向的對象在客戶端還不存在,他在客戶端上也沒有對應的GUID緩存 。由於找不到對應的對象,他只能先暫時記錄下這個指針指向對象的GUID,然後在其他的Tick時間再回來檢測這個對象是否存在。這種情況一般來說很難重現,不過這個問題有助於我們進一步加深對網路的理解。

3. UObject指針類型的屬性同步

屬性同步也好,RPC參數也好。我們都需要思考一下,我在傳遞一個UObject類型的指針時,這個UObject在客戶端存在么?如果存在,我如何能通過伺服器的一個指針找到客戶端上相同UObject的指針?

答案是通過FNetworkGUID。伺服器在同步一個對象引用(指針)的時候,會給其分配專門的FNetworkGUID並通過網路進行發送。客戶端上通過識別這個ID,就可以找到對應UObject。

那麼如此說來,是不是只有標記Replicate的對象才能同步其引用或指針呢?

也不是。對於直接從數據包載入出來的對象(如地圖裡面實現搭建好的建築地形),我們可以直接認為伺服器上的該地形對象與客戶端上對應的地形對象就是一個對象,那麼在伺服器上指向該地形的指針發送到客戶端也應該就是指向對應地形的指針。所以總結來說一個UObject對象是否可以通過網路發送他的引用有如下條件(參考官方文檔):

您通常可以按照以下原則來確定是否可以通過網路引用一個對象:

任何複製的 actor 都可以複製為一個引用

任何未複製的 actor 都必須有可靠命名(直接從數據包載入)

任何複製的組件都可以複製為一個引用

任何未複製的組件都必須有可靠命名。

其他所有 UObject(非actor 或組件)必須由載入的數據包直接提供

什麼是擁有可靠命名的對象?

擁有可靠命名的對象指的是存在於伺服器和客戶端上的同名對象。

1.如果Actor 是從數據包直接載入(並非在遊戲期間生成),它們就被認為是擁有可靠命名。

2.滿足以下條件的組件即擁有可靠命名:

● 從數據包直接載入

● 通過construction scripts腳本添加

● 採用手動標記(通過 UActorComponent::SetNetAddressable 設置)

● 只有當您知道要手動命名組件以便其在伺服器和客戶端上具有相同名稱時,才應當使用這種方法(最好的例子就是 AActor C++ 構造函數中添加的組件)

最後總結一下就是有四種情況下UObject對象的引用可以在網路上傳遞成功

  1. 標記replicate
  2. 從數據包直接Load
  3. 通過Construction scripts添加或者C++構造函數裡面添加
  4. 使用UActorComponent::SetNetAddressable標記(這個只針對組件,其實藍圖裡面創建的組件默認就會執行這個操作)

六.組件同步

組件在同步上分為兩大類:靜態組件與動態組件

對於靜態組件:一旦一個Actor被標記為同步,那麼這個Actor身上默認所掛載的組件也會隨Actor一起同步到客戶端(也需要序列化發送)。什麼是默認掛載的組件?就是C++構造函數裡面創建的默認組件或者在藍圖裡面添加構建的組件。所以,這個過程與該組件是否標記為Replicate是沒有關係的。

對於動態組件:就是我們在遊戲運行的時候,伺服器創建或者刪除的組件。比如,當玩家走進一個洞穴時,給洞穴裡面的火把生成一個粒子特效組件,然後同步到客戶端上,當玩家離開的時候再刪除這個組件,玩家的客戶端上也隨之刪除這個組件。

對於動態組件,我們必須要設置他的Replicate屬性為true,即通過函數 AActorComponent ::SetIsReplicated(true)來操作。而對於靜態組件,如果我們不想同步組件上面的屬性,我們就沒有必要設置Replicate屬性。

一旦我們執行了SetIsReplicated(true)。那麼組件在屬性同步以及RPC上與Actor的同步幾乎沒有區別,組件上也需要設置GetLifetimeReplicatedProps來執行屬性同步,Actor同步的時候會遍歷他的子組件查看是否標記Replicate以及是否有屬性要同步。

bool AActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags){ check(Channel); check(Bunch); check(RepFlags); bool WroteSomething = 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); } } return WroteSomething;}

對於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); }}

推薦閱讀:

使用Unity2017開發MTV:一個Timeline和Cinemacine的實踐
我的世界手游末地傳送門框架在哪兒?末地傳送門框架怎麼找?
翻譯-《彩虹六號:圍攻》中可破壞關卡中的動態音頻設計
GMS2官方教程系列8/8——隨機生成敵人
遊戲資源傳送門

TAG:遊戲開發 | 虛幻引擎 | 網路遊戲 |