藍圖
大家都知道,藍圖是UE4提供的極其容易上手的一種可視化腳本,更具體的就不說了。
純靠藍圖搭建的UE4游戲是存在的,但是這類游戲往往優化很差(除非游戲玩法本身的性能需求不高)。更合適的流程往往需要程序員編寫C++代碼創建一些藍圖可用元素,而設計師再通過藍圖快速搭建游戲。
藍圖命名規范
藍圖命名:"BP"+類別縮寫+"_"+名字
例如: BPA_Player
藍圖類別 | 前綴 |
---|---|
藍圖Actor | BPA_ |
藍圖結構 | BPS_ |
藍圖枚舉 | BPE_ |
藍圖接口 | BPI_ |
藍圖函數庫 | BFL_ |
藍圖宏庫 | BML_ |
藍圖優化
在Project Settings -》 Packaging -》 Experimental 下面有個選項:Nativize Blueprint Assets
如果勾選這個選項,那么在打包時會將藍圖腳本編譯成C++代碼。
暴露C++至藍圖
C++中常常使用UE4中的一些宏來設置想要暴露於藍圖的類、屬性、方法等(實質內部是UE4的反射機制)。
暴露C++類
一般使用UCLASS([specifiers])暴露類至藍圖,並添加頭文件#include "XXX.generated.h",在類里第一行使用GENERATED_BODY()。
UCLASS([specifier, specifier, ...], [meta(key=value, key=value, ...)])
class ClassName : public ParentName
{
GENERATED_BODY()
}
常用[specifiers]參數:
- Blueprintable:將該類公開為可接受的用於創建藍圖的基類。默認值為不可藍圖化(NotBlueprintable),但以其他方式繼承時除外。這是由子類繼承的。
- BlueprintType:
將該類公開為可用於藍圖中的變量的類型。 - NotBlueprintable:指定此類不是用於創建藍圖的可接受基類。否定指定了可藍圖化關鍵字的父類的效果。
至於meta(元數據說明符)參數,主要是規范(限制)用,相對沒有specifiers常用。此處不做多講,下文同理,感興趣可以參考具體官方文檔。
//例子:
#include "AMyActor.generated.h"
UCLASS(Blueprintable)
class AMyActor : public AActor {
GENERATED_BODY()
};
暴露C++屬性
使用UPROPERTY([specifiers])宏暴露屬性至藍圖。
UPROPERTY([specifier, specifier, ...], [meta(key=value, key=value, ...)])
Type VariableName;
常用[specifier]參數:
- EditAnywhere:可通過“屬性”窗口在原型和實例上進行編輯。
- VisibleAnywhere:該屬性在“屬性”窗口中可見,但無法編輯,與EditAnywhere不兼容。
- BlueprintReadOnly:此屬性可以由藍圖讀取,但不能修改。
- BlueprintReadWrite:此屬性可以通過藍圖讀取或寫入。
- BlueprintAssignable:應公開屬性以在藍圖中分配。
- BlueprintCallable:應公開屬性以在藍圖圖表中調用。
- Category = “XXX”:給該屬性分類以便在虛幻編輯器中查詢。
此處注意EditAnyWhere和BlueprintReadWrite的區別,前者表示在虛幻編輯器中可以在“屬性”窗口中對該屬性值進行編輯。然而若需要在藍圖腳本編輯器中設置該屬性,則需要使用BlueprintReadWrite,相當於為該屬性自動添加了get和set方法。
//例子:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Character")
float health;
暴露C++函數
使用UPROPERTY([specifiers])宏暴露屬性至藍圖,如:
UFUNCTION([specifier, specifier, ...], [meta(key=value, key=value, ...)])
ReturnType FunctionName([Parameter, Parameter, ...]);
常用[specifier]參數:
- BlueprintCallable:表示此函數可以直接在藍圖中執行,函數的實現只能在C++中進行。
- BlueprintImplementableEvent:該函數的具體實現只能在藍圖中進行。對於沒有返回值的函數,可以當做一種事件來處理,不必有具體的實現。而對於有返回值的函數,則需要在藍圖編輯器中的左邊欄查找該函數並進行覆寫。其調用還只能在C++原生代碼中進行。
- BlueprintNativeEvent:藍圖可以調用該函數,該函數的默認實現在C++中已經完成了,但是藍圖可以對該函數進行覆蓋重寫。這個參數可以實現最靈活的函數調用。
注意:對於BlueprintNativeEvent函數,需要一些特殊處理:
- 要聲明一個新的虛函數,函數名為 FunctionName_Implementation;
- 對該函數的C++實現要轉而對該虛函數進行;
- 無論C++或者藍圖調用該函數時,都是直接使用函數的原名。
// 例子:
// 頭文件
UFUNCTION(BlueprintNativeEvent)
void CountdownHasFinished();
virtual void CountdownHasFinished_Implementation();
// 源文件
void ACountdown::CountdownHasFinished_Implementation() {
CountdownText->SetText(TEXT(“Go!”));
}
void ACountdown::BeginPlay() {
Super::BeginPlay();
CountdownHasFinished();
}
暴露C++結構體/枚舉
結構體可包含變量,包括UProperty變量、函數和運算符,且只需用USTRUCT([specifiers])標記該結構體和內置一句GENERATED_USTRUCT_BODY(),無需繼承。
USTRUCT([Specifier, Specifier, ...])
struct StructName {
GENERATED_USTRUCT_BODY()
};
常用[specifier]參數:
- BlueprintType:表示結構體可以在藍圖中使用。
與UObject不同的是,UStruct不會被垃圾回收,必須自行管理其生命周期。UStruct應該是純傳統數據類型,包含UObject反射支持,可以在虛幻編輯器、藍圖操控、序列化、聯網等中編輯。
//例子:結構體
USTRUCT(BlueprintType)
struct FCharcterStatus {
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Character")
float health;
};
枚舉則直接用UENUM([specifiers])標記。
//例子:枚舉
UENUM(Meta = (Bitflags))
enum class EColorBits
{
ECB_Red,
ECB_Green,
ECB_Blue
};
暴露C++接口
接口類有助於確保一組(可能)不相關的類實現一組通用函數。在某些游戲功能可能被大量復雜而不同的類共享的情況下,這非常有用。
例如,某個游戲可能有這樣一個系統,依靠該系統輸入一個觸發器體積可以激活陷阱、警告敵人或向玩家獎勵點數。這可以通過針對陷阱、敵人和點數獎勵器執行“ReactToTrigger”函數來實現。
然而,陷阱可能派生自“AActor”,敵人可能派生自專門的“APawn”或“ACharacter”子類,點數獎勵可能派生自“UDataAsset”。所有這些類都需要共享功能,但它們沒有除“UObject”之外的共同上級。在這種情況下,推薦使用接口。
UINTERFACE([specifier, specifier, ...], [meta(key=value, key=value, ...)])
class UClassName : public UInterface
{
GENERATED_BODY()
};
常用[specifier]參數:
- BlueprintType:表示接口可以在藍圖中使用。
//例子:
#include "ReactToTriggerInterface.generated.h"
UINTERFACE(Blueprintable)
class UReactToTriggerInterface : public UInterface
{
GENERATED_BODY()
};
藍圖和C++的結合方案
一個將藍圖和C++結合的常見方案是先大量使用藍圖制作項目,后續再用C++把復雜的藍圖重寫一遍,從而提升優化效果。
然而現在藍圖有Nativize Blueprint Assets的功能,可以將藍圖編譯成C++代碼。所以個人認為絕大部分藍圖並不需要重寫,直接Nativize Blueprint Assets即可。而對於某些包含復雜邏輯計算的藍圖,則才適合用C++來替代藍圖。
推薦看參考里面的[UE4]使用C++重寫藍圖,SpawnObject根據類型動態創建UObject博客來加深理解。
使用繼承重寫藍圖
-
創建一個C++類作為藍圖的父類(C++類繼承藍圖一樣的父類),在UE4中修改藍圖的父類。
-
用C++中實現好的方法逐個替換藍圖中方法,每次替換一個方法就必須要運行游戲進行詳細測試,防止修改太多萬一出錯無法定位問題所在(盡量避免出現要同時替換2個以上藍圖方法才能正常運行游戲。這一點非常重要。同樣也是防止修改太多萬一出錯無法定位問題所在)
如下圖所示:保留原藍圖的實現,方便C++代碼查錯。
使用組合重寫藍圖
-
創建一個繼承自UObject的C++類,一般加后綴Helper,並且加上BlueprintType標簽,共藍圖作為變量類型使用。
-
在藍圖中添加一個名為MyHelper的變量,類型是第一步創建的C++類型。
-
在使用helper對象之前,必須先實例化。接着要初始化helper的成員變量值。其中當前藍圖對象的引用(也就是self)要傳遞給me參數,這是關鍵,用helper的成員對象保存起來。
-
最終用helper對象的C++方法一個一個替換原來用藍圖寫的功能。
方案比較
兩個方案都要注意的事項:
- C++類中的方法、成員變量與藍圖一一對應,並且方法和成員變量名稱不能與藍圖的重復。
- 在替換藍圖的過渡時期,代表相同含義的藍圖變量和C++類變量可能同時存在。那么給變量賦值時,應注意2個變量都要同時賦值,以保證藍圖方法和C++方法都能正常運行。
- A藍圖不能直接使用B藍圖的變量,A藍圖把要公開的變量封裝在函數內返回,並且只返回UE4自帶的基礎變量類型,不能返回自定義類型,以方便C++重寫時返回C++中的成員變量。
參考博客原文的結論是:使用繼承和組合都可以實現C++重寫藍圖,但是組合比繼承要更好,耦合度更低。
參考
虛幻引擎4 官方文檔 | 虛幻架構 創建和實現游戲性類的參考
[UE4]使用C++重寫藍圖,SpawnObject根據類型動態創建UObject
系列其他文章:Aery的UE4 C++開發之旅系列文章