關於UE4++、GC與標準庫的一些雜談

posted in: UnrealEngine | 1

  從上一節 (http://dorgon.horizon-studio.net/archives/773) 的內容我們知道官方堅持不將Script導入引擎的原因,但對於稍微進階一點的程序員可能就會開始思考:既然都使用C++了,為什麼還要在引擎中實作Garbage Collection(GC)?直接使用RAII的機制不是能夠更進一步的避掉因為GC而產生的效能耗損? 先撇開效能問題,其實導入GC的優點很明顯:它幫助我們從複雜的記憶體管理議題中逃脫出來,進而能夠專心在遊戲邏輯的撰寫上。C++雖然是一個非常強大的語言,但反過來說,卻也是非常的複雜。 例如上面所提到的RAII,一般最常使用smart pointer的概念來實現。若沒有經過特別的訓練的話,則很有可能就會迷失在shared pointer跟weak pointer的使用情境之中。更糟的是,若使用者打從一開始就不知道weak pointer的概念,則有很大的可能會寫出物件間循環參照的邏輯,進而造成物件的記憶體永遠不會被釋放。 因此,並不是所有的使用者都能夠正確掌握語言的特性,為了理解並正確使用這些功能,是需要好幾年的經驗與養成才有辦法好好的揮舞這把強力武器。 而UE4為了將C++複雜的特性從一般使用者中隔離出來,所採用的並不是跟其他大部份的遊戲引擎一樣導入Script創造一個Sandbox環境的方法。相反的,它在原本的C++語法之上更進一步的設計了一套Reflection(UProperty)跟Garbage Collection(GC)的機制。 雖然導入GC會影響效能表現,但相對的C++整體在使用上的複雜度也降低到跟其他高階語言差不多的程度。而且,由於UE所實作的這套GC機制是為了遊戲這類即時互動性高的系統所設計的,因此裡面的GC機制不僅有許多回收的優化機制(見Figure 18),而且其採用的「漸進式」方法不會在GC發動時讓整個遊戲卡住,意即,每次進行回收的動作不會超過所設定的秒數(預設為0.002秒)。   Figure 18 GC預設設定,於Project Setting->Engine->Garbage Collection選項。其中在Optimization中的Create Garbage Collector UObject Clusters這個選項,在目前的版本(4.15)只支援Material跟Particle。 大部份的應用在這套系統下面其實不會有什麼大問題。 當然,C++裡面一個非常重要的設計哲學:不要為不需要的功能付出效能上的代價 (Stroustrup, March 1994)。對於更進階的效能需求,其實我們還是可以使用原本標準庫中所有的C++方法。當我們的類別不是繼承自UObject體系時,是可以使用自己的記憶體管理機制。基本上,UE4++包含了所有C++的語法特性,然後再加上一些給UHT自動產生reflection資訊的標記語言。在4.15版本釋出時,引擎更是在全平台支援C++14所有語法的特性。 只是當我們開始深入研究相關的方法的時候,會發現:為什麼引擎中實作了許多跟標準C++相同概念的方法?例如TArray對應到std::vector;TMap對應到了std::map。為什麼不使用標準庫中的方法而要自己實作一套呢?難不成UE4自己實作的方法有比標準庫中的還要快嗎? 其實,速度並不是主要的考量。 根據官方於論壇上的說法,歷史因素才是造成目前設計的主因。要知道,UE4是一個已經存在好幾十年的引擎,在第一代引擎釋出的那個年代其實也正是 C++發佈第一套標準 (C++98)。由於標準剛發佈時的各方實作並不是很穩定,各個不同平台下的行為模式還不是很一致。為了保證穩定性,當時的引擎有了自己的一套實現,並且一直延用並演化到現在。 由於現在C++標準庫已經非常的穩定,官方是有在思考將引擎中對應的方法全部替換成標準庫相關實作的可能性。只是,這部份的變更目前並不考慮在第四世代的引擎中實現。或許在「第五世代(UE5)」釋出的時候,我們就可以看到引擎跟標準庫有著緊密的結合。   本系列文章為個人原創,未經授權,謝絕轉載

[UE4]關於C++、Blueprint與Script的一些雜談

posted in: UnrealEngine | 2

我們知道,除了使用Blueprint之外,還可以選擇使用C++來進行開發。然而,UE4中的C++有著許多跟標準C++不太一樣的規則。這是由於引擎為了減少使用C++編程時的門檻,因此在底層實作了一套Reflection(UProperty)跟Garbage Collection(GC)的機制。只要照著UE4的編程規則進行C++類別的設計,不僅可以減少各種資源管理上的議題,還能夠自動享受到由引擎方所提供的許多便利功能。 或許有人會有疑問:C#或JAVA也有Reflection跟GC,為什麼UE4還要自己用C++實作一套?直接跟Unity一樣用C#當成Script來撰寫遊戲邏輯部份不就好了嗎? 其實UE4++跟.NET C#在機制上有著根本上的不同。前者是在執行編譯之前,會先使用Unreal Header Tool(UHT)產生相關需要的資訊,然後用著同一套C++ Compiler將這些產生出來的Code編譯成可以執行的Native Code;而C#則是先編譯成Bytecode,在.NET中稱為Common Intermediate Language(CIL),然後再實際安裝到目標機器或第一次呼叫相關方法時,呼叫Just-In-Time(JIT) Compiler將Bytecode編譯成Native Code執行。 UE4的方法可以想見,其執行速度會比較快,但由於最後的執行檔內包含了所有的Reflection資訊,因此檔案會變大;而使用Bytecode的方法,我們所損失的就只有第一次執行程式時的速度,以及一些可以使用在C++中的優化技巧。 其實在UE4釋出之前的幾代引擎,是有自己的一套UnrealScript來當成遊戲邏輯的編程使用,其第一代遊戲引擎更是在1998年就已經釋出。因此我們的問題應該要換成:為什麼要將UnrealScript從四代的移除?雖然使用Script會需要以執行期的效能為代價,但先編成Bytecode的方式,不僅可以避免因為C++語言的複雜性而造成新手進入的障礙,而且還能讓開發者減少許多等待C++編譯的時間。若真的非常需要效能應用,還是有技術能將這些Script轉譯成C++後再編譯。 相較之下,似乎沒有特別的理由堅持使用C++來進行開發,不是嗎? 是的,這也是為何官方將三代中的Kismet這套視覺編程系統進行強化,重新命名為Blueprint後,強勢回歸到四代引擎的原因。對於完全不懂程式的使用者,其實可以完全使用這套系統建構出一個遊戲。 這套強化過後的視覺化Script系統就是官方所給出的解答。 只是完全使用Blueprint來寫程式還是有不少的限制,尤其視覺編程系統實在是不適合用來撰寫複雜的邏輯。而且在多人協作上,由於寫出的檔案為Binary的格式,目前也沒辦法使用git進行merge。 基於以上限制,我們還是需要一個完整的純文字格式的Script語言給進階使用者不是嗎?不管是直譯式語言或是編譯式語言,將C++的複雜性從開發流程中隔離出來,對開發不是會比較有效率嗎? 事實上,在引擎底層是有開放接口,讓使用者自己綁定其他想使用的Script語言。而且目前在markplace上也可以找到Unreal.js這套免費的Plugin將V8 JavaScript Engine跟引擎進行綁定。其他如python、lua、C#……等等,也都有非官方的實現。 但問題是,為什麼官方堅持不在引擎層提供一套正式的純文字格式Script語言支持?根據官方於論壇上的回答,主要是因為以下幾點原因: 在引擎過去幾十年來的演進中,隨著使用人數的增加,有越來越多的使用者要求將C++中的某個特性開放給Script使用。而不斷開放這些進階功能的後果,這層為了減少複雜性而封裝起來的Sandbox環境看起來就跟C++中的宣告沒二樣。這時候再透過一層Script介面層的封裝,反而增加了整體理解的複雜度。 隨著Script介面層的擴張,其在跟C++層溝通的成本跟複雜度會變的非常大。又尤其要將一些比較複雜的資料型態互相傳輸時,例如Container,在Script跟C++的語意跟語法都不太相同的情況下,肯定會造成不少的維護成本。 當開發者在尋求一些更進階的功能時,勢必要將程式的邏輯分割成「Script部份」跟「C++部份」,而開發時間就在雙方的呼叫邏輯撰寫地獄中損失掉了。 當開發者需要進行斷點做程式的追蹤時,馬上就會發現script的debug工具跟C++使用的debug工具是完全的二回事。若你沒辦法直接從script層追到C++層的話,那麼在做除錯時就會變的非常的困難。(反之亦然) 從上面所列出的原因中我們可以發現,不支援Script有很大的原因,都是由於C++跟Script之間的互相操作性(Interoperability)在引擎演化到最後完全失控了。回歸純粹的C++架構,不僅可以解決引擎維護與除錯上的痛點,而且附加好處,則是效能上的增進與簡化跟第三方C++ Library的整合。 這個決策或許有些人並不認同,但至少我們可以看出官方目前對內建其他script上的態度。     本系列文章為個人原創,未經授權,謝絕轉載