了解UE4同步傳輸的開銷(Bunch Overhead)
這篇文章其實算是Network Profiler (一) (二)的後續
主要是利用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
這幾個項目
如圖1.所示
圖1. BunchOverhead的細項可在Network Profiler的Summary內找到 |
其中我只稍微追查Bunch Headers, Handles, Export GUIDS這幾項而已,其他項目因為傳輸不多所以我沒有深入追。
Bunch Headers
Bunch Header到底傳了那些,可以在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 profiller可以看到,大小至少是
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 spawn一個新的actor所需要的傳輸量其實在Network Profiler沒有頁面顯示出來
除了從程式碼可以稍微知道基本要傳哪些資訊 最後實際上同步新的actor的資料量其實是非常難知道的
這邊列出幾個我知道的項目
- GUIDS
- Transform與速度
- Replicate Properties
GUIDS 前面有提過,這個是NetworkProfiler內就能看到的項目
Transform, Rotation, Scale, Velocity等項目是要傳輸但無法在Profiler內找到的
最後是這個actor需要replicate的各個變數,如果與預設值不同就會傳輸。
這部份在NetworkProfiler可以看的到。
程式碼可以看到在傳各個Transform, Rotation, Scale. Velocity之前
會先給一個bit告知是不是預設值,如果是預設值的話就直接不傳了。
這個傳輸技巧還蠻值得學習,可以應用在NetSerialize實作的時候,
搜尋引擎程式碼也會看到這個技巧被大量應用在各個需要傳輸的地方。
然後因為Transform, Rotation等等都有自己的NetSerialize版本。
所以傳輸的資料量是變動的,造成SerializeNewActor的成本很難預估。
另一個值得注意的是Vector是使用FVector_NetQuantize10,Rotation是使用FRotator。
平常有需要同步座標或旋轉的時候,記得使用這兩種類型來降低傳輸。
以上就是有關網路傳輸開銷的內容
Gameplay Network 傳輸相關的內容會暫時告一段落,目前沒有新的項目要分享。
不過如果有想知道UE4的課題,也歡迎讓我知道,謝謝~
留言
張貼留言