Aery的UE4 C++游戲開發之旅(3)藍圖


藍圖


大家都知道,藍圖是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()
};

可參考:虛幻4文檔——游戲性類

暴露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;

可參考:虛幻4文檔——屬性

暴露C++函數

使用UPROPERTY([specifiers])宏暴露屬性至藍圖,如:

UFUNCTION([specifier, specifier, ...], [meta(key=value, key=value, ...)])
ReturnType FunctionName([Parameter, Parameter, ...]);

常用[specifier]參數:

  • BlueprintCallable:表示此函數可以直接在藍圖中執行,函數的實現只能在C++中進行。
  • BlueprintImplementableEvent:該函數的具體實現只能在藍圖中進行。對於沒有返回值的函數,可以當做一種事件來處理,不必有具體的實現。而對於有返回值的函數,則需要在藍圖編輯器中的左邊欄查找該函數並進行覆寫。其調用還只能在C++原生代碼中進行。
  • BlueprintNativeEvent:藍圖可以調用該函數,該函數的默認實現在C++中已經完成了,但是藍圖可以對該函數進行覆蓋重寫。這個參數可以實現最靈活的函數調用。

注意:對於BlueprintNativeEvent函數,需要一些特殊處理:

  1. 要聲明一個新的虛函數,函數名為 FunctionName_Implementation;
  2. 對該函數的C++實現要轉而對該虛函數進行;
  3. 無論C++或者藍圖調用該函數時,都是直接使用函數的原名。
// 例子:
// 頭文件
UFUNCTION(BlueprintNativeEvent)
void CountdownHasFinished();

virtual void CountdownHasFinished_Implementation();

// 源文件
void ACountdown::CountdownHasFinished_Implementation() {
	CountdownText->SetText(TEXT(“Go!”));
}

void ACountdown::BeginPlay() {
	Super::BeginPlay();
	CountdownHasFinished();
}

可參考:虛幻4文檔——函數

暴露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
};

可參考:虛幻4文檔——結構體

暴露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()
};

可參考:虛幻4文檔——接口

藍圖和C++的結合方案


一個將藍圖和C++結合的常見方案是先大量使用藍圖制作項目,后續再用C++把復雜的藍圖重寫一遍,從而提升優化效果。
然而現在藍圖有Nativize Blueprint Assets的功能,可以將藍圖編譯成C++代碼。所以個人認為絕大部分藍圖並不需要重寫,直接Nativize Blueprint Assets即可。而對於某些包含復雜邏輯計算的藍圖,則才適合用C++來替代藍圖。

推薦看參考里面的[UE4]使用C++重寫藍圖,SpawnObject根據類型動態創建UObject博客來加深理解。

使用繼承重寫藍圖

  1. 創建一個C++類作為藍圖的父類(C++類繼承藍圖一樣的父類),在UE4中修改藍圖的父類。

  2. 用C++中實現好的方法逐個替換藍圖中方法,每次替換一個方法就必須要運行游戲進行詳細測試,防止修改太多萬一出錯無法定位問題所在(盡量避免出現要同時替換2個以上藍圖方法才能正常運行游戲。這一點非常重要。同樣也是防止修改太多萬一出錯無法定位問題所在)

如下圖所示:保留原藍圖的實現,方便C++代碼查錯。

使用組合重寫藍圖

  1. 創建一個繼承自UObject的C++類,一般加后綴Helper,並且加上BlueprintType標簽,共藍圖作為變量類型使用。

  2. 在藍圖中添加一個名為MyHelper的變量,類型是第一步創建的C++類型。 

  3. 在使用helper對象之前,必須先實例化。接着要初始化helper的成員變量值。其中當前藍圖對象的引用(也就是self)要傳遞給me參數,這是關鍵,用helper的成員對象保存起來。 

  4. 最終用helper對象的C++方法一個一個替換原來用藍圖寫的功能。

方案比較

兩個方案都要注意的事項:

  • C++類中的方法、成員變量與藍圖一一對應,並且方法和成員變量名稱不能與藍圖的重復。
  • 在替換藍圖的過渡時期,代表相同含義的藍圖變量和C++類變量可能同時存在。那么給變量賦值時,應注意2個變量都要同時賦值,以保證藍圖方法和C++方法都能正常運行。
  • A藍圖不能直接使用B藍圖的變量,A藍圖把要公開的變量封裝在函數內返回,並且只返回UE4自帶的基礎變量類型,不能返回自定義類型,以方便C++重寫時返回C++中的成員變量。

參考博客原文的結論是:使用繼承和組合都可以實現C++重寫藍圖,但是組合比繼承要更好,耦合度更低。

參考


虛幻引擎4 官方文檔 | 虛幻架構 創建和實現游戲性類的參考

[UE4]使用C++重寫藍圖,SpawnObject根據類型動態創建UObject

UE4初學筆記

系列其他文章:Aery的UE4 C++開發之旅系列文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM