UnrealEngine - 反射系統分析

lawliet9發表於2023-03-30

1. 反射

什麼是反射?或者說反射能做什麼,簡單來說,反射可以提供一種能力,能夠在執行時動態獲取物件的成員資訊,如成員函式成員變數
UE 在其反射系統上支援了許多功能,如:

  • 編輯器中可供編輯的屬性
  • GC
  • 序列化
  • 網路同步

1.1 使用反射的準備工作

UE 中應用反射需要與它定義的宏相結合,主要有 3 種型別,如下所示:

  • 類註冊
#include "Weapon.generated.h" // 包含自動生成的標頭檔案資訊
UCLASS() // 註冊類資訊
class AWeapon : public AActor {
	GENERATED_BODY() // 生成類輔助程式碼
public:
	UPROPERTY()  // 註冊類屬性
	FName WeaponName;

	UFUNCTION() // 註冊類成員函式
	void Fire();
}
  • 結構體註冊(需要注意的是,UFUNCTION 只能在 Class 中使用)
#include "Weapon.generated.h" // 包含自動生成的標頭檔案資訊
USTRUCT() // 註冊結構體
struct FWeapon {
	UPROPERTY() // 註冊結構體屬性
	FName WeaponName;
}
  • 列舉註冊
#include "Weapon.generated.h" // 包含自動生成的標頭檔案資訊
UENUM() // 註冊列舉資訊
enum WeaponType {
	Short,
	Middle,
	Far,
}

1.2 反射的簡單應用

前面註冊完畢反射後,就能簡單的使用反射了,如下:

#include "human.generated.h" // 包含自動生成的標頭檔案資訊
/** UHuman.h **/
class UHuman {
public:
	UPROPERTY()
	FString Name = "Hello, Reflection!!!";
	UPROPERTY()
	UHuman* Child;
}

UHuman* Human = NewObject<UHuman>();
UClass* UCHuman = UHuman::StaticClass();
// 轉為對應的Property
if (FStrProperty* StrProperty = CastField<FStrProperty>(Property))
{
    // 取Property地址(因為屬性系統知道屬性在類記憶體中的偏移值)
    void* PropertyAddr = StrProperty->ContainerPtrToValuePtr<void>(Human);
    // 透過地址取值(其實就是型別轉換,畢竟我們都拿到記憶體地址了)
    FString PropertyValue = StrProperty->GetPropertyValue(PropertyAddr);
    UE_LOG(LogTemp, Warning, TEXT("Property's Value is %s"), *PropertyValue);
}

但是這種使用只是最粗淺的使用,更多時候反射的應用對我們來說是無感知的,如網路同步,編輯器的屬性編輯等,都是建立在反射系統之上的,反射系統更多是一個基層系統,輔助構建其他高層次的系統。

2. 反射整體結構

UE 的反射系統其整體的結構如下:

總體來說,其各種結構對應收集不同型別的反射資訊:

  • UClass :收集類資料,描述一個類的成員變數,函式,父類等資訊
  • UEnum:收集列舉資料
  • UScriptStruct :收集結構體資料
  • UFunction:收集函式資訊
    以 UClass 為例,其採用 FProperty 來儲存所有的簡單屬性資訊(如 BoolInt),而一些複合型別資料則使用 UField 儲存(如 AActorTArray)。這裡需要認識到:UClass 等反射結構其本質上只是描述一個類的結構,本身與業務類無實際耦合關係,每個標記了 UCLASS(...) 宏的 class 都會有一個 UClass* Object 儲存其反射資訊。

3. 構建流程

從寫程式碼的角度來說,我們只需要對變數,類等定義標註一個 宏,再 include 一個標頭檔案就完事了,具體構建的過程則是由 UE 的編譯工具去完成的。也就是 Unreal Build ToolUBT) 和 Unreal Header ToolUHT)。
接下來以前面的 class AWeapon 為例,展示其自動生成的內容和如何初始化其反射資訊。

[!note]
UHT 是一個用於預處理原始碼檔案的工具,它可以識別 UCLASSUFUNCTION 等宏,並透過生成額外的 C++ 程式碼來擴充套件類的功能。UHT 還可以用於生成反射資訊,例如類的後設資料和屬性資訊,以便在執行時進行藍圖互動等操作。

UBT 是一個用於編譯和連結 UE4 專案的構建系統。它可以自動管理專案中的依賴項,並生成可執行檔案和動態連結庫等二進位制檔案。UBT 還可以執行諸如打包、部署和測試等其他任務。

兩個工具在 UE4 開發中密切相關,因為 UHT 生成的反射資訊需要在 UBT 中使用,以便生成最終的可執行檔案和動態連結庫。因此,在構建 UE4 專案時,UBT 將首先呼叫 UHT 來處理原始碼檔案,然後使用生成的程式碼來編譯和連結專案。

3.1 自動生成檔案

在 [[原理#^644683|1.1 使用反射的準備工作]] 中,主要工作分為兩步:

  • 標註宏資訊(如 UCLASSUFUNCTIONUPROPERTY
  • 包含標頭檔案 #include ${filename}.generated.h
    這裡標頭檔案是利用 UHT 工具掃描生成的,其附帶還會生成一個 ${filename}.gen.cpp 的原始檔。這兩個檔案主要負責兩件事情:
  1. 定義一個或多個輔助類(根據 UCLASSUSTRUCT 等標註的結構數量),收集標註了宏資訊的結構,該輔助類建構函式會返回一個構造好的 UClass
  2. 定義一個 FCompileDeferInfo 靜態變數,其建構函式會在啟動時將輔助類的資訊匯入到一個全域性的容器中,啟動時會遍歷這個容器,構建好 UClass 等反射資訊。
    其大致流程如下:

3.2 預生成程式碼

接下來分析預先生成的 generated.h 和 gen.cpp 都做了什麼事情
一個 Class 需要註冊反射資訊時,其使用方式如下(有一個必要的前提條件為該 Class 的繼承鏈中需要有 UObject):

#include "Weapon.generated.h" // 包含自動生成的標頭檔案資訊
UCLASS() // 註冊類資訊
class AWeapon : public AActor {
	GENERATED_BODY() // 生成類輔助程式碼
public:
	UPROPERTY()  // 註冊類屬性
	FName WeaponName;

	UFUNCTION() // 註冊類成員函式
	void Fire();
}

可以看到其相關的宏主要有如下幾個:

  • UCLASS
  • GENERATED_BODY
  • UPROPERTY
  • UFUNCTION
    這裡首先需要了解這些宏背後都做了什麼

3.2.1 宏展開

關鍵的宏定義如下:

/* 將 ABCD 4 個名稱連結起來*/
#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D
#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)

/* 拼接成另一個宏 */
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);

/* 純標記,用給 UHT 掃描 */
#define UPROPERTY(...)  
#define UFUNCTION(...)

以 3.2 的示例為例,展開後內容大致如下:

UdemyProject_Source_UdemyProject_AWeapon_h_3_PROLOG // 註冊類資訊
class AWeapon : public AActor {
	UdemyProject_Source_UdemyProject_AWeapon_h_5_GENERATED_BODY // 生成類輔助程式碼
public:
	UPROPERTY()  // 由於是標記,這裡展開之後是沒有特殊資訊的
	FName WeaponName;

	UFUNCTION() // 由於是標記,這裡展開之後是沒有特殊資訊的
	void Fire();
}

可以看到展開後是一個個神秘的符號,其實這都是宏的名稱,其定義在自動生成的 generated.h 檔案中。
這裡展示了一個特點,儘管不同的類都使用的相同的宏,但是 UHT 還是能保證掃描生成的檔案資訊唯一性。
這裡主要關注兩個宏:

  • GENERATED_BODY_LEGACY
  • GENERATED_BODY
    接著展示一下兩個宏其對應的檔案資訊。
#define UdemyProject_Source_UdemyProject_AWeapon_h_3_PROLOG  
#define UdemyProject_Source_UdemyProject_AWeapon_h_5_GENERATED_BODY_LEGACY \  
PRAGMA_DISABLE_DEPRECATION_WARNINGS \  
public: \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_PRIVATE_PROPERTY_OFFSET \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_SPARSE_DATA \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_RPC_WRAPPERS \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_INCLASS \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_STANDARD_CONSTRUCTORS \  
public: \

#define UdemyProject_Source_UdemyProject_AWeapon_h_5_GENERATED_BODY \  
PRAGMA_DISABLE_DEPRECATION_WARNINGS \  
public: \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_PRIVATE_PROPERTY_OFFSET \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_SPARSE_DATA \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_RPC_WRAPPERS_NO_PURE_DECLS \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_INCLASS_NO_PURE_DECLS \  
   UdemyProject_Source_UdemyProject_AWeapon_h_5_ENHANCED_CONSTRUCTORS \  
private: \

可以看到 GENERATED_BODY_LEGACYGENERATED_BODY 的內容基本一致,查閱資料發現這主要是為了前向相容。因此可以先忽略 GENERATED_BODY_LEGACY 內容,關注 GENERATED_BODY 的內容。
可以看到 GENERATED_BODY 又巢狀了一堆宏(宏的定義在自動生成的 generated.h 標頭檔案),其展開之後才是真正的程式碼,比如

/* UFUNCTION Wrapper 函式 */
#define UdemyProject_Source_UdemyProject_AWeapon_h_5_RPC_WRAPPERS_NO_PURE_DECLS \   
DECLARE_FUNCTION(execFire);

可以對其完整展開,還原其最終的樣貌

/* 該宏可以忽略 */
UdemyProject_Source_UdemyProject_AWeapon_h_5_GENERATED_BODY_LEGACY  
class AWeapon : public AActor {
public:
	/* 
	UdemyProject_Source_UdemyProject_AWeapon_h_5_RPC_WRAPPERS_NO_PURE_DECLS
	UFunction 的 Wrapper Function 集合
	*/
	static void execFire( UObject* Context, FFrame& Stack, RESULT_DECL );
private: 
	static void StaticRegisterNativesAWeapon(); 
	friend struct Z_Construct_UClass_AWeapon_Statics; 
public: 
	/* 
	DECLARE_CLASS(AWeapon, AActor, COMPILED_IN_FLAGS(0 | CLASS_Config), CASTCLASS_None, TEXT("/Script/UdemyProject"), NO_API) 
	類輔助定義相關 
	*/
private: \  
    AWeapon& operator=(AWeapon&&);   \  
    AWeapon& operator=(const AWeapon&);   \  
   TRequiredAPI static UClass* GetPrivateStaticClass(); \  
public: \  
   /** Bitwise union of #EClassFlags pertaining to this class.*/ \  
   enum {StaticClassFlags=COMPILED_IN_FLAGS(0 | CLASS_Config}; \  
   /** Typedef for the base class ({{ typedef-type }}) */ \  
   typedef AActor Super;\  
   /** Typedef for {{ typedef-type }}. */ \  
   typedef AWeapon ThisClass;\  
   /** Returns a UClass object representing this class at runtime */ \  
   inline static UClass* StaticClass() \  
   { \  
      return GetPrivateStaticClass(); \  
   } \  
   /** Returns the package this class belongs in */ \  
   inline static const TCHAR* StaticPackage() \  
   { \  
      return TEXT("/Script/UdemyProject"); \  
   } \  
   /** Returns the static cast flags for this class */ \  
   inline static EClassCastFlags StaticClassCastFlags() \  
   { \  
      return CASTCLASS_None; 
   } 
   /** For internal use only; use StaticConstructObject() to create new objects. */ 
   inline void* operator new(const size_t InSize, EInternal InInternalOnly, UObject* InOuter = (UObject*)GetTransientPackage(), FName InName = NAME_None, EObjectFlags InSetFlags = RF_NoFlags) 
   { 
      return StaticAllocateObject(StaticClass(), InOuter, InName, InSetFlags); 
   } 
   /** For internal use only; use StaticConstructObject() to create new objects. */ 
   inline void* operator new( const size_t InSize, EInternal* InMem ) \  
   { 
      return (void*)InMem; 
   }

	/* 序列化相關 */
	friend FArchive &operator<<( FArchive& Ar, AWeapon*& Res ) 
	{ 
	   return Ar << (UObject*&)Res; 
	}
	friend void operator<<(FStructuredArchive::FSlot InSlot, AWeapon*& Res) \  
	{ 
	   InSlot << (UObject*&)Res;
	}

	/* 建構函式相關 */
    /** Standard constructor, called after all reflected properties have been initialized */ 
   NO_API AWeapon(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; 
private: 
   /** Private move- and copy-constructors, should never be used */ 
   NO_API AWeapon(AWeapon&&);
   NO_API AWeapon(const AWeapon&); 
public: 
	/* 預設建構函式 */
	NO_API AWeapon(FVTableHelper& Helper);
	static UObject* __VTableCtorCaller(FVTableHelper& Helper)
	{
	   return new (EC_InternalUseOnlyConstructor, (UObject*)GetTransientPackage(), NAME_None, RF_NeedLoad | RF_ClassDefaultObject | RF_TagGarbageTemp) AWeapon(Helper);
	}
   static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())AWeapon(X); }

public:
	UPROPERTY()  // 註冊類屬性
	FName WeaponName;

	UFUNCTION() // 註冊類成員函式
	void Fire();
}

可以看到 GENERATED_BODY 宏為 AWeapon 擴充套件了很多功能,包括但不限於:

  • 增加了建構函式
  • 增加了序列化功能
  • UFunction 增加 Wrapper Function 以供呼叫
  • 增加獲取當前父類以及當前類的 UClass 功能

3.2.2 gen.cpp 內容分析

gen.cpp 的內容主要為構建好描述 AWeapon 反射資訊的 UClass

UFUNCTION 相關程式碼

首先以 AWeapon::Fire 為例,對其標記 UFUNCTION 後檢查其生成的相關內容大致如下:

  1. 實現 Wrapper Function 的內容,這個介面主要供 藍圖 或者 RPC 使用。
DEFINE_FUNCTION(AWeapon::execFire)  
{  
   P_FINISH;  
   P_NATIVE_BEGIN;  
   P_THIS->Fire();  // 實際上就是呼叫了下 Navtive 的 Fire 函式
   P_NATIVE_END;  
}
  1. 生成一個結構體 FFunctionParams,儲存構建 UFunction 所需的引數,並提供構建 UFunction 的方法,引數內容主要分為:
  • 函式的標記(比如標記為 Server 或者 Client 等)
  • 函式的名稱
  • 函式的引數和返回值(其統一用一個 List 儲存,每個元素會有一個 Flag 標記其是引用還是返回值還是普通引數)
  • 引數的數量
    /* 定義一個結構體,引數為構建一個 UFunction 所需要的引數 */
    struct Z_Construct_UFunction_AWeapon_Fire_Statics  
   {  
      static const UE4CodeGen_Private::FFunctionParams FuncParams;  
   };
   /* 初始化一個結構體 */
   const UE4CodeGen_Private::FFunctionParams Z_Construct_UFunction_AWeapon_Fire_Statics::FuncParams = 
   { (UObject*(*)())Z_Construct_UClass_AWeapon,
    nullptr,
    "Fire",
    nullptr,
    nullptr,
    0,
    nullptr,
    0,
    RF_Public|RF_Transient|RF_MarkAsNative,
	(EFunctionFlags)0x00020401,
	0,
	0, METADATA_PARAMS(Z_Construct_UFunction_AWeapon_Fire_Statics::Function_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UFunction_AWeapon_Fire_Statics::Function_MetaDataParams)) };  
	/* 生成一個構造方法,用來構造 AWeapon::Fire 的 UFunction 資訊 */
	UFunction* Z_Construct_UFunction_AWeapon_Fire()  
	{  
	   static UFunction* ReturnFunction = nullptr;  
	   if (!ReturnFunction)  
	   {      UE4CodeGen_Private::ConstructUFunction(ReturnFunction, Z_Construct_UFunction_AWeapon_Fire_Statics::FuncParams);  
	   }   return ReturnFunction;  
	}

UPROPERTY 相關程式碼

類似生成 UFunction,此處由於 WeaponName 是基礎型別,所以直接初始化一個 FNamePropertyParams 的結構體。
這裡面就包含了:

  • 變數的名稱
  • 變數的 Flag(比如標記為 Replicated)
  • 變數的偏移(方便從類指標從偏移獲取該變數)
const UE4CodeGen_Private::FNamePropertyParams Z_Construct_UClass_AWeapon_Statics::NewProp_WeaponName = { 
"WeaponName",
nullptr,
(EPropertyFlags)0x0010000000000000,
UE4CodeGen_Private::EPropertyGenFlags::Name,
RF_Public|RF_Transient|RF_MarkAsNative,
1,
STRUCT_OFFSET(AWeapon, WeaponName), 
METADATA_PARAMS(Z_Construct_UClass_AWeapon_Statics::NewProp_WeaponName_MetaData,
UE_ARRAY_COUNT(Z_Construct_UClass_AWeapon_Statics::NewProp_WeaponName_MetaData)) };  

UCLASS 相關程式碼

前面定義的函式和成員變數的程式碼都已經生成完畢了,接下來看具體是如何將其結合到 Class 中的。
首先 gen.cpp 中會生成程式碼將 Function 和 Property 分開儲存,定義如下:

/** 成員變數 **/
const UE4CodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_AWeapon_Statics::PropPointers[] = {  
   (const UE4CodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_AWeapon_Statics::NewProp_WeaponName,  
};

/** 成員函式 **/
const FClassFunctionLinkInfo Z_Construct_UClass_AWeapon_Statics::FuncInfo[] = {  
   { &Z_Construct_UFunction_AWeapon_Fire, "Fire" }, // 2996945510  
};

接著提供構建 AWeaponUClass 資訊,類似構建 UFunction 一般,其填充了一個 FClassParams 的結構體,主要內容包括但不限於:

  • 成員變數列表
  • 函式列表
  • 類標記(即 UCLASS 宏中標記)
const UE4CodeGen_Private::FClassParams Z_Construct_UClass_AWeapon_Statics::ClassParams = {  
   &AWeapon::StaticClass,  
   "Engine",  
   &StaticCppClassTypeInfo,  
   DependentSingletons,  
   FuncInfo,  
   Z_Construct_UClass_AWeapon_Statics::PropPointers,  
   nullptr,  
   UE_ARRAY_COUNT(DependentSingletons),  
   UE_ARRAY_COUNT(FuncInfo),  
   UE_ARRAY_COUNT(Z_Construct_UClass_AWeapon_Statics::PropPointers),  
   0,  
   0x008000A4u,  
   METADATA_PARAMS(Z_Construct_UClass_AWeapon_Statics::Class_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UClass_AWeapon_Statics::Class_MetaDataParams))  
};

然後提供一個構建 UClass 的介面

UClass* Z_Construct_UClass_AWeapon()  
{  
   static UClass* OuterClass = nullptr;  
   if (!OuterClass)  
   {      UE4CodeGen_Private::ConstructUClass(OuterClass, Z_Construct_UClass_AWeapon_Statics::ClassParams);  
   }   return OuterClass;  
}

至此,整個類的自動生成的反射程式碼基本描述完了。

3.2.3 小結

3.2 主要闡述自動生成的程式碼內容大致是什麼東西,個人認為主要分為如下幾點:

  • AWeapon 增加輔助介面(比如 Super,StaticClass,建構函式等)
  • 生成 AWeapon 中所有標記了 UPROPERTYUFUNCTION 的反射程式碼和構建介面
  • 生成 AWeapon 這個 Class 的反射程式碼和構建介面
    最後將介面暴露出去給引擎初始化呼叫即可。

3.3 初始化反射資訊

3.2 中預生成的程式碼已經封裝好所有反射結構的介面了,接下來只要呼叫就可以生成 AWeapon 的反射資訊了。

3.3.1 入口呼叫

UE 中反射資訊主要是在引擎啟動時初始化的,主要利用 gen.cpp 中自動生成的一個靜態變數

static FCompiledInDefer Z_CompiledInDefer_UClass_AWeapon(Z_Construct_UClass_AWeapon, &AWeapon::StaticClass, TEXT("/Script/UdemyProject"), TEXT("AWeapon"), false, nullptr, nullptr, nullptr);

其建構函式會將 構造 AWeapon 的 反射介面傳入到一個全域性容器,啟動時會呼叫 UObjectLoadAllCompiledInDefaultProperties 遍歷構造好 UClass。
大致虛擬碼如下:

// DeferredCompiledInRegistration 儲存了 Z_Construct_UClass_AWeapon
static void UObjectLoadAllCompiledInDefaultProperties(){
	TArray<UClass* (*)()> PendingRegistrants = MoveTemp(DeferredCompiledInRegistration);  
	for (UClass* (*Registrant)() : PendingRegistrants)  
	{  
		// 此處呼叫 Registrant,也就會呼叫 Z_Construct_UClass_AWeapon
		UClass* Class = Registrant();  
		/* 省略一些程式碼 */
		NewClasses.Add(Class);  
	}
}

3.3.2 構建反射資訊

AWeapon 反射資訊的構建入口如下:

UClass* Z_Construct_UClass_AWeapon()  
{  
	static UClass* OuterClass = nullptr;  
	if (!OuterClass)  
	{      
	   UE4CodeGen_Private::ConstructUClass(OuterClass, Z_Construct_UClass_AWeapon_Statics::ClassParams);  
	}   
	return OuterClass;  
}

即使有多個 AWeapon 物件也是共用一個 UClass 來描述反射資訊。其具體的呼叫鏈如下(下面的 AWeapon 可替換為任意自定義的 Class):

4. QA

4.1 如何利用 UClass 構建一個物件

以 SpawnActor 為例,其介面格式如下:

AActor* UWorld::SpawnActor( UClass* Class, FVector const* Location, FRotator const* Rotation, const FActorSpawnParameters& SpawnParameters )

UClass* 引數可以透過如 AWeapon::StaticClass() 或者 TSubClassOf<AWeapon>() 獲取,核心呼叫鏈如下:

  • 準備構建引數,檢查 SpawnParameters.template,如果不存在則使用 CDO (每個 UClass 建立時會有對應描述的 Class 的 Default Object,可以認為是呼叫了 Class 的預設建構函式構建出來的)
  • 呼叫 NewObject
    • StaticConstructObject_Internal
    • StaticAllocateObject
      • 檢查物件是否已經存在
      • 不存在則呼叫 AllocateUObject 分配一個 UObject
    • 呼叫 UClass->ClassConstructor 在 UObject 上構建對應類
  • 返回 Actor

4.2 UClass 如何獲取描述類的建構函式

4.1 中說到,UClass 是利用 ClassConstructor 來構建對應描述的 Class 物件的,ClassConstructor 初始化的時機在於構建 UClassUClass 的構建透過呼叫 TClass::StaticClass ,具體執行流程參考 [[Pasted image 20230329232659.png|3.3.2]] 中第二步初始化 UClass。
其具體初始化方式便是透過宏 DECLARE_CLASSIMPLEMENT_CLASS 來生成相應程式碼並將其傳入到構建 UClass 的一環中。

4.3 UFunction 如何儲存引數及返回值

回顧類圖。

UFunction 的所有引數和返回值都儲存在父類 UStruct::PropertyLink,這是一個連結串列結構,元素型別為 FProperty,透過遍歷並且做標記比對來判斷 Property 是引數還是返回值,以獲取返回值為例,其操作如下:

/** 獲取 UFunction 返回值 **/
FProperty* UFunction::GetReturnProperty() const  
{  
	for( TFieldIterator<FProperty> It(this); It && (It->PropertyFlags & CPF_Parm); ++It )  
	{      
		if( It->PropertyFlags & CPF_ReturnParm )  
		{
			return *It;  
		}   
	} 
	return NULL;  
}

4.4 UFunction 的執行

首先在 UE 中,粗分下來有兩種函式:

  • 藍圖函式
  • C++ 函式
    UE 中用了一個 FUNC_Native 標記來區分,Native 函式是 C++ 函式,非 Native 函式則是藍圖函式。當執行 UFunction 時,需要呼叫 UFunction::Invoke 介面。介面會呼叫 UFunction::Func 函式指標。當 UFunction 型別為 Native 時,Func 指向實際呼叫的函式,反之 Func 則指向 UObject::ProcessInternal

藍圖函式的呼叫原理涉及到藍圖虛擬機器,在[[藍圖與 CPP 之間相互呼叫|藍圖篇]]做補充。

4.5 RPC 函式如何執行的

這裡以純 C++ 實現武器開火為例,開火顯然是一個需要伺服器認證的 Function,為了能夠在客戶端上呼叫,伺服器上執行,需要加上 Server 標記

#include "Weapon.generated.h" // 包含自動生成的標頭檔案資訊
UCLASS() // 註冊類資訊
class AWeapon : public AActor {
	GENERATED_BODY()
public:
	UFUNCTION(Server) /* client 呼叫,Server 執行 */
	void Fire(); /* 定義時只需要定義 Fire_Implementation */
}

接著需要在 Weapon.cpp 中定義 void Fire_Implementation() 介面,此介面為伺服器收到請求後執行的介面。 在呼叫開火時,只需要如下操作,就可以從 client 呼叫到 server 的 fire 函式:

AWeapon* Weapon = GetWeapon();
Weapon->Fire();

這裡的原理是 UHT 在對 RPC 函式會在 gen.cpp 中額外生成一個新的函式定義,格式如下:

/* gen.cpp */
void AWeapon::Fire()  
{  
   ProcessEvent(FindFunctionChecked(NAME_AWeapon_Fire),NULL);  
}

UObject::ProcessEvent 介面會呼叫 UObject::CallRemoteFuntion 將請求傳送到伺服器,伺服器接受到請求後再利用反射查詢要執行的函式名稱和物件,再對其進行執行。

/* gen.cpp */
// 函式名稱及執行函式關聯起來
static const FNameNativePtrPair Funcs[] = {
	{"Fire", &AWeapon::execFire},
}

// 伺服器執行的函式定義
DEFINE_FUNCTION(AWeapon::execFire)  
{  
   P_FINISH;  
   P_NATIVE_BEGIN;  
   P_THIS->SpawnHero13();  
   P_NATIVE_END;  
}

其執行流程大致如下:

相關文章