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]參數:.net

  • 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 官方文檔 | 中文文檔 | 虛幻架構 建立和實現遊戲性類的參考

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

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

UE4初學筆記

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

相關文章
相關標籤/搜索