了解 Unreal Engine 4 同步傳輸的開銷(Bunch Overhead)

windkey
NeoBards
Published in
9 min readJun 16, 2020
Photo by Kevin Horvat on Unsplash

這篇文章其實算是 Network Profiler (1) (2) 的後續,主要是利用 Profiler 分析的過程中發現 replicate property 已經減少很多,可是 total send Bytes 沒有如預期的下降到目標。

經過追查研究後,發現問題在 Bunch Overhead,因此才有了這一篇內容。

Bunch Overhead

如果降 replicate 傳輸到一定程度之後,會發現其實 Bunch Overhead 蠻大的。從 Profiler 裡面可以看到 Bunch Overhead 分為:

Bunch Headers
ContentBlock Headers
Content Footers
Handles
Export GUIDS
MustBeMapped GUIDS

這幾個項目,如下圖所示。

BunchOverhead 的細項可在 Network Profiler 的 Summary 內找到

其中我只稍微追查 Bunch Headers、Handles、Export GUIDS 這幾項而已,其他項目因為傳輸不多,因此沒有深入探究。

Bunch Headers

Bunch Headers 到底傳了那些,可以在:

Engine/Source/Runtime/Engine/Private/NetConnection.cpp

的 UNetConnection::SendRawBunch() 裡面追查到。大致上就是這個 Bunch是 open/close,是不是 reliable、channel index 是多少等等的資訊。

Bunch headers 的資料量從我們開發端是很難省掉的,不過引擎端因為Fornite 持續在開發的關係,應該會不斷地改進。例如以前有Bunch.bIsDormant,現在直接被合併進

enum EChannelCloseReason Bunch.CloseReason

Export GUIDS

這個項目主要發生在 Server 要同步一個新的 Actor 給 client 的時候,需要利用 GUID 跟 client 建立對應關係。如果是動態生成的物件,會直接使用物件的路徑+名稱,以字串的方式作為 GUID 傳送。

舉例來說,server 生成一個新的需要同步的 Actor,路徑在Content/Gameplay/Character/Skill/BP_bbb。
那麼生成這個 actor 就會產生 Export GUIDS 的項目,在 network profiler 可以看到,大小至少是 Gameplay/Character/Skill/BP_bbb 31 個 Bytes。
會說至少是因為前後還會再加上 Prefix 以及 Suffix,所以實際會更多。

除此之外,如果 BP_bbb 這個 Actor 的 component 也勾了 component replicate 的話,這個 component 也會產生 Export GUIDS 的項目。
也就是說如果你的遊戲很頻繁的動態生成物件,那麼 Export GUIDS 這個項目會很高。

但是 Export GUIDS 是可以透過物件池改善的。若同一個 Actor 可以重複使用,則不需要傳 GUID。當然網路版本的物件池如何同步狀態又是另一個難題了。

有關 Export GUIDS 輸出的程式碼大約是在

Engine/Source/Runtime/Engine/Private/PackageMapClient.cpp
UPackageMapClient::SerializeNewActor->
UPackageMapClient::SerializeObject->
UPackageMapClient::InternalWriteObject

有需要可以從這幾個地方開始追。

Handles

FNetworkProfiler::TrackWritePropertyHandle 這個函式就是用來處理 Handles 的項目,而呼叫的地方都是從

Engine/Source/Runtime/Engine/Private/RepLayout.cpp

的 WritePropertyHandle 觸發的。

如果搜尋程式碼的話就可以知道 Handle 就是 Server 在 Replicate Actor 的 replicate 變數的時候,用來告訴 client 後續的資料是哪個變數的。

舉例來說:BP_bbb 有三個可同步的 integer 變數 var1、var2、var3。
如果只有 var2 有改變,var2 的值從 0 變成 1,那 server 就需要送類似 BP_bbb (var2, 1) 這樣的資料給 client。而 var2 要怎麼表示讓 client 知道,就是由 Handle 負責。

基本上就是每個變數依照順序給編號,所以 var1=1、var2=2、var3=3,經由轉換後就是 BP_bbb (2, 1)。

不過因為 server 需要讓 client 知道 property 資料傳完了 所以最後還要傳編號 0 作為 property 的結束,也就是BP_bbb (2, 1, 0)。

Array property 的情況就更複雜了,除了 array property 的 index,最後也要加上編號 0 作 Array 結束的識別證明。詳細傳 Array 的過程的額外細節我就沒有特別追查,總之記得成本比一般的 property 多就對了。

Handle 的傳輸細節

Handle 的變數宣告為 uint16,所以如果我們傳 var2 的資料 (2, 1),大小會是多少呢?

在實際輸出 Handle 的時候,因為呼叫了
Writer.SerializeIntPacked(LocalHandle)
所以不會每次輸出都是 2+4 Bytes。但是還是有一定的大小。輸出的實際程式碼在

Engine/Source/Runtime/Core/Private/Serialization/BitWriter.cpp
FBitWriter::SerializeIntPacked(uint32& InValue)

Worst Case

上述的範例 假設 handle 是 1 Byte,那麼傳遞一個 integer 實際上是,
4/(4+1) = 80%,等於有 20% 是 overhead。

如果我們要同步的變數只有 1bit 的 boolean 呢? 最後可能要傳 9Bits,
1/ (8+1) = 11%,等於接近 90% 的傳輸都是 overhead!

改善Handle的Overhead

有一個做法可以避免 replicate 每個 property 前面都要帶一個 handle,那就是把多個 property 包成一個 structure,並且實作這個 structure 的NetSerialize()。
如此一來 這多個 property 的傳輸只會共用一個 handle 來處理。

範例
http://www.aclockworkberry.com/custom-struct-serialization-for-networking-in-unreal-engine/
可以參考這篇文章來實作。或是直接搜尋引擎程式碼,有很多範例。

缺點

整個 structure 實作 NetSerialize 後有一個重大的缺點,就是在計算變動的時候也是以整個 structure 作記憶體比較,所以整個 structure 如果只要有一個變數變動,也會呼叫 NetSerialize 並輸出。

原來的版本因為各個 property 拆開,所以每個 property 會獨自作記憶體比較再輸出,所以如果整個 structure 常常只有少部分變動的話,有可能實作NetSerialize 反而傳輸會變大。

實際上還是要在沒實作 NetSerialize+Handles 跟實作可能會浪費之間取得平衡點。

追蹤SerializeNewActor傳輸開銷

除了 Bunch Overhead 屬於比較隱密容易被漏掉的項目,Server 生成一個新的 actor 所需要的傳輸量其實在 Network Profiler 沒有顯示出來。

除了從程式碼可以稍微知道基本要傳哪些資訊,最後實際上同步新的 actor 的資料量其實是非常難知道的。

這邊列出幾個我知道的項目

GUIDS
Transform與速度
Replicate Properties

GUIDS 前面有提過,這個是 Network Profiler 內就能看到的項目。

Transform、Rotation、Scale、Velocity 等項目是要傳輸但無法在 Profiler 內找到的。

最後是這個 actor 需要 replicate 的各個變數,如果與預設值不同就會傳輸。 這部份在 Network Profiler 可以看到。

引擎對生成新 actor 的處理技巧

程式碼可以看到在傳各個 Transform、Rotation、Scale、Velocity 之前,會先給一個 bit 告知是不是預設值,如果是預設值的話就直接不傳了。這個傳輸技巧還蠻值得學習,可以應用在 NetSerialize 實作的時候,搜尋引擎程式碼也會看到這個技巧被大量應用在各個需要傳輸的地方。

然後因為 Transform、Rotation 等等都有自己的 NetSerialize 版本。所以傳輸的資料量是變動的,造成 SerializeNewActor 的成本很難預估。

另一個值得注意的是 Vector 是使用 FVector_NetQuantize10,Rotation 是使用 FRotator。平常有需要同步座標或旋轉的時候,記得使用這兩種類型來降低傳輸。

--

--

windkey
NeoBards

位於台灣遊戲業的程式設計師,樂於分享遊戲引擎相關的心得與開發技術。