虛幻4隨筆6 Object和序列化

誠如之前所說,虛幻4主要的一些特性都是由UObject穿針引線在一起的,想把虛幻玩到比較深的程度,UObject是遲早要面對、迴避不得的問題,所以,準備在其它主題之前,先把UObject好好弄一下。UObject主要完成了哪些工作呢?私以爲:

 

反射系統

 

UObject體系構建了整個虛幻反射系統的核心,每個UObject都來自於一個UClass,這個Class可以是Unreal Header Tool(以後統一遵循官網命名:UHT)生成的,也可以是來自於Blueprint生成的(UBlueprintGeneratedClass)。反射可以說是現在主流引擎的構建基礎,對國內多數人而言,可能更熟悉的是Unity透過Mono構建出來的反射,它的重要性不言而喻。

反射很大一坨的東西,具體就不說了,它最大的作用,相當於在運行時動態生成代碼,可以省掉很多手寫代碼的工作量。否則像UE這樣複雜的界面,全部Hardcode,100人是絕對不夠的,改一次所需的時間也是無法接受的。有了反射之後,剩下的很多就是很好理解的一條路就順下來了:屬性編輯器自動生成、自動消息包收發、自動序列化、自動生成BP節點、BP和C++的自動接口交互、自動淺拷貝深拷貝、甚至按照設定規則來進行拷貝……不勝枚舉。

共通性都是一樣:Get Class,Get Property,或者Get Function,分析Property和Function的屬性,然後,設值、獲取值、Invoke函數……

 

垃圾回收和生命期管理

 

UObject構建了虛幻的垃圾回收(GC)系統。GC這東西衆說紛紜,但博主本人持樂觀態度。最近的公司業務就遇到這麼個事兒:腳本里需要發動態包,於是就需要在腳本里手動生成一個動態包,並掛接在包上面。爲了完成這個目的,我就必須在腳本中製作一個生成動態包的節點,然後問題來了,我們必須還得要一個回收動態包的節點,否則這個動態包就無法回收……於是最後放棄了,回到了包里加各種Reserved的老路上去……有了GC,多數情況下都不用管這事兒……你讓一個策劃去理解回收這種事情,就是在給他們添麻煩,那本不是他們業務內的範疇。

GC的存在價值,並非是讓事情變得簡單這麼簡單,更多時候它能讓你節省下很多編程心力,把精力花在真正該關注的地方。真GC出問題時去查錯所帶來的成本,未必比忘寫delete帶來的成本要大,說不定反而更小。

虛幻的垃圾回收系統,基本上就是從Root開始,不斷遍歷所有的Property,標記其爲使用中。最後再遍歷一遍,確認哪些Object沒有標使用中就給它刪掉。基本上,你不需要管這個過程,因爲反射的作用,所以相關的信息都是UE自動就幫我們處理好的。有幾個要注意的:

"Singleton",需要一直存在的,直接AddToRoot。

F類本身是不走垃圾回收的,但是F 類內部又有U類,這種情況下你需要注意AddReferencedObjects。把F類內部的U類給加入到GC樹上。

Classes裏的類,標UPROPERTY的UObject屬性會被自動加入到當前類的GC列表裏,但不標的會不會,沒有具體跟,反正習慣隨手寫個就行了。

TArray和TMap裏面的UObject會被自動加入GC列表,但是如果寫的是std::vector和std::map,則應該是不會的,需要手動用AddReferencedObjects加進去。

 

資源管理

 

UObject最後一個作用是構建了虛幻的資源管理體系。包括資源的搜索、資源之間的引用管理,下面詳細展開。

首先要先說一下虛幻的Object命名,由於資源也都是UObject,所以其命名與UObject是同一個標準。按照現在的要求,是[類名']路徑名/路徑名/Asset名.[包內路徑.]Object本名:[屬性名]['](一般是Object所在類名+一個數字後綴)。比如:

Brush'/Script/Engine.Default__Brush'

BillboardComponent'/Script/Engine.Default__TextRenderActor:Sprite'

/Engine/TemplateResources/MI_Template_BaseGray_03_Metal.MI_Template_BaseGray_03_Metal

這個名稱解析跟UE3和UDK略有不同,UE3由於基於upk來對包進行管理,而又限制Content文件夾下的Upk包不能重名,所以不需要前面的路徑名。UE4基於Asset,Content不同子目錄下可以有同名UAsset,所以路徑名就是不可或缺的了。除此之外,Asset跟UPK沒有太多不同,我們後面說包,也是指的UAsset,雖然看起來這個不像包。

理論上,所有的UObject都可以交由StaticLoadObject來加載(事實上也確實是這麼做的),但是很多類是有基於LoadObject的特殊實現的,比如UClass(Blueprint Class),就必須用StaticLoadClass,而地圖必須使用LoadMap,LevelStreaming這樣地圖相關的加載流程。這些變種的主要區別是會針對相應的情況做一些特殊的處理和操作。但是核心都繞不開StaticLoadObject。所以搞明白這個StaticLoadObject,實際上就搞明白了虛幻主要的資源組織結構。

主要的流程如下:

解析路徑,找到對應包(UAsset或者UPK),如果還沒加載則加載包。

判斷Object是否已經加載,如果已經加載則直接返回。

對資源包的加載,會把整個資源包的所有Object全部預加載的(創建並調用PreLoad,對於資源等需要PostLoad的調用PostLoad)。同時,加載包時創建ULinkerLoad,這個LinkerLoad會自動分析每個包與其他包之間的關聯,通過Imports來記錄本包對其它包的引用,通過Exports記錄本包內的Object。

加載後,看看目標對象是不是個Redirector,如果是Redirector,則說明"曾經有個包在這裏,但是被移動到新地方去了",就重定位到必要的地方。

說了這麼多,其實你明白原理就很簡單:虛幻所有對象都是按照一個包含了路徑、包內路徑、對象類名的唯一名稱來命名的。而虛幻所有的包都是會記錄對其它包的引用的。

所以一旦一個包的路徑、對象的包內路徑、以及對象類名本身發生變化,都可能會導致舊有資源的丟失和重定向。

當然,相關也都有一些機制可以幫助你事後修正(比如Redirector,Engine.ini裏的Redirector config),但是,那都是補救措施,不能100%保證成功補救。好的情況,重新定位一下資源引用什麼的就可以解決,但是最糟糕的情況下,有可能會導致數據丟失(其中最容易發生的就是因爲BP類名修改,導致子BP類無法找到父BP類而導致子類無法正常使用只能刪了重來……)

所以,在做UE4的包路徑轉移、資源名修改之前,一定要做好備份工作。最好是把所有原型迭代完畢後,統一進行類似操作,並經常存檔或發SVN、GIT。

 

相關的注意事項:

 

Redirector需要提醒一點,虛幻裏進行資源文件從一個文件夾到另一個文件夾的移動,一定要在編輯器中進行。因爲虛幻資源之間的引用關係是通過前面說的Object命名來保證的,而路徑名又是Object的一部分,名稱不對等很容易發生問題。而編輯器移動資源後,有時候會發現移動前所在的路徑下多了一個1KB字節左右小尾巴,這個小尾巴就是Redirector,同樣不要手動刪除,而是要在資源查看器裏,通過對Redirector(需要Filter開啓)的Fix up命令來進行刪除。

先把Redirector打開

然後Fixup,或者

右鍵直接文件夾,Fix up Redirectors in Folder。

 

由於包是"Link Load"的,加載過程中會分析引用,所以如果包比較碎,這裏就會由於做了更多的文件訪問而導致速度變慢。單如果單個包內數據量較大,則也會導致加載單個包時速度變慢的情況,歸根到底,就是權衡啊權衡。(一般說來,10個1k > 1個10k)

 

Transient對象不會被存盤,Transient包(GetTransientPackage)是一個特殊的包,所有臨時對象都應該創建在這個包裏面。

 

異步實際並非真正異步。LoadObject加載過程中有一系列的全局變量,而且這些全局變量維護時沒有任何鎖,所以也無法真正做到異步加載。所以雖然您看到接口上有LoadPackageAsync,但那個的實現是在主線程每幀區分時間片來實現的。不過話說回來,多線程讀包真的有必要嗎?機械硬盤的訪問速度本身是最大的限制因素,讀包過程中的多線程,CPU其實幫不上任何忙。

 

真正大量磁盤或網絡數據的異步加載可以參考Texture Streaming(爲何只有Texture做了Streaming就是因爲這玩意兒現在是遊戲最吃資源的了,十個模型的資源量不見得比得上一張貼圖啊),先把Object和少量基本信息當作佔位符加載進來,Object的實際數據則放到其他線程裏慢慢加載。如果您有類似需求,可以考慮這個方案。不過感覺是沒必要,比如我們遊戲常用的角色異步加載什麼的,其實走主線程時間片完全夠了。

 

編輯器中的資源會被標記爲Standalone,在無引用時仍然存在,其它還有一系列不會被GC的情況,需要注意。

發佈了10 篇原創文章 · 獲贊 66 · 訪問量 70萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章