為UE4++加入程式碼:初探UObject

前面幾個章節談了許多引擎在設計上的「為什麼」,但實際上我們到底該怎麼為專案加入C++類別?

基本上你可以純手動在${PROJECT_NAME}\Source\${PROJECT_NAME}下面新增.h跟.cpp檔,只不過會比較費力;但若所繼承的類別是屬於引擎預設支援的類別,你也可以選擇使用Editor中的工具。由於用Editor建立出來的程式碼會自動的套用所選擇繼承類別的模版,因此可以省下不少功夫。

UObject是整個UE4引擎中的核心類別,大部份引擎中的功能都是繼承這個類別後再開始發展各別的系統。

首先,讓我們先來看看該怎麼繼承這個類別。

點選左上角的File->New C++ Class後就會跳出視窗選擇欲繼承的類別,然後照著Figure 1.10.1、Figure 1.10.2、Figure 1.10.3的指示將類別建立出來。

Figure 1.10.1 在Editor中選擇File->New C++ Class後彈出的畫面,這裡只會出現常用類別,請接著選取右上方的Show All Classes。

Figure 1.10.2 選取右上角的Show All Classes之後,就會顯示所有引擎支援的模版類別,接著選取Object,按下Next。

Figure 1.10.3 選取成Public後,.h檔跟.cpp檔就會自動被放到Public跟Private中對應的資料夾。這裡我們在Public下建了一個叫Basic的資料夾,因此建出的MyBasicObject.h會被放到Public\Basic,而MyBasicObject.cpp則會被放到Private\Basic下面。

按下Create Class按鈕後,引擎就會根據內部設定好的Template幫我們將新的類別置放到所選擇的資料夾下面。對於引擎類別的Template有興趣的讀者可以查看${EngineVersion}\Engine\Content\Editor\Templates\這個資料夾下面的檔案,基本上就是根據這些檔案做字串置換的動作而已。

接著我們就可以在Source資料夾下找到建立出來的檔案,見Code 1.10.1。

UObject類別宣告
1. #pragma once

2. #include “UObject/NoExportTypes.h”

3. #include “MyBasicObject.generated.h”

4. UCLASS()

5. class HORIZONCLASSPROJECT_API UMyBasicObject : public UObject

6. {

7. GENERATED_BODY()

8. };

 

Code 1.10.1繼承自UObject的類別宣告,在UE4中只要是在UObject體系下的類別就能夠自動的享受到各種由引擎方提供的便利機制。

第1行:用來預防header重覆include。對於使用過C++的人可能會比較熟悉include guard,並且困惑為什麼引擎要採取#pragma once這個不屬於C++標準的作法。難道在跨平台編譯上不會有問題嗎?其實UE4所使用到的各平台編譯器,例如gcc、clang、跟微軟的msvc,目前也都已經非常穩定的支援該語法,因此引擎採用#pragma once這個比較簡潔的方案而不是include guard也是非常的合理設計。

第2行:包含引擎核心最少運作所需的基礎類別與數學相關的include。

第3行:UHT所產生的檔案,裡面用了大量的C++ macro定義了引擎內部使用的功能,例如:Reflection、GC、Serialization、HotReload、運算子重載(operator New、operator=)……等等。有興趣研究的讀者可以在下面的路徑找到定義的檔案:

${PROJECT_NAME}\Intermediate\Build\${PLATFORM}\UE4Editor\Inc\${MODULE_NAME}\${CLASS_NAME}.generated.h

裡面相關定義都會在下面的檔案中進行實作:

${PROJECT_NAME}\Intermediate\Build\${PLATFORM}\UE4Editor\Inc\${MODULE_NAME}\${ MODULE_NAME }.generated.cpp

第4行:給UHT用的標示語言,用來描述這個class在editor中該有的行為,例如宣告成UCLASS(Blueprintable)後我們就可以用blueprint繼承這個類別。所有的關鍵字可以在官方文檔中找到詳細功能說明,見下方連結:https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/Reference/Classes/

第5行:由於在windows中所有的module都會編譯成一個獨立的dll檔。這裡宣告${MODULE_NAME}_API的用義,就是用來將這個類別的放到dll的symbol table中,這樣引用這個module的人才不會產生link error。要特別注意的是${MODULE_NAME}_API中所有的字母都必須要是大寫。為什麼呢?進一步的往引擎底層追蹤,我們會發現每一個類別都會藉著UHT產生以下的define:

#define HORIZONCLASSPROJECT_API DLLEXPORT

DLLEXPORT則是由引擎各平台分別定義,在Windows中如下:

#define DLLEXPORT __declspec(dllexport)

另外值得注意的是,跟windows編譯成dynamic library(dll)的方法不同,在mac跟iOS中所有的module都會編譯成static library,並將DLLEXPORT設成空定義。

為什麼UE4要特別大費周章的更改編譯的方式呢?維持編譯方式的一致不是很好嗎?

原因其實並不在於任何技術上的議題,而是Apple規範所有上架到appstore的應用,其程式運行邏輯的部份不可以有能夠規避審查的機制,而dynamic library正是屬於能夠用動態下載的方式安裝或替換執行的程式碼,因此也不難理解引擎中會有相關處理的實作。

關於apple的規範,可以參見appstore review guidelines:

https://developer.apple.com/app-store/review/guidelines/

2.5.2 Apps should be self-contained in their bundles, and may not read or write data outside the designated container area, nor may they download, install, or execute code, including other iOS, watchOS, macOS, or tvOS apps.」

第7行:上面*generated.h只是宣告跟定義,GENERATED_BODY這個macro則是實際的將UHT所產生的那些便利功能放這個類別當中。然而在使用的過程,總是會看到有些範例程式會使用GENERATED_UCLASS_BODY。到底這2個不同的宣告有什麼不同?其實GENERATED_UCLASS_BODY是4.6版本之前使用的macro,在4.6發佈之後我們其實已經可以全部都使用GENERATED_BODY就行了。這2個宣告主要的差異點在於GENERATED_UCLASS_BODY強迫你去實作下面這個建構式:

UMyBasicObject::UMyBasicObject(const FObjectInitializer& ObjectInitializer)
Super(ObjectInitializer) {}

若是使用GENERATED_BODY這個版本就可以將建構式完全省略,若是需要建構式,則完全可以走標準的C++宣告方法。

Leave a Reply

Your email address will not be published. Required fields are marked *