UE4_網絡同步原理深入
本文更多是對Exploring in UE4有關網絡同步原理以及官方文檔的一些自己了解和總結。
1. 通信的基本流程
1.1 UE4伺服器與用戶端的通信流程
UE4程序内部伺服器Server與用戶端Client的通信 主要如下:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2Lc1TP3FGaWhUY2FjMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2LcRHelR3LcJzLctmch1mclRXY39zN5ITOwYDNxEjMxkDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
每一個用戶端叫做一個Connection,如圖,就是一個server連接配接到兩個用戶端的效果。對于每一個用戶端,都會建立起一個Connection。在伺服器上這個Connection叫做ClientConnection,對于用戶端這個Connection叫做ServerConnection。每一個Channel都會歸屬于一個Connection,這樣這個Channel才知道他對應的是哪個用戶端上的對象。 接下來我們繼續細化,圖中的Channel隻标記了1,2,3,那麼實際上都有哪些Channel?這些Channel對應的都是什麼對象?其實,在第一部分的概念裡我已經列舉了常見的3中Channel,分别是ControlChannel,ActorChannel,以及VoiceChannel。一般來說,ControlChannel與VoiceChannel在遊戲中隻存在一個,而ActorChannel則對應每一個需要同步的Actor。
1.2 Connection和Channel之間的關系和聯系。
- Connection:表示一個網絡連接配接。伺服器上,一個用戶端到一個伺服器的一個連接配接叫一個ClientConnection。在用戶端上,一個伺服器到一個用戶端的連接配接叫一個ServerConnection。
- Channel:資料通道,每一個通道隻負責交換某一個特定類型特定執行個體的資料資訊。
- ControlChannel:用戶端伺服器之間發送控制資訊,主要是發送接收連接配接與斷開的相關消息。在一個Connection中隻會在初始化連接配接的時候建立一個該通道執行個體。
- VoiceChannel:用于發送接收語音消息。在一個Connection中隻會在初始化連接配接的時候建立一個該通道執行個體。
- ActorChannel:處理Actor本身相關資訊的同步,包括自身的同步以及子元件,屬性的同步,RPC調用等。每個Connection連接配接裡的每個同步的Actor都對應着一個ActorChannel執行個體。 常見的隻有這3種:枚舉裡面還有FileChannel等類型,不過沒有使用。
Connection和Channel之間的關系:
1.3 接收和發送資訊
到這裡我們基本上就了解了UE4的基本通信架構了,下面我們進一步分析網絡傳輸資料的流程。首先我們要知道,UE4的資料通信是建立在UDP-Socket的基礎上的,與其他的通信程式一樣,我們需要對Socket的資訊進行封裝發送以及接收解析。這裡面主要涉及到Bunch,RawBunch,Packet等概念。
如下圖所示,這個是主要的接收和發送資訊的整體流程:
通過上圖可以看出,主要由TickFlush和TickDispatch來監控接收和發送資訊。發送資訊過程主要是由Channel根據消息類型來封裝資訊,Connection再把這些資訊處理後發送。而接受資訊則與這個恰好相反。
以下是借鑒Exploring in UE4的圖,分别是詳細的發送消息和接收消息的過程。
1.3.1 發送消息
1.3.2 接收資訊
2. 連接配接建立
在第一塊我們了解了通信的基本流程,但是在一開始建立通信這個過程是如何連接配接的呢?
通過這個圖我們可以知道網絡通信其實是在GameInstance建立之後,然後去建立網絡相關的闆塊。
然而在上面的内容我們都知道了,是由NetDriver去驅動整個網絡通信,并且用戶端通過Connection連接配接伺服器。
那麼這個過程是怎麼建立的呢?
2.1 伺服器網絡子產品初始化流程
2.2 用戶端網絡子產品初始化流程
2.3 伺服器與用戶端建立連接配接流程
二者都完成初始化後,用戶端就會開始發送一個Hello類型的ControlChannel消息給伺服器(上面用戶端初始化最後一步)。伺服器接收到消息之後開始處理,然後會根據條件再給用戶端發送對應的消息,如此來回處理幾個回合,完成連接配接的建立,主要流程如下:
主要參考這幅圖簡化而來的。
// message type definitions
DEFINE_CONTROL_CHANNEL_MESSAGE_THREEPARAM(Hello, , uint8, uint32, FString); // initial client connection message
DEFINE_CONTROL_CHANNEL_MESSAGE_THREEPARAM(Welcome, , FString, FString, FString); // server tells client they're ok'ed to load the server's level
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Upgrade, , uint32); // server tells client their version is incompatible
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Challenge, , FString); // server sends client challenge string to verify integrity
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Netspeed, , int32); // client sends requested transfer rate
DEFINE_CONTROL_CHANNEL_MESSAGE_FOURPARAM(Login, , FString, FString, FUniqueNetIdRepl, FString); // client requests to be admitted to the game
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Failure, , FString); // indicates connection failure
DEFINE_CONTROL_CHANNEL_MESSAGE_ZEROPARAM(Join, ); // final join request (spawns PlayerController)
DEFINE_CONTROL_CHANNEL_MESSAGE_TWOPARAM(JoinSplit, , FString, FUniqueNetIdRepl); // child player (splitscreen) join request
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Skip, , FGuid); // client request to skip an optional package
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Abort, , FGuid); // client informs server that it aborted a not-yet-verified package due to an UNLOAD request
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(PCSwap, , int32); // client tells server it has completed a swap of its Connection->Actor
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(ActorChannelFailure, , int32); // client tells server that it failed to open an Actor channel sent by the server (e.g. couldn't serialize Actor archetype)
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(DebugText, , FString); // debug text sent to all clients or to server
DEFINE_CONTROL_CHANNEL_MESSAGE_TWOPARAM(NetGUIDAssign, , FNetworkGUID, FString); // Explicit NetworkGUID assignment. This is rare and only happens if a netguid is only serialized client->server (this msg goes server->client to tell client what ID to use in that case)
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(SecurityViolation, , FString); // server tells client that it has violated security and has been disconnected
DEFINE_CONTROL_CHANNEL_MESSAGE_TWOPARAM(GameSpecific, , uint8, FString); // custom game-specific message routed to UGameInstance for processing
DEFINE_CONTROL_CHANNEL_MESSAGE_ZEROPARAM(EncryptionAck, );
以上的控制資訊具體作用,請閱讀源碼。
3. Actor同步細節
3.1 Actor同步流程
有了前面的描述,我們已經知道NetDiver負責整個網絡的驅動,而ActorChannel就是專門用于Actor同步的通信通道。
伺服器在NetDiver的TickFlush裡面,每一幀都會去執行
ServerReplicateActors
來同步Actor的相關内容,大多數 actor 複制操作都發生在
UNetDriver::ServerReplicateActors
内。在這裡,伺服器将收集所有被認定與各個用戶端相關的 actor,并發送那些自上次(已連接配接的)用戶端更新後出現變化的所有屬性。
這裡還定義了一個專門流程,指定了 actor 的更新方式、要調用的特定架構回調,以及在此過程中使用的特定屬性。其中最重要的包括:
-
- 用于确定 actor 的複制頻度AActor::NetUpdateFrequency
-
- 在複制發生前調用AActor::PreReplication
-
- 如果此 actor 僅複制到所有者,則值為 trueAActor::bOnlyRelevantToOwner
-
- 用于确定 bOnlyRelevantToOwner 為 true 時的相關性AActor::IsRelevancyOwnerFor
-
- 用于确定 bOnlyRelevantToOwner 為 false 時的相關性AActor::IsNetRelevantFor
相應的進階流程如下:
- 循環每一個主動複制的 actor(
)AActor::SetReplicates( true )
- 确定這個 actor 是否在一開始出現休眠(
),如果是這樣,則立即跳過。DORM_Initial
- 通過檢查 NetUpdateFrequency 的值來确定 actor 是否需要更新,如果不需要就跳過
- 如果
為 true,則檢查此 actor 的所屬連接配接以尋找相關性(對所屬連接配接的觀察者調用AActor::bOnlyRelevantToOwner
)。如果相關,則添加到此連接配接的已有相關清單。AActor::IsRelevancyOwnerFor
- 此時,這個 actor 隻會發送到單個連接配接。
- 對于任何通過這些初始檢查的 actor,都将調用
。AActor::PreReplication
- PreReplication 可以讓您決定是否針對連接配接來複制屬性。這時要使用
。DOREPLIFETIME_ACTIVE_OVERRIDE
- 如果同過了以上步驟,則添加到所考慮的清單。
- 确定這個 actor 是否在一開始出現休眠(
- 對于每個連接配接(connection):
- 對于每個所考慮的上述 actor
- 确定是否休眠
- 是否還沒有通道
- 确定用戶端是否加載了 actor 所處的場景
- 如未加載則跳過
- 針對連接配接調用
,以确定 actor 是否相關AActor::IsNetRelevantFor
- 如不相關則跳過
- 在歸連接配接所有的相關清單上添加上述任意 actor
- 這時,我們擁有了一個針對此連接配接的相關 actor 清單
- 按照優先級對 actor 排序
- 官網文檔
-
優先級排序規則是什麼?
答案:是按照是否有controller,距離以及是否在視野。通過FActorPriority構造代碼可以定位到APawn::GetNetPriority,這裡面會計算出目前Actor對應的優先級,優先級越高同步越靠前,是否有Controller的權重最大)
- 對于每個排序的 actor:
- 如果連接配接沒有加載此 actor 所在的關卡,則關閉通道(channel)(如存在)并繼續
- 每 1 秒鐘調用一次 AActor::IsNetRelevantFor,确定 actor 是否與連接配接相關
- 如果不相關的時間達到 5 秒鐘,則關閉通道(channel)
- 如果相關且沒有通道打開,則立即打開一個通道(channel)
- 如果此連接配接出現飽和 (飽和處理)
- 對于剩下的 actor
- 如果保持相關的時間不到 1 秒,則強制在下一時鐘機關進行更新
- 如果保持相關的時間超過 1 秒,則調用
以确定是否應當在下一時鐘機關更新AActor::IsNetRelevantFor
- 對于通過了以上這幾點的 actor,将調用
将其複制到連接配接。UChannel::ReplicateActor
3.2 将 Actor 複制到連接配接
UChannel::ReplicateActor
将負責把 actor 及其所有元件複制到連接配接中。其大緻流程如下:
- 确定這是不是此 actor 通道(channel)打開後的第一次更新
- 如果是,則将所需的特定資訊(初始方位、旋轉等)序列化
- 确定該連接配接是否擁有這個 actor
- 如果沒有,而且這個 actor 的角色是
,則降級為ROLE_AutonomousProxy
ROLE_SimulatedProxy
- 如果沒有,而且這個 actor 的角色是
- 複制這個 actor 中已更改的屬性
- 複制每個元件中已更改的屬性
- 對于已經删除的元件,發送專門的删除指令
總之,大體上Actor同步的邏輯就是在TickFlush裡面去執行ServerReplicateActors,然後進行前面說的那些處理。最後對每個Actor執行ActorChannel::ReplicateActor将Actor本身的資訊,子對象的資訊,屬性資訊封裝到Bunch并進一步封裝到發送緩存中,最後通過Socket發送出去。
3.3 Actor同步過程
伺服器Actor同步堆棧圖如下:
用戶端Actor建立同步堆棧圖如下:
用戶端Actor初始化後同步圖如下:
其實Actor同步主要是其子元件和其屬性的同步。
4. 屬性同步
官方文檔
4.1 網絡同步的一些資料結構
-
FObjectReplicator
屬性同步的執行器,每個Actorchannel對應一個FObjectReplicator,每一個FObjectReplicator對應一個對象執行個體。設定ActorChannel通道的時候會建立出來。
-
FRepState
針對每個連接配接同步的曆史資料,記錄同步前用于比較的Object對象資訊,存在于FObjectReplicator裡面。
-
FRepLayOut
同步的屬性布局表,記錄所有目前類需要同步的屬性,每個類或者RPC函數有一個。
-
FRepChangedPropertyTracker
屬性變化軌迹記錄,一般在同步Actor前建立,Actor銷毀的時候删掉。
-
FReplicationChangelistMgr
存放目前的Object對象,儲存屬性的變化曆史記錄
UE4_網絡同步原理深入UE4_網絡同步原理深入 UE4_網絡同步原理深入UE4_網絡同步原理深入 一個Actorchannel類對應一個FObjectReplicator,屬于屬性同步最重要的類。
關于FRepLayout中Parents屬性與CMD屬性:FRepLayout裡面,數組parents示目前類所有的需要同步的屬性,而數組cmd會将同步的複雜類型屬性【包括數組、結構體、結構體數組但不包括類類型的指針】進一步展開放到這裡面 。
下面開始進一步描述屬性同步的基本思路:我們給一個Actor類的同步屬性A做上标記Replicates(先不考慮其他的宏),然後UClass會将所有需要同步的屬性儲存到ClassReps清單裡面(該過程在反射過程實作),這樣我們就可以通過這個Actor的UClass擷取這個Actor上所有需要同步的屬性,當這個Actor執行個體化一個可以同步的對象并開始建立對應的同步通道時,我們就需要準備屬性同步了。
首先,我們要有一個同步屬性清單來記錄目前這個類有哪些屬性需要同步(FRepLayout,每個對象有一個,從UClass裡面初始化,屬于一種參考表,不儲存具體屬性資料);其次,我們需要針對每個對象儲存一個緩存資料,來及時的與發生改變的Actor屬性作比較,進而判斷與上一次同步前是否發生變化(FRepState,裡面有一個Staticbuff來儲存具體的屬性資料,和FRepLayout對照使用);然後,我們要有一個屬性變化跟蹤器記錄所有發生改變同步屬性的序号(可能是因為節省記憶體開銷等原因是以不是儲存這個屬性),便于發送同步資料時處理(FRepChangedPropertyTracker,對各個Connection可見,被各個Connection的Repstate儲存一個共享指針,新版本被FRepChangelistState替換)。最後,我們還需要針對每個連接配接的每個對象有一個控制前面這些資料的執行者(FObjectReplicator)。
4.2 屬性同步過程
在上文,我們知道Actor的同步主要是通過ServerReplcateActors實作的。那麼具體的流程又大概是怎樣的呢?
通過該圖我們可以看出,首先通過SetChannelActor為上述我們網絡同步的四個類初始化,然後就是同步Actor。
4.3 屬性同步初始化
當Actor同步時如果發現目前的Actor沒有對應的通道,就會給其建立一個通道并執行
SetChannelActor
。這個
SetChannelActor
所做的工作就是屬性同步的關鍵所在,這個函數裡面會對上面四個關鍵的類構造并做初始化,詳細的内容參考下圖:
通過圖檔,我們可以看出在SetChannelActor中初始化建構了上述我們所講的幾個類。
4.4 屬性同步過程
// 以下代碼有删減
bool FObjectReplicator::ReplicateProperties( FOutBunch & Bunch, FReplicationFlags RepFlags )
{
UObject* Object = GetObject();
// some games ship checks() in Shipping so we cannot rely on DO_CHECK here, and these checks are in an extremely hot path
UNetConnection* OwningChannelConnection = OwningChannel->Connection;
FNetBitWriter Writer( Bunch.PackageMap, );
// Update change list (this will re-use work done by previous connections)
ChangelistMgr->Update( Object, Connection->Driver->ReplicationFrame, RepState->LastCompareIndex, RepFlags, OwningChannel->bForceCompareProperties ); // 更新函數,判斷屬性是否發生變化。
// Replicate properties in the layout
const bool bHasRepLayout = RepLayout->ReplicateProperties( RepState.Get(), ChangelistMgr->GetRepChangelistState(), ( uint8* )Object, ObjectClass, OwningChannel, Writer, RepFlags ); // 同步屬性過程。
// Replicate all the custom delta properties (fast arrays, etc)
ReplicateCustomDeltaProperties( Writer, RepFlags );
//... 下面删減很大一部分
return WroteImportantData;
}
再次拿出伺服器同步屬性的流程,我們可以看到屬性同步是通過
FObjectReplicator::ReplicateProperties
函數執行的,進一步執行
RepLayout->ReplicateProperties
。這裡面比較重要的細節就是伺服器是如何判斷目前屬性發生變化的,我們在前面設定通道
Actor
的時候給
FObjectReplicator
設定了一個Object指針,這個指針儲存的就是目前同步的對象,而在初始化
RepChangelistState
的同時我們還建立了一個
Staticbuffer
,并且把
buffer
設定和目前Object的大小相同,對
buffer
取
OffSet
把對應的同步屬性值添加到
buffer
裡面。是以,我們真正比較的就是這兩個對象,一般來說,
staticbuffer
在建立通道的同時自己就不會改變了,隻有當與
Object
比較發現不同的時候,才會在發送前把屬性值置為改變後的。這對于長期同步的
Actor
沒什麼問題,但是對于休眠的
Actor
就會出現問題了,因為每次删除通道并再次同步強制同步的時候這裡面的
StaticBuffer
都是Object預設的屬性值,那比較的時候就可能出現0不同步這樣奇怪的現象了。真正比較兩個屬性是否相同的函數是
PropertiesAreIdentical()
,他是一個
static
函數。
static FORCEINLINE bool PropertiesAreIdentical( const FRepLayoutCmd& Cmd, const void* A, const void* B )
{
const bool bIsIdentical = PropertiesAreIdenticalNative( Cmd, A, B );
return bIsIdentical;
}
在
Compareproperties
函數中把現在
UObject
的資訊和
FReplicationChangelistMgr
中的
StaticBuffer
比較更新,然後在屬性同步前判斷條件複制屬性的條件,如果符合則發送屬性。
4.5 屬性回調函數執行
雖然屬性同步是由伺服器執行的,但是
FObjectReplicator
,
RepLayOut
這些資料可并不是僅僅存在于伺服器,用戶端也是存在的,用戶端也有
Channel
,也需要執行
SetChannelACtor
。不過這些資料在用戶端上的作用可能就有一些變化,比如
StaticBuffer
,伺服器是用它存儲上次同步後的對象,然後與目前的Object比較看是否發生變化。在用戶端上,他是用來臨時存儲目前同步前的對象,然後再把通過過來的屬性複制給目前
Object
,
Object
再與
StaticBuffer
對象比較,看看屬性是否發生變化,如果發生變化,就在
Replicator
的
RepState
裡面添加一個函數回調通知
RepNotifies
。 在随後的
ProcessBunch
進行中,會執行
RepLayout->CallRepNotifies( RepState, Object )
;處理所有的函數回調,是以我們也知道了為什麼接收到的屬性發生變化才會執行函數回調了。
在随後的
ProcessBunch
進行中,會執行
RepLayout->CallRepNotifies( RepState, Object )
;處理所有的函數回調,是以我們也知道了為什麼接收到的屬性發生變化才會執行函數回調了。
思考:伺服器如果發生變化,那麼回調函數是否一定會執行?
回答:不是。舉個例子;
UPROPERTY(Replicated, ReplicatedUsing = OnRep_Health)
float health;
void AFPSAIGuard::OnRep_Health()
{
GEngine->AddOnScreenDebugMessage(, f, FColor::Red, GetFName().ToString() + FString::SanitizeFloat(health));
OnHealthChanged(health);
}
void AFPSAIGuard::OnHealthHurt()
{
health -= f;
if (health <= f) {
Destroy();
}
}
// health預設是100
就比上面的例子,當伺服器和用戶端同時都調用這個
OnHealthHurt
這個函數的時候,由于伺服器的health發生變化改為80,而用戶端此時也變為了80,伺服器同步到用戶端的時候,會去比較兩個值,如果發生變化,就在
Replicator
的
RepState
裡面添加一個函數回調通知
RepNotifies
。 在随後的
ProcessBunch
進行中,會執行
RepLayout->CallRepNotifies( RepState, Object )
;處理所有的函數回調,是以我們也知道了為什麼接收到的屬性發生變化才會執行函數回調了。
思考:RPC與Actor同步誰先執行?
下面我們讨論一下RPC與同步直接的關系,這裡提出一個這樣的問題
問題:伺服器ActorA在建立一個新的ActorB的函數裡同時執行自身的一個Client的RPC函數,RPC與ActorB的同步哪個先執行?
答案:是RPC先執行。你可以這樣了解,我在建立一個Actor的同時立刻執行了RPC,那麼RPC相關的操作會先封裝到網絡傳輸的包中,當這個函數執行完畢後,伺服器再去調用同步函數并将相關資訊封裝到網絡包中。是以RPC的消息是靠前的。
那麼這個問題會造成什麼後果呢?
- 當你建立一個新的Actor的同時(比如在一個函數内),你将這個Actor作為RPC的參數傳到用戶端去執行,這時候你會發現用戶端的RPC函數的參數為NULL。
- 你設定了一個bool類型屬性A并用UProperty标記了一個回調函數OnRep_Use。你先在伺服器裡面修改了A為true,同時你調用了一個RPC函數讓用戶端把A置為true。結果就導緻你的OnRep_Use函數沒有執行。但實際上,這會導緻你的OnRep_Use函數裡面還有其他的操作沒有執行。
5. Actor子元件同步
官網文檔
//UActorChannel::ReplicateActor() DataChannel.cpp
// The Actor
WroteSomethingImportant |= ActorReplicator->ReplicateProperties( Bunch, RepFlags );
// 子對象的同步操作
WroteSomethingImportant |= Actor->ReplicateSubobjects(this, &Bunch, &RepFlags);
//ActorReplication.cpp
boolAActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)
{
check(Channel);
check(Bunch);
check(RepFlags);
bool WroteSomething = false;
for (int32 CompIdx =; CompIdx < ReplicatedComponents.Num(); ++CompIdx )
{
UActorComponent * ActorComp = ReplicatedComponents[CompIdx].Get();
//如果元件标記同步
if (ActorComp && ActorComp->GetIsReplicated())
{
WroteSomething |= ActorComp->ReplicateSubobjects(Channel, Bunch, RepFlags); // Lets the component add subobjects before replicating its own properties.檢測元件否還有子元件
WroteSomething |= Channel->ReplicateSubobject(ActorComp, *Bunch, *RepFlags); // (this makes those subobjects 'supported', and from here on those objects may have reference replicated) 同步該元件
}
}
return WroteSomething;
}
//DataChannel.cpp
boolUActorChannel::ReplicateSubobject(UObject *Obj, FOutBunch&Bunch, constFReplicationFlags&RepFlags)
{
if ( !Connection->Driver->GuidCache->SupportsObject( Obj ) )
{
FNetworkGUID NetGUID = Connection->Driver->GuidCache->AssignNewNetGUID_Server(Obj ); //Make sure he gets a NetGUID so that he is now 'supported'
}
bool NewSubobject = false;
if (!ObjectHasReplicator(Obj))
{
Bunch.bReliable = true;
NewSubobject = true;
}
//元件的屬性同步需要先在目前的ActorChannel裡面建立新的FObjectReplicator
bool WroteSomething = FindOrCreateReplicator(Obj).Get().ReplicateProperties(Bunch, RepFlags);
if (NewSubobject && !WroteSomething)
{
......
}
return WroteSomething;
}
通過上述圖檔和代碼,我們可以看出,同步Actor子元件其實最終也是在同步屬性。其實也就是我們在第4部分所做的内容。
6. RPC
其實如果了解了屬性回調函數執行過程的話,那麼RPC其實也是類似的方法。(前提實作了反射機制)
以下是RPC函數執行封包過程:
而我們在代碼裡的函數之是以必須要加上_Implementation,就是因為在調用端裡面,實際執行的是.genenrate.cpp檔案函數,而不是我們自己寫的這個。同時結合下面的RPC執行堆棧,我們可以看到在UObject這個對象系統裡,我們可以通過反射系統查找到函數對應的UFuntion結構,同時利用ProcessEvent函數來處理UFuntion。通過識别UFunction裡面的标記,可以知道這個函數是不是一個RPC函數,是否需要發送給其他的端。 當我們開始調用CallRemoteFunction的時候,RPC相關的初始化就開始了。NetDiver會進行相關的初始化,并試着擷取RPC函數的Replayout,那麼問題是函數有屬性麼?正常來說,函數本身就是一個執行過程,函數名是一個起始的執行位址,他本身是沒有記憶體空間,更不用說存儲屬性了。不過,在UE4的反射系統裡面,函數可以被額外的定義為一個UFunction,進而儲存自己相關的資料資訊。RPC函數的參數就被儲存在UFunction的基類Ustruct的屬性連結清單PropertyLink裡面,RepLayOut裡面的屬性資訊就是從這裡擷取到的。 一旦函數的RepLayOut被建立,也同樣會放到NetDiver的RepLayoutMap裡面。随後立刻調用FRepLayout::SendPropertiesForRPC将RPC的參數序列化封裝與RPC函數一同發送。
以下是RPC函數接收執行過程:
傳遞的過程中是以
FFieldNetCache
的資料形式儲存的。
在
ReceivedRPC()
解析函數并且最後執行函數。
UE4版本 4.20
參考:
Exploring in UE4
Actor 複制流程詳述