https://docs.unrealengine.com/latest/CHN/Programming/Introduction/index.html
UE4 中的 C++ 編程介紹

虛幻 C++ 妙不可言!
此指南講述如何在虛幻引擎中編寫 C++ 代碼。不必擔心,虛幻引擎中的 C++ 編程樂趣十足,上手完全不難!我們可以將虛幻 C++ 視為“輔助 C++”,因為諸多功能使 C++ 的使用變得十分簡單。
閱讀此指南的前提是您需要熟悉 C++ 或其他編程語言。理解此指南的前提是您已有 C++ 使用經驗,但如您了解 C#、Java 或 JavaScript,也會發現其中的共通之處。
如您編程經驗為零,我們也能助您一臂之力!閱讀 藍圖可視化腳本 后即可上手。可通過藍圖腳本編寫創建整個游戲!
可以在虛幻引擎中編寫“純舊式 C++ 代碼”,但您通讀此指南並學習虛幻編程模型的基礎后可達到更高的成就。我們將在隨后進一步討論。
C++ 和藍圖
虛幻引擎提供兩種方法創建游戲性元素:C++ 和藍圖可視化腳本。程序員可通過 C++ 添加基礎游戲性系統。設計師即可在此系統上(或使用此系統)創建關卡或游戲的自定義游戲性。在這類情況下,C++ 程序員在他們最擅長的 IDE (通常為 Microsoft Visual Studio 或 Apple Xcode)中工作,而設計師則在虛幻編輯器的藍圖編輯器中工作。
兩個系統均可使用游戲性 API 和框架類。這兩個系統可單獨使用,而結合使用形成相互補充后將展示真正的強大之處。那么這究竟意味着什么呢?這意味着:程序員在 C++ 中創建游戲性構建塊,設計師利用這些塊打造有趣游戲性時,引擎能發揮最佳工作效率。
如此說來,讓我們一探究竟,了解 C++ 程序員為設計師創建構建塊的典型工作流。在此例中,我們將創建一個類。此類稍后會由設計師或程序員通過藍圖進行延展。在此類中,我們將創建一些設計師可進行設置的屬性,並且我們將從這些屬性派生出新數值。結合我們提供的工具和 C++ 宏即可輕松完成整個過程的操作。
類向導
首先我們將使用虛幻編輯器中的類向導生成基礎 C++ 類,以便藍圖稍后進行延展。下圖展示了向導的第一步 - 新建一個 Actor。

進程中的第二步是告知向導需要生成類的命名。下圖顯示的第二步中使用了默認命名。
選擇創建類后,向導將生成文件並打開開發環境,以便開始編輯。這便是生成的類定義。如需了解類向導的更多信息,請查閱此 鏈接 。
#include "GameFramework/Actor.h" #include "MyActor.generated.h" UCLASS() class AMyActor : public AActor { GENERATED_BODY() public: // 設置該 actor 屬性的默認值 AMyActor(); // 游戲開始時或生成時調用 virtual void BeginPlay() override; // 每幀調用 virtual void Tick( float DeltaSeconds ) override; };
類向導通過指定為重載的 BeginPlay() 和 Tick() 生成類。BeginPlay() 是一個事件,將告知您 Actor 已以可操作狀態進入游戲中。現在便適合開始類的游戲性邏輯。Tick() 每幀調用一次,對應的時間量為自上次調用傳入的實際運算時間。在此可創建反復邏輯。如不需要此功能,最好將其移除,以節約少量性能開銷。如要移除此功能,必須將構建函數中說明 tick 應該發生的代碼行刪除。以下構建函數包含討論中的代碼行。
AMyActor::AMyActor() { // 將此 actor 設為每幀調用 Tick()。不需要時可將此關閉,以提高性能。 PrimaryActorTick.bCanEverTick = true; }
使屬性出現在編輯器中
類創建好之后,現在即可創建一些屬性(設計師可在虛幻編輯器中設置這些屬性)。使用特殊宏 UPROPERTY() 即可輕松將屬性公開到編輯器。只需在屬性聲明之前使用 UPROPERTY(EditAnywhere) 宏即可,如以下類所示。
UCLASS() class AMyActor : public AActor { GENERATED_BODY() UPROPERTY(EditAnywhere) int32 TotalDamage; ... };
執行這些操作后,即可在編輯器中對數值進行編輯。有多種方式控制其編輯方法和位置。為 UPROPERTY() 宏傳入更多信息可完成此操作。例如:如需 TotalDamage 屬性和相關屬性出現在一個部分中,可使用分類功能。以下屬性聲明對此進行演示。
UPROPERTY(EditAnywhere, Category="Damage") int32 TotalDamage;
用戶需要編輯此屬性時,它將和其他屬性(這些屬性已以此類型命名標記)一同出現在 Damage 標題之下。這可將常用設置放置在一起,便於設計師進行編輯。
現在讓我們將相同屬性對藍圖公開。
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage") int32 TotalDamage;
如您所見,存在一個藍圖特有的參數。正是此參數使屬性為可讀取和可編寫狀態。還存在一個單獨選項 - BlueprintReadOnly。可通過此選項使屬性在藍圖中被識別為常量。此外還有多個選項可控制屬性對引擎公開的方式。如需了解更多選項,請查閱此 鏈接 。
繼續討論以下部分之前,我們來添加一些屬性到這個示例類。已有屬性對此 actor 輸出的傷害總量進行控制。我們更進一步,實現隨時間輸出傷害。以下代碼添加了一個設計師可進行設置的屬性,和另一個設計師可查看但無法進行更改的屬性。
UCLASS() class AMyActor : public AActor { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage") int32 TotalDamage; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage") float DamageTimeInSeconds; UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient, Category="Damage") float DamagePerSecond; ... };
DamageTimeInSeconds 是設計師可進行修改的屬性。DamagePerSecond 屬性是使用設計師設置的計算值(詳見下一部分)。VisibleAnywhere 標記意味着屬性在虛幻編輯器中為可見狀態,但不可進行編輯。Transient 標記意味着無法從硬盤對其進行保存或加載;它應該為一個派生的非持久值。下圖將屬性顯示為類默認的部分。

在構建函數中設置默認值
在構建函數中設置屬性默認值和典型 C++ 類方法一致。以下是在構建函數中設置默認值的兩個例子,它們在功能上相同。
AMyActor::AMyActor() { TotalDamage = 200; DamageTimeInSeconds = 1.f; } AMyActor::AMyActor() : TotalDamage(200), DamageTimeInSeconds(1.f) { }
下圖是在構建函數中添加默認值后的屬性視圖。

為支持設計師對每個實例設置屬性,數值也從給定對象的實例數據中加載。此數據應用在構建函數之后。與 PostInitProperties() 調用鏈掛鈎即可基於設計師設置的數值創建默認值。此處的進程范例中,TotalDamage 和 DamageTimeInSeconds 為設計師指定的數值。即時這些數值為設計師指定,您仍然可以為它們提供恰當的默認值,正如我們在上例中執行的操作。
void AMyActor::PostInitProperties() { Super::PostInitProperties(); DamagePerSecond = TotalDamage / DamageTimeInSeconds; }
下圖是添加以上 PostInitProperties() 代碼后的屬性視圖。

熱重載
如您習慣於使用 C++ 在其他項目中編程,虛幻引擎的一個炫酷功能可能會讓您小吃一驚。無需關閉編輯器即可對 C++ 變更進行編譯!有兩種方法實現:
-
在編輯器仍在運行時直接以普通方式從 Visual Studio 或 Xcode 進行編譯。編輯器將檢測到新編譯的 DLL 文件並即時重載變更!

-
或者,直接點擊編輯器主工具欄上的 Compile 按鈕。

此功能可用於此教程之后的部分。
通過藍圖延展 C++ 類
迄今為止,我們已通過 C++ 類向導創建了一個簡單的游戲性類,並添加了一些供設計師設置的屬性。現在我們一起來了解設計師應該如何從零開始創建唯一類。
首先我們需要從 AMyActor 類新建一個藍圖類。注意下圖中選中的基類名顯示為 MyActor,而非 AMyActor。這是刻意設置的結果。對設計師隱藏工具使用的命名規則,使命名更加淺顯易懂。

按下 Select 后,便將新建一個默認命名的藍圖類。在此例中,如下方的 內容瀏覽器 截圖所示 - 命名被設為 CustomActor1。

這是我們以設計師身份進行自定義的第一個類。首先我們需要變更傷害屬性的默認值。在此例中,設計師將 TotalDamage 改為 300,將輸出該傷害的時間設為 2 秒。這便是屬性現在出現的方式。

我們的計算值與期望的數值不匹配。它應該為 150,但卻仍然為默認的 200。出現此現象的原因是 - 屬性從載入過程被初始化后,才會對每秒傷害數值進行計算。虛幻編輯器中的運行時變更並非原因所在。因為目標對象在編輯器中被更改時引擎將對其進行通知,所以該問題擁有簡單的解決方法。以下代碼顯示派生值在編輯器中發生變化時進行計算所需要添加的鈎。
void AMyActor::PostInitProperties() { Super::PostInitProperties(); CalculateValues(); } void AMyActor::CalculateValues() { DamagePerSecond = TotalDamage / DamageTimeInSeconds; } #if WITH_EDITOR //nafio_Info編輯器模式,運行中修改編輯器參數 void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { CalculateValues(); Super::PostEditChangeProperty(PropertyChangedEvent); } #endif
需要注意的一點是 - PostEditChangeProperty() 法存在於編輯器特有的 #ifdef 中。這是為了用游戲必需的代碼進行游戲構建,並刪除使可執行文件容量無謂變大的多余代碼。將代碼編譯后,如下圖所示,DamagePerSecond 數值與期望值達成匹配。

跨 C++ 和藍圖邊界調用函數
我們已經談到如何對藍圖公開屬性,在深入探索引擎之前還有最后一個需要介紹的要點。在游戲性系統的創建中,設計師需要調用 C++ 程序員創建的函數,而游戲性程序員需要從 C++ 代碼調用藍圖中實現的函數。首先,我們先實現從藍圖中調用 CalculateValues() 函數。對藍圖公開函數和公開屬性同樣簡單。在函數聲明前放置一個宏即可!以下代碼片段顯示了所需內容。
UFUNCTION(BlueprintCallable, Category="Damage")//nafio_Info向藍圖公開方法 void CalculateValues();
UFUNCTION() 宏把 C++ 函數對反射系統公開。BlueprintCallable 選項將其對藍圖虛擬機公開。每個對藍圖公開的函數都需要與其相關的類型,右鍵單擊快捷菜單才能正常使用。下圖顯示了類型對快捷菜單的影響。

如您所見,可從 Damage 類型選擇函數。以下藍圖代碼顯示 TotalDamage 數值發生變化后將進行調用,重新計算依賴數據。

計算依賴屬性使用的函數與之前添加的函數相同。引擎的大部分通過 UFUNCTION() 宏對藍圖公開,開發者無需編寫 C++ 代碼即可構建游戲。然而,最佳方法是使用 C++ 構建基礎游戲性系統和與性能關系密切的代碼,而藍圖則用於自定義行為或從 C++ 構建塊創建合成行為。
實現設計師調用 C++ 代碼的操作后,我們來尋找一個越過 C++/藍圖邊界的好方法。此方法允許 C++ 代碼調用藍圖中定義的函數。通常使用此方法告知設計師在適當時可進行反饋的事件。通常這包括特效生成或其他視覺效果,如 actor 的隱藏和現身。以下代碼片段顯示藍圖實現的函數。
UFUNCTION(BlueprintImplementableEvent, Category="Damage")//nafio_info向藍圖暴露事件回調 void CalledFromCpp();
此函數的調用方式和其他 C++ 函數相同。虛幻引擎在后台生成一個基礎 C++ 函數實現;它理解如何調入藍圖 VM。這通常被稱作 Thunk(形實轉換程序)。如討論中的藍圖不為此方法提供函數主體,函數的行為則與不含主體行為的 C++ 函數一樣:不執行任何操作。如果希望提供 C++ 默認實現,同時仍允許藍圖覆寫此方法,結果會怎樣?UFUNCTION() 宏也擁有針對此情況的選項。以下代碼片段顯示達成此效果需要在頭中進行的的變更。
UFUNCTION(BlueprintNativeEvent, Category="Damage")//nafio_info向藍圖暴露可以有默認回調方法的事件回調 void CalledFromCpp();
此版本仍然生成 thunking 法,以調入藍圖 VM。那么如何提供默認實現呢?工具還將生成外觀與 _Implementation() 相似的新函數實現。您必須提供函數的這個版本,否則項目將無法鏈接。以下是上方聲明的實現代碼。
void AMyActor::CalledFromCpp_Implementation() { // 玩點花活 }
現在,討論中的藍圖不覆寫方法時將調用函數的這個版本。需要注意:在編譯工具的舊版本中,_Implementation() 聲明為自動生成。在 4.8 或更高版本中,這會被顯式添加到頭中。
了解常規游戲性程序員工作流以及協同設計師構建游戲性功能的方法后,您便可以開始自己的游戲開發冒險之旅。您可繼續閱讀此文檔了解如何在引擎中使用 C++,也可直接對 launcher 中的實例進行操作,獲得實際操作經驗。
深入了解
您決定繼續和我們一同冒險。太棒啦!下個討論要點圍繞游戲性類層級進行。這部分我們將討論基礎構建塊以及它們之間相互關聯的方式。在此我們將了解虛幻引擎如何使用繼承和合成構建自定義游戲性功能。
游戲性類:對象、Actor 和組件
多數游戲性類派生自 4 個主要類型。它們是 UObject、AActor、UActorComponent 和 UStruct。以下部分會對這些構建塊進行一一說明。當然,您還可以創建並非派生自這些類的類型,但其無法采用引擎中內置的功能。UObject 層級樹之外創建的類的典型用法有:整合第三方庫、封裝操作系統特定功能等。
虛幻對象(UObject)
虛幻引擎中的基礎構建塊被稱作 UObject。此類結合 UClass 提供引擎中最重要的若干基礎服務:
-
屬性和方法反射
-
屬性序列化
-
垃圾回收
-
按命名查找 UObject
-
可配置屬性數值
-
屬性和方法網絡支持
派生自 UObject 的每個類擁有一個為其創建的單例 UClass,此對象包含關於類實例的所有元數據。UObject 和 UClass 是游戲性對象在其生命期中執行所有操作的根源。區分 UClass 和 UObject 的最佳方式:UClass 描述 UObject 實例的組成、可用於序列化的屬性、網絡等。多數的游戲性開發不會直接從 UObject 進行派生,而從 AActor 和 UActorComponent 進行派生。編寫游戲性代碼無需了解 UClass/UObject 的工作細節。但了解這些系統的存在也會有所幫助。
AActor
AActor 是作為游戲體驗一部分的對象。AActor 將被設計師放置在關卡中,或通過游戲性系統在運行時創建。所有可放入關卡的對象均延展自此類。范例有 AStaticMeshActor、ACameraActor 和 APointLight actor。AActor 派生自 UObject,因此可使用上一部分列出的所有標准功能。可通過游戲性代碼(C++ 或藍圖)顯式銷毀 AActor。擁有關卡從內存被卸載后,通過標准垃圾回收機制進行銷毀。AActor 負責游戲對象的高級行為。AActor 還是可進行網絡復制的基類。在網絡復制中,AActor 還可分布 UActorComponent 的信息。UActorComponent 為需要網絡支持的 AActor 所擁有。
AActor 擁有其自身的行為(通過繼承的特殊化),但它們仍作為 UActorComponent 層級的容器(通過合成的特殊化)。這通過 AActor 的 RootComponent 成員完成。此成員包含一個單一 UActorComponent,而這個組件又可依次包含其他組件。在 AActor 可被放入關卡之前,它必須包含至少一個 USceneComponent。此組件包含此 AActor 的平移、旋轉和尺寸。
AActor 擁有一系列事件,可在生命周期中進行調用。以下列表是說明生命周期的簡化事件集。
-
BeginPlay - 對象首次出現在游戲進程中時調用
-
Tick - 每幀調用一次,在一段時間內執行操作
-
EndPlay - 對象離開游戲進程時調用
在 Actor 中查看關於 AActor 的詳細討論。
運行時生命周期
之前我們討論了 AActor 生命周期的一個子集。對於放置在關卡中的 actor 而言,通過想象便可輕松理解生命周期:actor 加載,出現,隨后關卡被卸載,actor 被銷毀。運行時創建和銷毀的過程是怎樣的?虛幻引擎在運行時生成調用 AActor 的創建。較之於在游戲中創建一個普通對象,actor 的生成稍顯復雜。原因是 AActor 需要通過各種運行時系統進行注冊,以滿足所有需要。需要設置 actor 的初始位置和旋轉。物理可能需要知曉這些信息。負責告知 actor 進行 tick 的管理器需要知曉這些信息。諸如此類。因此,我們擁有一個用於 actor 生成的方法 - UWorld::SpawnActor()。一旦 actor 成功生成后,它的 BeginPlay() 方法將被調用,下一幀將出現 Tick()。
一旦 actor 的生命期完結,即可調用 Destroy() 將其銷毀。在此過程中將調用 EndPlay(),在此可設置自定義銷毀邏輯。控制 actor 存在時長的另一個選項是使用壽命成員。可在對象的構建函數中設置時間段,或通過運行時的其他代碼進行設置。時間量耗盡后,actor 將自動調用 Destroy()。
如需了解 actor 生成的更多內容,請查閱 生成 Actors 頁面。
UActorComponent
UActorComponent 擁有其自身行為,通常負責在多種類型 AActor 之間共享的功能,如提供可視網格體、粒子效果、攝像機透視和物理互動。通常為 AActor 指定的是與其在游戲中全局作用相關的高級目標,而 UActorComponent 通常執行的是支持這些高級目標的單個任務。組件也可附着到其他組件,或為 Actor 的根組件。組件只能附着到一個父組件或 Actor,但可被多個子組件附着。想象一個組件樹。子組件擁有與其父組件或 Actor 相對的位置、旋轉和尺寸。
使用 Actor 和組件的方法有多種,而理解 Actor - 組件關系的方式是 Actor 會提出問題“這是什么?”,而組件會回答“這由什么組成?”
-
RootComponent - 這是在 AActor 組件樹中擁有頂層組件的 AActor 成員
-
Ticking - 組件作為擁有 AActor Tick() 的部分被點擊
剖析第一人稱角色
之前的幾個部分敘述較多,展示較少。為展示 AActor 和其 UActorComponent 之間的關系,我們一來研究基於第一人稱模板創建新項目時創建的藍圖。下圖是 FirstPersonCharacter Actor 的 組件 樹。RootComponent 為 CapsuleComponent。附着到 CapsuleComponent 的是 ArrowComponent、Mesh 組件和 FirstPersonCameraComponent。葉最多的組件是以 FirstPersonCameraComponent 為父項的 Mesh1P 組件,意味着第一人稱網格體與第一人稱攝像機相對。

視覺外觀而言,組件 樹與下圖相似,可看到除 Mesh 組件外的所有組件均在 3D 空間中。

此組件樹被附着到一個 actor 類。從此例中可了解到 - 使用繼承和合成可構建復雜的游戲性對象。需要對現有 AActor 或 UActorComponent 進行自定義時使用繼承。需要多個不同 AActor 類型共享功能時使用合成。
UStruct
使用 UStruct 時不必從任意特定類進行延展,只需要使用 USTRUCT() 標記結構體,編譯工具將執行基礎工作。和 UObject 不同,UStruct 不會被垃圾回收。如創建其動態實例,則必須自行管理其生命周期。UStruct 為純舊式數據類型。它們擁有 UObject 反射支持,以便在虛幻編輯器、藍圖操作、序列化和網絡通信中進行編輯。
討論完游戲性類構建中使用的基礎層級后,即可再次選擇路徑。可在 此處 閱讀關於游戲性類的內容、使用 launcher 中帶有更多信息的樣本、或進一步深入研究構建游戲的 C++ 功能。
繼續深入了解
很高興您能繼續學習。讓我們繼續深入了解引擎的工作。
虛幻反射系統
游戲性類使用特殊的標記。因此在開始了解它們之前,我們有必要了解虛幻屬性系統的一些基礎知識。UE4 使用其自身的反射實現,可啟用動態功能,如垃圾回收、序列化、網絡復制和藍圖/C++ 通信。這些功能為選擇加入,意味着您需要為類型添加正確的標記,否則引擎將無視類型,不生成反射數據。以下是基礎標記的快速總覽:
-
UCLASS() - 告知虛幻引擎生成類的反射數據。類必須派生自 UObject。
-
USTRUCT() - 告知虛幻引擎生成結構體的反射數據。
-
GENERATED_BODY() - UE4 使用它替代為類型生成的所有必需樣板文件代碼。
-
UPROPERTY() - 使 UCLASS 或 USTRUCT 的成員變量可用作 UPROPERTY。UPROPERTY 用途廣泛。它允許變量被復制、被序列化,並可從藍圖中進行訪問。垃圾回收器還使用它們來追蹤對 UObject 的引用數。
-
UFUNCTION() - 使 UCLASS 或 USTRUCT 的類方法可用作 UFUNCTION。UFUNCTION 允許類方法從藍圖中被調用,並在其他資源中用作 RPC。
以下是 UCLASS 的聲明范例:
#include "MyObject.generated.h" UCLASS(Blueprintable) class UMyObject : public UObject { GENERATED_BODY() public: MyUObject(); UPROPERTY(BlueprintReadOnly, EditAnywhere) float ExampleProperty; UFUNCTION(BlueprintCallable) void ExampleFunction(); };
首先注意 - “MyClass.generated.h”文件已包含。虛幻引擎將生成所有反射數據並將放入此文件。必須在聲明類型的頭文件中將此文件作為最后的 include 包含。
您還會注意到,可以在標記上添加額外的說明符。此處已添加部分常用說明符用於展示。通過說明符可對類型擁有的特定行為進行說明。
-
Blueprintable - 此類可由藍圖延展。
-
BlueprintReadOnly - 此屬性只可從藍圖讀取,不可寫入。
-
Category - 定義此屬性出現在編輯器 Details 視圖下的部分。用於組織。
-
BlueprintCallable - 可從藍圖調用此函數。
說明符太多,無法一一列舉於此,以下鏈接可用作參考:
對象/Actor 迭代器
對象迭代器是非常實用的工具,用於在特定 UObject 類型和子類的所有實例上進行迭代。
// 將找到當前所有的 UObjects 實例 for (TObjectIterator<UObject> It; It; ++It) { UObject* CurrentObject = *It; UE_LOG(LogTemp, Log, TEXT("Found UObject named:%s"), *CurrentObject.GetName()); }
為迭代器提供更為明確的類型即可限制搜索范圍。假設您有一個派生自 UObject,名為 UMyClass 的類。您會發現此類的所有實例(以及派生自此類的實例)與此相似:
for (TObjectIterator<UMyClass> It; It; ++It) { // ... }
Actor 迭代器與對象迭代器的工作方式非常相近,但只能用於派生自 AActor 的對象。Actor 迭代器不存在下列問題,只返回當前游戲世界實例使用的對象。
創建 actor 迭代器時,需要為其賦予一個指向 UWorld 實例的指針。許多 UObject 類(如 APlayerController)會提供 GetWorld 方法,助您一臂之力。如不確定,可在 UObject 上檢查 ImplementsGetWorld 方法,確認其是否應用 GetWorld 方法。
APlayerController* MyPC = GetMyPlayerControllerFromSomewhere(); UWorld* World = MyPC->GetWorld(); // 和對象迭代器一樣,您可提供一個特定類,只獲取為該類的對象 // 或從該類派生的對象 for (TActorIterator<AEnemy> It(World); It; ++It) { // ... }
內存管理和垃圾回收
此部分中,我們將了解到 UE4 中的基礎內存管理和垃圾回收系統。
UObject 和垃圾回收
UE4 使用反射系統實現垃圾回收系統。通過垃圾回收便無需手動刪除 UObjects,只需維持對它們的有效引用即可。類須派生自 UObject,方能啟用垃圾回收。這是我們將要使用的簡單范例類:
UCLASS() class MyGCType : public UObject { GENERATED_BODY() };
在垃圾回收器中存在稱為根集的概念。此根集是一個對象列表。回收器不會對這些對象進行垃圾回收。只要根集中的對象到討論中的對象之間存在引用路徑,對象便不會被垃圾回收。如對象到根集的此路徑不存在,它便會被識別為無法達到,垃圾回收器下次運行時便會將其收集(刪除)。引擎以特定間隔運行垃圾回收器。
什么被視作“引用”?存儲在 UPROPERTY 中的 UObject 指針。我們來看一個簡單的例子:
void CreateDoomedObject() { MyGCType* DoomedObject = NewObject<MyGCType>(); }
調用以上函數后便新建了一個 UObject,但我們不在 UPROPERTY 中保存指向它的指針,它也不是根集的一部分。垃圾回收器將逐步檢測到此對象為無法達到,並將其銷毀。
Actors 和垃圾回收
Actors 通常不會被垃圾回收。Actors 生成后,必須在其上手動調用 Destroy()。它們不會被立即刪除,而會在下個垃圾回收階段被清理。
常見情況下 actors 帶有 UObject 屬性。
UCLASS() class AMyActor : public AActor { GENERATED_BODY() public: UPROPERTY() MyGCType* SafeObject; MyGCType* DoomedObject; AMyActor(const FObjectInitializer& ObjectInitializer) :Super(ObjectInitializer) { SafeObject = NewObject<MyGCType>(); DoomedObject = NewObject<MyGCType>(); } }; void SpawnMyActor(UWorld* World, FVector Location, FRotator Rotation) { World->SpawnActor<AMyActor>(Location, Rotation); }
調用上述函數時,將在世界場景中生成一個 actor。Actor 的構建函數創建兩個對象。一個指定到 UPROPERTY,另一個指定到裸指針。Actors 自動成為根集的一部分,SafeObject 將不會被垃圾回收,因為它從根集對象出到達。然而 DoomedObject 的進展不是十分順利。我們未將其標為 UPROPERTY,因此回收器並不知道其正在被引用,而會將它逐漸銷毀。
UObject 被垃圾回收時,對其的所有 UPROPERTY 引用將被設為 nullptr。這可使您安全地檢查一個對象是否已被垃圾回收。
if (MyActor->SafeObject != nullptr) { // 使用 SafeObject }
這十分重要,正如之前所述,已在自身上調用 Destroy() 的 actor 在垃圾回收器再次運行之前不會被移除。您可檢查 IsPendingKill() 方法,確定 UObject 正等待被刪除。如方法返回 true,則應將對象視為廢棄物,不進行使用。
UStructs
如之前所述,UStructs 是 UObject 的一個簡化版本。就這點而言,UStructs 無法被垃圾回收。如必須使用 UStructs 的動態實例,應使用智能指針,稍后我們將談到它。
非 UObject 引用
普通的非 UObject 也可添加到對象的引用,以防止被垃圾回收。對象必須派生自 FGCObject 並覆寫它的 AddReferencedObjects 類。
class FMyNormalClass : public FGCObject { public: UObject* SafeObject; FMyNormalClass(UObject* Object) :SafeObject(Object) { } void AddReferencedObjects(FReferenceCollector& Collector) override { Collector.AddReferencedObject(SafeObject); } };
我們可使用 FReferenceCollector 手動為需要的、不能被垃圾回收的 UObject 添加硬引用。對象被刪除,其析構函數運行時,它將自動清除添加的所有引用。
類命名前綴
虛幻引擎為您提供在構建過程中生成代碼的工具。這些工具擁有一些類命名規則。如命名與規則不符,將觸發警告或錯誤。下方的類前綴列表說明了命名的規則。
-
派生自 Actor 的類前綴為 A,如 AController。
-
派生自 對象 的類前綴為 U,如 UComponent。
-
枚舉 的前綴為 E,如 EFortificationType。
-
接口 類的前綴通常為 I,如 IAbilitySystemInterface。
-
模板 類的前綴為 T,如 TArray。
-
派生自 SWidget(Slate UI)的類前綴為 S,如 SButton。
-
其余類的前綴均為 字母 F ,如 FVector。
數字類型
因為不同平台基礎類型的尺寸不同,如 short、int 和 long,UE4 提供了以下類型,可用作替代品:
-
int8/uint8 :8 位帶符號/不帶符號 整數
-
int16/uint16 :16 位帶符號/不帶符號 整數
-
int32/uint32 :32 位帶符號/不帶符號 整數
-
int64/uint64 :64 位帶符號/不帶符號整數
標准 浮點 (32-bit) 和 雙倍(64-bit)類型也支持浮點數。
虛幻引擎擁有一個模板 TNumericLimits,用於找到數值類型支持的最小和最大范圍。如需了解詳情,請查閱此 鏈接 。
字符串
UE4 提供多個不同類使用字符串,可滿足多種需求。
FString
FString 是一個可變字符串,類似於 std::string。FString 擁有許多方法,便於簡單地使用字符串。使用 TEXT() 宏可新建一個 FString:
FString MyStr = TEXT("Hello, Unreal 4!").
FText
FText 與 FString 相似,但用於本地化文本。使用 NSLOCTEXT 宏可新建一個 FText。此宏擁有默認語言的命名空間、鍵和一個數值。
FText MyText = NSLOCTEXT("Game UI", "Health Warning Message", "Low Health!")
也可使用 LOCTEXT 宏,只需要在每個文件上定義一次命名空間。確保在文件底層取消它的定義
// 在 GameUI.cpp 中 #define LOCTEXT_NAMESPACE "Game UI" //... FText MyText = LOCTEXT("Health Warning Message", "Low Health!") //... #undef LOCTEXT_NAMESPACE // 文件末端
FName
FName 將經常反復出現的字符串保存為辨識符,以便在對比時節約內存和 CPU 時間。FName 不會在引用完整字符串的每個對象間對其進行多次保存,而是使用一個映射到給定字符串的較小存儲空間 索引。這會單次保存字符串內容,在字符串用於多個對象之間時節約內存。檢查 NameA.Index 是否等於 NameB.Index 可對兩個字符串進行快速對比,避免對字符串中每個字符進行相等性檢查。
TCHAR
TCHARs 用於存儲不受正在使用的字符集約束的字符。平台不同,它們也可能存在不同。UE4 字符串在后台使用 TCHAR 陣列將數據保存在 UTF-16 編碼中。使用返回 TCHAR 的重載解引用運算符可以訪問原始數據。
部分函數會需要它。如 FString::Printf,‘%s’ 字符串格式說明符需要 TCHAR,而非 FString。
FString Str1 = TEXT("World"); int32 Val1 = 123; FString Str2 = FString::Printf(TEXT("Hello, %s! You have %i points."), *Str1, Val1);
FChar 類型提供一個靜態效用函數集,以便使用單個 TCHAR。
TCHAR Upper('A'); TCHAR Lower = FChar::ToLower(Upper); // 'a'
容器
容器也是類,它們的主要功能是存儲數據集。常見的類有 TArray、TMap 和 TSet。它們的大小均為動態,因此可變為所需的任意大小。
TArray
在這三個容器中,虛幻引擎 4 使用的主要容器是 TArray。它的作用和 std::vector 相似,但卻多出許多功能。以下是一些常規操作:
TArray<AActor*> ActorArray = GetActorArrayFromSomewhere(); // 告知當前 ActorArray 中保存的元素(AActors)數量。 int32 ArraySize = ActorArray.Num(); // TArrays 從零開始(第一個元素在索引 0 處) int32 Index = 0; // 嘗試獲取在給定索引處的元素 TArray* FirstActor = ActorArray[Index]; // 在陣列末端添加一個新元素 AActor* NewActor = GetNewActor(); ActorArray.Add(NewActor); // 只有元素不在陣列中時,才在陣列末端添加元素 ActorArray.AddUnique(NewActor); // 不會改變陣列,因為 NewActor 已被添加 // 移除陣列中所有 NewActor 實例 ActorArray.Remove(NewActor); // 移除特定索引處的元素 // 索引上的元素將被下調一格,以填充空出的位置 ActorArray.RemoveAt(Index); // RemoveAt 的高效版,但無法保持元素的排序 ActorArray.RemoveAtSwap(Index); // 移除陣列中的所有元素 ActorArray.Empty();
TArray 還有一個額外好處 - 可使其元素被垃圾回收。這將假定 TArray 被標記為 UPROPERTY,並存儲 UObject 派生的指針。
UCLASS() class UMyClass :UObject { GENERATED_BODY(); // ... UPROPERTY() TArray<AActor*> GarbageCollectedArray; };
之后章節中我們將深度討論垃圾回收。
TMap
TMap 是鍵值對的合集,與 std::map 相似。TMap 可基於元素的鍵快速尋找、添加、並移除元素。只要鍵擁有為其定義的 GetTypeHash 函數(稍后對此進行了解),即可使用任意類型的鍵。
假設您創建了一個基於網格的桌面游戲,需要保存並詢問每個方格上的塊。通過 TMap 即可輕松完成。如棋盤尺寸較小且保持不變,還存在更加高效的處理方式。但出於范例的緣故,暫且談到這里吧!
enum class EPieceType { King, Queen, Rook, Bishop, Knight, Pawn }; struct FPiece { int32 PlayerId; EPieceType Type; FIntPoint Position; FPiece(int32 InPlayerId, EPieceType InType, FIntVector InPosition) : PlayerId(InPlayerId), Type(InType), Position(InPosition) { } }; class FBoard { private: // 使用 TMap 時可通過塊的位置對其進行查閱 TMap<FIntPoint, FPiece> Data; public: bool HasPieceAtPosition(FIntPoint Position) { return Data.Contains(Position); } FPiece GetPieceAtPosition(FIntPoint Position) { return Data[Position]; } void AddNewPiece(int32 PlayerId, EPieceType Type, FIntPoint Position) { FPiece NewPiece(PlayerId, Type, Position); Data.Add(Position, NewPiece); } void MovePiece(FIntPoint OldPosition, FIntPoint NewPosition) { FPiece Piece = Data[OldPosition]; Piece.Position = NewPosition; Data.Remove(OldPosition); Data.Add(NewPosition, Piece); } void RemovePieceAtPosition(FIntPoint Position) { Data.Remove(Position); } void ClearBoard() { Data.Empty(); } };
TSet
TSet 保存唯一值的合集,與 std::set 相似。TArray 通過 AddUnique 和 Contains 方法可用作集。然而 TSet 可更快實現這些操作,但無法像 TArray 那樣將它們用作 UPROPERTY。TSet 不會像 TArray 那樣將元素編入索引。
TSet<AActor*> ActorSet = GetActorSetFromSomewhere(); int32 Size = ActorSet.Num(); // 如集尚未包含元素,則將其添加到集 AActor* NewActor = GetNewActor(); ActorSet.Add(NewActor); // 檢查元素是否已包含在集中 if (ActorSet.Contains(NewActor)) { // ... } // 從集移除元素 ActorSet.Remove(NewActor); // 從集移除所有元素 ActorSet.Empty(); // 創建包含 TSet 元素的 TArray TArray<AActor*> ActorArrayFromSet = ActorSet.Array();
需注意:TArray 是當前唯一能被標記為 UPROPERTY 的容器類。這意味着無法復制、保存其他容器類,或對其元素進行垃圾回收。
容器迭代器
使用迭代器可在容器的每個元素上進行循環。以下是使用 TSet 的迭代器語法范例。
void RemoveDeadEnemies(TSet<AEnemy*>& EnemySet) { // 從集的開頭開始迭代到集的末端 for (auto EnemyIterator = EnemySet.CreateIterator(); EnemyIterator; ++EnemyIterator) { // * 運算符獲得當前的元素 AEnemy* Enemy = *EnemyIterator; if (Enemy.Health == 0) { // RemoveCurrent 由 TSets 和 TMaps 支持 EnemyIterator.RemoveCurrent(); } } }
可結合迭代器使用的其他支持操作:
// 將迭代器移回一個元素 --EnemyIterator; // 以一定偏移前移或后移迭代器,此處的偏移為一個整數 EnemyIterator += Offset; EnemyIterator -= Offset; // 獲得當前元素的索引 int32 Index = EnemyIterator.GetIndex(); // 將迭代器重設為第一個元素 EnemyIterator.Reset();
For-each 循環
迭代器很實用,但如果只希望在每個元素之間循環一次,則可能會有些累贅。每個容器類還支持 for each 風格的語法在元素上進行循環。TArray 和 TSet 返回每個元素,而 TMap 返回一個鍵值對。
// TArray TArray<AActor*> ActorArray = GetArrayFromSomewhere(); for (AActor* OneActor :ActorArray) { // ... } // TSet - 和 TArray 相同 TSet<AActor*> ActorSet = GetSetFromSomewhere(); for (AActor* UniqueActor :ActorSet) { // ... } // TMap - 迭代器返回一個鍵值對 TMap<FName, AActor*> NameToActorMap = GetMapFromSomewhere(); for (auto& KVP :NameToActorMap) { FName Name = KVP.Key; AActor* Actor = KVP.Value; // ... }
注意:auto 關鍵詞不會自動指定指針/引用,需要自行添加。
通過 TSet/TMap(散列函數)使用您自己的類型
TSet 和 TMap 需要在內部使用 散列函數。如要創建在 TSet 中使用或作為 TMap 鍵使用的自定義類,首先需要創建自定義散列函數。通常會放入這些類型的多數 UE4 類型已定義其自身的散列函數。
散列函數接受到您的類型的常量指針/引用,並返回一個 uint64。此返回值即為對象的 散列代碼,應該是對該對象唯一虛擬的數值。兩個相等的對象固定返回相同的散列代碼。
class FMyClass { uint32 ExampleProperty1; uint32 ExampleProperty2; // 散列函數 friend uint32 GetTypeHash(const FMyClass& MyClass) { // HashCombine 是將兩個散列值組合起來的效用函數 uint32 HashCode = HashCombine(MyClass.ExampleProperty1, MyClass.ExampleProperty2); return HashCode; } // 出於展示目的,兩個對象為相等 // 應固定返回相同的散列代碼。 bool operator==(const FMyClass& LHS, const FMyClass& RHS) { return LHS.ExampleProperty1 == RHS.ExampleProperty1 && LHS.ExampleProperty2 == RHS.ExampleProperty2; } };
現在, TSet 和 TMap 在散列鍵時將使用適當的散列函數。如您使用指針作為鍵(即 TSet<FMyClass*>),也將實現 uint32 GetTypeHash(const FMyClass* MyClass)。
