《InsideUE4》UObject(三)類型系統設定和結構

垃圾分類,從我作起!html

引言

上篇咱們談到了爲什麼設計一個Object系統要從類型系統開始作起,並探討了C#的實現,以及C++中各類方案的對比,最後獲得的結論是UE採用UHT的方式蒐集並生成反射所需代碼。接下來咱們就應該開始着手設計真正的類型系統結構。
在以後的敘述中,我會同時用兩個視角來考察UE的這套Object系統:
一是以一個通用的遊戲引擎開發者角度來從零開始設計,設想咱們正在本身實現一套遊戲引擎(或者別的須要Object系統的框架),在體悟UE的Object系統的同時,思考哪些是真正的核心部分,哪些是後續的錦上添花。踏出一條重建Object系統的路來。
二是以當前UE4的現狀來考量。UE的Object系統從UE3時代就已經存在了(再遠的UE3有知道的前輩還望告知),歷經風雨,修修補補,又通過UE4的大改造,因此一些代碼讀起來非常詰屈聱牙,筆者也並不敢說通曉每一行代碼寫成那樣的起因,只能儘可能從UE的角度去思考這麼寫有什麼用意和做用。同時咱們也要記得UE是很博大精深沒錯,但並不表明每一行代碼都完美。總體結構上很優雅完善,但也一樣有不少小漏洞和缺陷,也並非全部的實現都是最優的。因此也支持讀者們在瞭解的基礎上進行源碼改造,符合本身自己的開發需求。c++

PS:類型系統不可避免的談到UHT(Unreal Header Tool,一個分析源碼標記並生成代碼的工具),但本專題不會詳細敘述UHT的具體工做流程和原理,只假定它萬事如我心意,UHT的具體分析後續會有特定章節討論。express

設定

先假定咱們已經接受了UE的設定:
在c++寫的class(struct同樣,只是默認public而已)的頭上加宏標記,在其成員變量和成員函數也一樣加上宏標記,大概就是相似C#Attribute的語法。在宏的參數能夠按照咱們自定的語法寫上內容。在UE裏咱們就能夠看到這些宏標記:數組

#define UPROPERTY(...)
#define UFUNCTION(...)
#define USTRUCT(...)
#define UMETA(...)
#define UPARAM(...)
#define UENUM(...)
#define UDELEGATE(...)
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#define UINTERFACE(...) UCLASS()

真正編譯的時候,大致上都是一些空宏。UCLASS有些特殊,通常狀況下最後也都是空宏,另一些狀況下會生成一些特定的事件參數聲明等等。不過這暫時跟本文的重點無關。這裏重點有兩點,一是咱們能夠經過給類、枚舉、屬性、函數加上特定的宏來標記更多的元數據;二是在有必要的時候這些標記宏甚至也能夠安插進生成的代碼來合成編譯。
咱們也暫時不用管UHT到底應該怎麼實現,就也先假定有那麼一個工具會在每次編譯前掃描咱們的代碼,獲知那些標記宏的位置和內容,並緊接着分析下一行代碼的聲明含義,最後生成咱們所須要的代碼。
還有兩個小問題是:微信

爲什麼是生成代碼而不是數據文件?
畢竟C++平臺和C#平臺不同,同時在引用1裏的UnrealPropertySystem(Reflection)裏也提到了最重要的區分之處:網絡

One of the major benefits of storing the reflection data as generated C++ code is that it is guaranteed to be in sync with the binary. You can never load stale or out of date reflection data since it’s compiled in with the rest of the engine code, and it computes member offsets/etc… at startup using C++ expressions, rather than trying to reverse engineer the packing behavior of a particular platform/compiler/optimization combo. UHT is also built as a standalone program that doesn’t consume any generated headers, so it avoids the chicken-and-egg issues that were a common complaint with the script compiler in UE3.架構

簡單來講就是避免了不一致性,不然又得有機制去保證數據文件和代碼能匹配上。同時跨平臺需求也很難保證結構間的偏移在各個平臺編譯器優化的不一樣致使得差別。因此還不如簡單生成代碼文件一塊兒編譯進去得了。框架

若是標記應該分析哪一個文件?
既然是C++了,那麼生成的代碼天然也差很少是.h.cpp的組合。假設咱們爲類A生成了A.generated.h和A.generated.cpp(按照UE習俗,名字無所謂)。此時A.h通常也都須要Include "A.generated.h",好比類A的宏標記生成的代碼若是想跟A.generated.h裏咱們生成的代碼來個內外夾攻的話。另外一方面,用戶對背後的代碼生成應該是保持最小驚訝的,用戶寫下了A.h,他在使用的時候天然也會想include "A.h",因此這個時候咱們的A.generated.h就得找個方式一塊兒安插進來,最方便的方式莫過於直接讓A.h include A.generated.h了。那既然每一個須要分析的文件最後都會include這麼一個*.generated.h,那天然就能夠把它自己就看成一種標記了。因此UE目前的方案是每一個要分析的文件加上該Include而且規定只能看成最後一個include,由於他也擔憂會有各類宏定義順序產生的問題。編輯器

#include "FileName.generated.h"

若是你一開始想的是給每一個文件也標記個空宏,其實倒也無不可,只不過沒有UE這麼簡潔。可是好比說你想控制你的代碼分析工具在分析某個特定文件的時候專門定製化一些邏輯,那這種像是C#裏AssemblyAttribute的文件宏標記就顯示出做用了。UHT目前不須要因此沒作罷了。ide

結構

在接受了設定以後,是否是以爲原本這個寫法有點怪的Hello類看起來也有點可愛呢?

#include "Hello.generated.h"
UClass()
class Hello
{
public:
    UPROPERTY()
    int Count;
    UFUNCTION()
    void Say();
};

先什麼都無論,僞裝UHT已經爲咱們蒐集了完善的信息,而後這些信息在代碼裏應該怎麼儲存?這就要談到一些基本的程序結構了。一個程序,簡單來講,能夠認爲是由衆多的類型和函數嵌套組成的,類型有基礎類型,枚舉,類;類裏面可以再定義字段和函數,甚至是子類型;函數有輸入和輸出,其內部也依然能夠定義子類型。這是C++的規則,但你在支持的時候就能夠在上面進行縮減,好比你就能夠不支持函數內定義的類型。
先來看看UE裏造成的結構:
UFieldAndChildren2.jpg-35.9kB
C++有聲明和定義之分,圖中黃色的的均可以看做是聲明,而綠色的UProperty能夠看做是字段的定義。在聲明裏,咱們也能夠把類型分爲可聚合其餘成員的類型和「原子」類型。

  • 聚合類型(UStruct):
    • UFunction,只可包含屬性做爲函數的輸入輸出參數
    • UScriptStruct,只可包含屬性,能夠理解爲C++中的POD struct,在UE裏,你能夠看做是一種「輕量」UObject,擁有和UObject同樣的反射支持,序列化,複製等。可是和普通UObject不一樣的是,其不受GC控制,你須要本身控制內存分配和釋放。
    • UClass,可包含屬性和函數,是咱們日常接觸到最多的類型
  • 原子類型:
    • UEnum,支持普通的枚舉和enum class。
    • int,FString等基礎類型不必特別聲明,由於能夠簡單的枚舉出來,能夠經過不一樣的UProperty子類來支持。

把聚合類型們統一塊兒來,就造成了UStruct基類,能夠把一些通用的添加屬性等方法放在裏面,同時能夠實現繼承。UStruct這個名字確實比較容易引發歧義,由於實際上C++中USTRUCT宏生成了類型數據是用UScriptStruct來表示的。
還有個類型比較特殊,那就是接口,能夠繼承多個接口。跟C++中的虛類同樣,不一樣的是UE中的接口只能夠包含函數。通常來講,咱們本身定義的普通類要繼承於UObject,特殊一點,若是是想把這個類看成一個接口,則須要繼承於UInterface。可是記得,生成的類型數據依然用UClass存儲。從「#define UINTERFACE(...) UCLASS()」就能夠看出來,Interface其實就是一個特殊點的類。UClass裏經過保存一個TArray<FImplementedInterface> Interfaces數組,其子項又包含UClass* Class來支持查詢當前類實現了那些接口。

最後是定義,在UE裏是UProperty,能夠理解爲用一個類型定義個字段「type instance;」。UE有Property,其Property有子類,子類之多,一屏列不下。實際深刻代碼的話,會發現UProperty經過模板實例化出特別多的子類,簡單的如UBoolProperty、UStrProperty,複雜的如UMapProperty、UDelegateProperty、UObjectProperty。後續再一一展開。

元數據UMetaData其實就是個TMap<FName, FString>的鍵值對,用於爲編輯器提供分類、友好名字、提示等信息,最終發佈的時候不會包含此信息。

爲了加深一下概念,我列舉一些UE裏的用法,把圖和代碼加解釋一塊兒關聯起來理解的會更深入些:

#include "Hello.generated.h"
UENUM()
namespace ESearchCase
{
    enum Type
    {
        CaseSensitive,
        IgnoreCase,
    };
}

UENUM(BlueprintType)
enum class EMyEnum : uint8
{
    MY_Dance    UMETA(DisplayName = "Dance"),
    MY_Rain     UMETA(DisplayName = "Rain"),
    MY_Song     UMETA(DisplayName = "Song")
};

USTRUCT()
struct HELLO_API FMyStruct
{
    GENERATED_USTRUCT_BODY()
    
    UPROPERTY(BlueprintReadWrite)
    float Score;
};

UCLASS()
class HELLO_API UMyClass : public UObject
{
    GENERATED_BODY()
public:
    UPROPERTY(BlueprintReadWrite, Category = "Hello")
    float Score;

    UFUNCTION(BlueprintCallable, Category = "Hello")
    void CallableFuncTest();
    
    UFUNCTION(BlueprintCallable, Category = "Hello")
    void OutCallableFuncTest(float& outParam);

    UFUNCTION(BlueprintCallable, Category = "Hello")
    void RefCallableFuncTest(UPARAM(ref) float& refParam);

    UFUNCTION(BlueprintNativeEvent, Category = "Hello")
    void NativeFuncTest();

    UFUNCTION(BlueprintImplementableEvent, Category = "Hello")
    void ImplementableFuncTest();
};

UINTERFACE()
class UMyInterface : public UInterface
{
    GENERATED_UINTERFACE_BODY()
};

class IMyInterface
{
    GENERATED_IINTERFACE_BODY()

    UFUNCTION(BlueprintImplementableEvent)
    void BPFunc() const;

    virtual void SelfFunc() const {}
};

先不用去管宏裏面參數的含義,目前先造成大局的印象。可是注意,我這裏沒有提到藍圖裏能夠建立的枚舉、接口、結構、類等。它們也都是相應的從各自UEnum、UScriptStruct、UClass再派生出來。這個留待以後再講。讀者們須要明白的是,一旦咱們可以用數據來表達類型了,咱們就能夠自定義出不一樣的數據來動態建立出不一樣的其餘類型。

思考:爲何還須要基類UField?
UStruct好理解,表示聚合類型。那爲何不直接UProperty、UStruct、UEnum繼承於UObject?在筆者看來,主要有三點:

  1. 爲了統一全部的類型數據,若是全部的類型數據類都有個基類的話,那麼咱們就很容易用一個數組把全部的類型數據都引用起來,能夠方便的遍歷。另外也關乎到一個順序的問題,好比在類型A裏定義了P一、F一、P二、F2,屬性和函數交叉着定義,在生成類型A的類型數據UClass內部就也能夠是以一樣的順序,之後要是想回溯出來一份定義,也能夠跟原始的代碼順序一致,若是是用屬性和函數分開保存的話,就會麻煩一些。
  2. 如上圖可見,全部的無論是聲明仍是定義(UProperty、UStruct、UEnum),均可以附加一份額外元數據UMetaData,因此應該在它們的基類裏保存。
  3. 方便添加一些額外的方法,好比加個Print方法打印出各個字段的聲明,就能夠在UField里加上虛方法,而後在子類裏重載實現。

UField名字顧名思義,就是無論是聲明仍是定義,均可以看做是類型系統裏的一個字段,或者叫領域也行,術語不一樣,但能理解到一個更抽象統一的意思就行。

思考:爲何UField要繼承於UObject?
這問題,其實也是在問,爲何類型數據也要一樣繼承於UObject?反過來問,若是不繼承會怎麼樣?把繼承鏈斷開,類型數據自成一派,其實也何嘗不可。咱們來列舉一下UObject身上有哪些功能,看看哪些是類型系統所須要的。

  • GC,無關緊要,類型數據一開始分配了就能夠不釋放,當前GC也是利用了類型系統來支持對象引用遍歷
  • 反射,略
  • 編輯器集成,也能夠沒有,編輯器就是利用類型數據來進行集成編輯的,固然當咱們在藍圖裏建立函數變量等操做其實也能夠看做就是在編輯類型數據。
  • CDO,不須要,每一個類型的類型數據通常只有一份,CDO是用在對象身上的
  • 序列化,必須有,類型數據固然須要保存下來,好比藍圖建立的類型。
  • Replicate,用處不大,由於目前網絡間的複製也是利用了類型數據來進行的,類型數據自己的字段的改變複製想來好像沒有什麼應用場景
  • RPC,也無所謂
  • 自動屬性更新,也不須要,類型數據通常不會那麼頻繁變更
  • 統計,無關緊要

總結下來,發現序列化是最重要的功能,GC和其餘一些功能算是錦上添花。因此歸結起來無關緊要再加上一些必要功能,本着統一的思想,就讓全部類型數據也都繼承於UObject了,這樣序列化等操做也不須要寫兩套。雖然這看起來不是那麼的純粹,可是整體上來講利大於弊。
在對象上,你能夠用Instance->GetClass()來得到UClass對象,在UClass自己上調用GetClass()返回的是本身自己,這樣能夠用來區分對象和類型數據。

總結

UE的這套類型數據組織架構,以我目前的瞭解和知識,私覺得優雅程度有80/100分。大致上可用,沒什麼問題,從UE3時代修修改改過來,我以爲已經很不容易了。只是不少地方從技術角度上來講,不是那麼的純粹,好比接口的類型數據也依然是UClass,可是卻又不容許包含屬性,這個從結構上就沒有作限制,只能經過UHT檢查和代碼中類型判斷來區分;又好比UStruct裏包含的是UField鏈表,其實隱含的意思就是UStruct裏既能夠有嵌套類型又能夠有屬性,靈活的同時也少了限制,嵌套類型目前是沒有了,可是UFunction也只能包含屬性,UScriptStruct只有屬性而不能有函數;還有UStruct裏用UStruct* SuperStruct指向繼承的基類。可是UFunction的基Function是什麼意義?因此以後若有含糊之時,讀者朋友們能夠用下面這個圖結構來清醒一下:
UFieldAndChildren.jpg-34.9kB
能夠簡單理解這就是UE想表達的真正含義。UMetaData雖然在UPackage裏用TMap<UObject*,TMap<FName, FString>>來映射,可是實際上也只有UField裏有GetMetaData的接口,因此通常UMetaData也只是跟UField關聯罷了。UStruct包含UProperty,UClass和UScriptStruct又包含UFunction,這纔是通常實操時用到的數據關聯。

含糊之處固然無傷大雅,只不過若是讀者做爲一個通用引擎研究開發者而言,也要認識到UE的系統的不足之處,不可一一照抄。讀者若是本身想要實現的話,左右有兩種方向,一種是向着類型單一,可是更多用邏輯來控制,好比C#的類型系統,一個Type之下能夠得到各類FieldInfo、MethodInfo等;另外一種是向着類型細分,用結構來限制,好比增長UScriptInterface來表達Interface的元數據,把包含屬性和函數的功能封裝成PropertyMap和FunctionMap,而後讓UScriptStruct、UFunction、UClass擁有PropertyMap,讓UClass,UScriptInterface擁有FunctionMap。都有各自的利弊和靈活度不一樣,這裏就不展開一一細說了,讀者們能夠本身思考權衡。
咱們當前更關注是如何理解UE這套類型系統(也叫屬性系統,爲了和圖形裏的反射做區分),因此下篇咱們將繼續深刻,瞭解UE裏怎麼開始開始構建這個結構。

上篇:類型系統概述

引用

  1. UnrealPropertySystem(Reflection)
  2. 虛幻4屬性系統(反射)翻譯 By 風戀殘雪
  3. Classes
  4. Interfaces
  5. Functions
  6. Properties
  7. Structs

UE4.14.2


知乎專欄:InsideUE4
UE4深刻學習QQ羣:456247757(非新手入門羣,請先學習完官方文檔和視頻教程)
微信公衆號:aboutue,關於UE的一切新聞資訊、技巧問答、文章發佈,歡迎關注。
我的原創,未經受權,謝絕轉載!

相關文章
相關標籤/搜索