1. 程式人生 > >UE4網路同步詳解(一)——理解同步規則

UE4網路同步詳解(一)——理解同步規則

    這篇文章主要以問題的形式,針對UE同步的各個方面的內容,做一個詳細而充分的討論。對於新手理解UE的同步機制非常有幫助,對於有一定的基礎而沒有深入的UE程式也或許有一些啟發。如果想深入瞭解同步的實現原理,可以參考  

        問題一:如何理解Actor與其所屬連線?

            附加:1. Actor的Role是ROLE_Authority就是服務端麼?

        問題二:你真的會用RPC麼?

  附加:1. 多播MultiCast RPC會發送給所有客戶端麼?

        問題三:COND_InitialOnly怎麼用?

        問題四:客戶端與伺服器一致麼?

        問題五:屬性同步的基本規則是?

            附加:1.  結構體的屬性同步有什麼特別的?

 問題六:元件同步的基本規則是?

        Tips:同步注意的一些小細節

問題一:如何理解Actor與其所屬連線?

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

 主要步驟如下:

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)

{

         check(Channel);

         check(Bunch);

         check(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);

          

   }

}boolAActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)

{

         check(Channel);

         check(Bunch);

         check(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