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

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

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

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

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

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

        問題三:COND_InitialOnly怎麼用?

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

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

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

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

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

 


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


      www/UE4/CHN/Gameplay/Networking/Actors/OwningConnections/index.html。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寫成下面的格式就可以了。

       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

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