第一章 UE4框架基礎(上)
文章目錄
一、藍圖與C++交互
在藍圖中創建變量時,有四個選項可供選擇:
變量類型與C++分別對應的是:
AActor* //對象引用
TSubclassOf<AActor> //類類型
TSoftObjectPtr<AActor> //對象軟引用
TSoftClassPtr<AActor> //類類型軟引用
TAssetPtr<UObject> //資源引用,對FStringAssetReference的封裝,包含對象路徑和對象弱指針
FStringAssetReference //對象路徑的引用,不用區分類型
方法與藍圖交互:
BlueprintCallable 藍圖可調用且由C++定義
BlueprintImplementableEvent C++聲明,由藍圖實現
BlueprintNativeEvent C++實現,藍圖可覆蓋,C++實現的方法需要加後綴_Implementation,但實現的方法不需要聲明
Exec 控制檯可調用
meta 可以限定對象屬性
例如:
//限定對象屬性
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(MetaClass="Actor", ToolTip="Bool Type"))
FStringAssetReference AssetRef;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(ClampMin=10, ClampMax=100), Category="FrameWork")
int32 CInt;
//BlueprintNativeEvent方法聲明
UFUNCTION(BlueprintNativeEvent)
void NativeFunc();
//BlueprintNativeEvent方法實現
void AMyActor::NativeFunc_Implementation()
{...}
//控制檯命令方法
UFUNCTION(Exec)
void ConsoleFunc(const FString& info);
/*Exec標記的函數,只有在主要的幾個類中(GameInstance,GameMode等)纔會執行。
我們可以在GameInstance中重寫ProcessConsoleExec方法,使其可以被執行到。*/
virtual bool ProcessConsoleExec(const TCHAR* Cmd, FOutputDevice& Ar, UObject* Executor) override;
bool UMyGameInstance::ProcessConsoleExec(const TCHAR* Cmd, FOutputDevice& Ar, UObject* Executor)
{
bool Res = Super::ProcessConsoleExec(Cmd, Ar, Executor);
if(!Res) //命令沒有執行成功(沒有找到相關命令需要執行)
{
for(TActorIterator<AActor> It(GetWorld()); It; ++It) //遍歷Actor類
{
Res = It->ProcessConsoleExec(Cmd, Ar, Executor); //逐個執行
if(Res) break; //如果執行成功,就退出循環並結束
}
}
return Res;
}
對象獲取方法:
- 方法一:迭代器獲取
for(TActorIterator<AActor> It(GetWorld()); It; ++It){...}
- 方法二:靜態方法獲取
TArray<AActor*> Actors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ACharacter::StaticClass(), Actors);
屬性修改事件:
//編輯器模式修改變量(屬性)時,將會觸發此方法
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
#if WITH_EDITOR
void AMyCharacter::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
//當對象被修改的屬性名等於AMyCharacter類(一般爲當前對象的類名)的Param變量(屬性名)時,執行此段代碼
if(PropertyChangedEvent.Property&&
PropertyChangedEvent.Property->GetName() == GET_MEMBER_NAME_CHECKED(AMyCharacter, Param)){...}
}
#endif
開發時,注意的幾個點
- 聲明的函數,如果不是BlueprintImplementationEvent函數,請一定要定義,否則可能會編譯失敗
- 引用頭文件時,如果是此類的聲明頭文件,可以直接使用#include “XXX.h”,不用加路徑,而其它自己創建的頭文件,要注意加上路徑,但路徑中不用加Public。這個主要跟模塊設置有關。
- 因IDE的關係,編譯報錯的點不一定就是真正的問題所在,但可以從報錯的文件中查找錯誤
二、UMG與C++交互
文件目錄:
在PlayerController中設置輸入模式:
//輸入模式
//輸入模式有三種類型:FInputModeGameAndUI,FInputModeGameOnly,FInputModeUIOnly
FInputModeGameAndUI InputMode;
InputMode.SetLockMouseToViewBehavior(EMouseLockMode::DoNotLock);
InputMode.SetHideCursorDuringCapture(false)
SetInputMode(InputMode);
//顯示鼠標
bShowMouseCursor = true;
創建Widget:
//方法1:
TSubclassOf<UMyWidget> MyWidgetClass; //可以在藍圖中選擇UMyWidget及繼承自UMyWidget的子類
UMyWidget* MyWidget = CreateWidget<UMyWidget>(GetWorld(), MyWidgetClass);
MyWidget->AddToViewport(); //添加到界面
//方法2:在Widget內部,通過WidgetTree創建。可用於創建子組件
UImage* DynImage = WidgetTree->ConstructWidget<UImage>(UImage::StaticClass());
從藍圖獲取組件:
//方法一:從根節點獲取
UCanvasPanel* RootPanel = Cast<UCanvasPanel>(GetRootWidget());
if(RootPanel)
{
UImage* BGImage = Cast<UImage>(RootPanel->GetChildAt(0));
}
//方法二:GetWidgetFromName
UButton* ButtonTwo = (UButton*)GetWidgetFromName(TEXT("ButtonTwo")); //通過方法獲取到UMG中變量名爲ButtonTwo的控件
//方法三:反射綁定
UPROPERTY(meta=(BindWidget))
UButton* ButtonOne; //變量名應與UMG中控件名一致
給控件綁定函數(事件):
//方法一:__Internal_AddDynamic
Button->OnClick.__Internal_AddDynamic(this, &UMyWidget::ButtonEvent, FName("ButtonEvent"));
//方法二:FScriptDelegate
FScriptDelegate BtnEventDel;
BtnEventDel.BindUFuncion(this, "ButtonEvent");
Button->OnReleased.Add(BtnEventDel);
//方法三:聲明BlueprintCallable,藍圖調用
UFUNCTION(BlueprintCallable)
void ButtonEvent();
添加和移除組件:
//添加子組件
UWidget->AddChildXXX(...);
//移除子組件
UWidget->RemoveChild(...);
//從父組件移除
UWidget->RemoveFromParent();
設置佈局:
//在Canvas中添加子組件會返回slot插槽對象,通過設置這個對象可以改變佈局
//UCanvasPanel* MyCanvasPanel;
//UImage* MyImage;
UCanvasPanelSlot* ImgSlot = MyCanvasPanel->AddChildToCanvas(MyImage);
ImgSlot->SetAnchors(FAnchors(0.f)); //設置錨點
ImgSlot->SetOffsets(FMargin(100.f, 100.f, 50.f, 50.f)); //設置位移
三、生命週期探索
Actor的初始化過程:
- Actor的構造函數
- 初始化Components
- PostInitializeCompoents()
- BeginPlay()
- Tick(float DeltaSeconds)
UserWidget的初始化過程:
- 構造器函數 UUserWidget(const FObjectInitializer& ObjectInitializer);
- Initialize()
- NativeTick(const FGeometry& MyGeometry, float InDeltaTime);
世界的創建:
- GameInstance實例化,執行Init函數
- 執行關卡藍圖構造器
- 執行場景中Actors的構造器
- 執行GameMode構造器
- 執行關卡藍圖、場景中Actors及GameMode的PostInitializeComponents函數(子組件構造完成後執行)
- 執行Controller、HUD、Character等在GameSettings中設置的Gameplay框架類的構造器及PostInitializeComponents函數
- 執行場景中Actor、GameMode、關卡藍圖、Gameplay框架類的BeginPlay函數
- 若有Widget在執行HUD類的BeginPlay函數時被創建,將執行Widget的構造器及Initialize函數
- 執行所有已經初始化完成類的Tick函數
提前預置Actor和設置中的類(如Gameplay框架類)等會在都執行完成Construct和PostInitializeComponents後纔會執行BeginPlay,然後一起執行Tick。另外Gameplay框架類會在GameMode執行完 PostInitializeComponents後,纔會開始構造。
上圖中,因爲UserWidget是在HUD執行BeginPlay時才被創建,所以會在HUD後執行構造器函數和Initialize函數
四、全局類與接口
全局類
方法一:UBlueprintFunctionLibrary
繼承自UBlueprintFunctionLibrary的類,主要是給藍圖提供靜態函數訪問,如果只在C++中使用,不必繼承此類。常用的C++類,例如UGameStatics、UGameUserSettings類等,均是繼承自UBlueprintFunctionLibrary類。
方法二:GameEngine->GameSingleton
GEngine->GameSingletion是GEngine下的一個UObject指針,可以作爲全局變量提供給所有對象調用,這個變量可以在編輯器中指定其類型,UE4建議在這個對象中放置不需要修改的變量。
GameSingleton類
/*
UCLASS中Blueprintable及BlueprintType解釋
Blueprintable/NotBlueprintable
此說明符表示藍圖可繼承此類,默認情況下是NotBlueprintable。但如果此類的父類包含此說明符,在默認情況下將繼承父類的說明符,允許藍圖繼承此類。因爲UObject沒有此標識符,所以若要藍圖可以繼承和使用此類,需要聲明此標識符。
BlueprintType
表示藍圖可以創建此類類型的變量。但其實,只要聲明Blueprintable,就可以作爲變量在藍圖中使用了。所以,結構體和枚舉可能更需要使用此標識符。此處,還可以再探究。
*/
UCLASS(Blueprintable, BlueprintType)
class XXX_API UMyGameSingleton: public UObject
{
GENERATED_BODY()
...
}
獲取GameSingleton的變量
UMyGameSingleton* MySingleton = Cast<UMyGameSingletonClass>(GEngine->GameSingleton);
其它引擎全局類
UGameInstance:生命週期一直伴隨整個遊戲週期,從打開程序直到關閉程序。
UGameplayStatics:可以獲取常用的Gamplay對象
UGameUserSettings:可以獲取到配置信息
接口
一般,我們創建接口類時,儘量繼承UInterface類。繼承自UInterface的接口類,可以通過函數反射,提供給藍圖實現和使用。
當創建繼承自UInterface的類時,引擎幫我們生成的頭文件會有兩個類,我們在第二個類中(以I開頭)聲明函數。
如果接口中的抽象方法需要在藍圖中調用,我們實現的自定義接口類一般有兩種情況:1. 函數可以在C++子類和藍圖類中實現和調用 2.函數只能在C++中實現,在藍圖類中調用。
情況一:默認接口類中的函數是必須要允許在藍圖中實現
通過集成UInterface類,引擎自動生成的接口類的宏不帶標識符meta=(CannotImplementInterfaceInBlueprint)。所以,我們在接口類中聲明的方法的UFUNCTION宏必須使用BlueprintNativeEvent或者BlueprintImplementableEvent標識符。
自定義接口類頭文件:
//默認情況下,UINTERFACE標識符只有MinimalAPI
UINTERFACE(MinimalAPI)
class UMyInterface : public UInterface
{
GENERATED_BODY()
}
class XXX_API IMyInterface
{
GENERATED_BODY()
public:
/*默認下的接口類,在聲明UFUNCTION宏時,必須使用BlueprintNativeEvent或BlueprintImplementableEvent標識符。
第一和第三個函數可以在C++文件中實現;這四個函數均可在藍圖中實現;前兩個函數可以在藍圖中調用。
這四個函數沒有使用virtual和=0,第一和第三個函數在C++中實現見下面代碼*/
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void FuncOne();
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
void FuncTwo();
UFUNCTION(BlueprintNativeEvent)
void FuncThree();
UFUNCTION(BlueprintImplementableEvent)
void FuncFour();
}
繼承上述接口類的子類頭文件:
class XXX_API AMyActor : public AActor, public UMyInterface
{
GENERATED_BODY()
public:
//注意這裏的實現,需要加virtual、override關鍵字,且函數名加_Implementation後綴
virtual void FuncOne_Implementation() override;
virtual void FuncThree_Implementation() override;
}
如果接口中的方法只在C++中實現和調用,那就在接口中聲明抽象方法並實現,比如:
virtual void GetIndex() {}
情況二:函數不需要在藍圖中實現
如果虛函數不能在藍圖中實現,我們必須在自定義接口類的宏中添加meta=(CannotImplementInterfaceInBlueprint)標識符。
只有加上此標識符,虛函數的UFUNCTION宏纔可以不添加BlueprintNativeEvent和BlueprintImplementableEvent標識符,也就不需要在藍圖中實現。
UINTERFACE(MinimalAPI, meta=(CannotImplementInterfaceInBlueprint))
class UMyInterface : public UInterface
{
GENERATED_BODY()
}
class XXX_API IMyInterface
{
GENERATED_BODY()
public:
//此類需要使用virtual關鍵字,函數後邊的=0表示繼承的子類需要實現此方法。如果允許藍圖調用,可以加上BlueprintCallable,否則可以不加
UFUNCTION(BlueprintCallable)
virtual void FuncOne() = 0;
}
繼承UInterface的接口,不能創建UPROPERTY()變量
虛函數的調用
允許在藍圖實現的函數和不允許在藍圖實現的函數的調用方法不同。
//調用第一步是相同的,就是首先我們需要獲取一個繼承定義接口類的對象
TArray<AActor*> Actors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AMyInterfaceActor::StaticClass(), Actors);
if(Actors.Num() > 0)
{
IMyInterface* MyInterfacePtr = Cast<IMyInterface>(Actors[0]); //強制轉換爲父類接口
/*
因爲使用了BlueprintNativeEvent和BlueprintImplementableEvent標識符的函數的實現與一般C++不同,所以調用方法不同。
下面代碼的調用就是允許在藍圖中實現的函數的調用。
*/
MyInterfacePtr->Execute_FuncOne(Actors[0]); //函數名添加前綴Execute_,第一個參數爲子類對象,後續參數爲函數實際參數
//不在藍圖實現的虛函數與一般C++相同,所以按照常規方法調用即可
//MyInterfacePtr->FuncOne(); //而一般的虛函數可以在轉換後直接調用,或者直接調用對象的函數即可。Actors[0]->FuncOne();
}
五、委託與函數傳遞
委託
UE4委託參見另一篇筆記《【UE4筆記】委託》
函數傳遞
TFunction
聲明TFunction模板變量
public:
//設置TFunction模板變量的值,這裏主要說明如何將函數作爲參數傳入
void SetFuncOne(TFunction<void(FString)> Func)
{ //變量實現應在cpp中,這裏爲方便說明,所以在聲明中實現
FuncOne = Func;
}
public:
//TFunction模板聲明
TFunction<void(FString)> FuncOne; //void是函數返回類型,FString爲函數參數類型
通過Lambda表達式傳參,調用SetFuncOne函數
public:
void MyFunc(FString Str);
//SetFuncOne的調用
XXX->SetFuncOne([this](FString InfoStr){MyFunc(InfoStr);});
使用TFunction定義統一接口
聲明函數模板
template<typename RetType, typename... VarTypes>
void PassFunc(TFunction<RetType(VarTypes...)> TarFunc)
{
bool Result = TarFunc(FString("Hello"), 1024);
}
========================================================
調用函數模板
bool Func(FString, int32);
XXX->PassFunc<bool, FString, int32>([this](FString InfoStr, int32 Count){return Func(Info, Count);});
TMemFunPtrType
使用這種方法傳遞的函數,函數的返回值和傳入參數都已經在函數參數指明,所以傳入時,只需要按照函數要求傳入即可。
//聲明單播委託,這裏主要是測試傳遞函數,所以將接收的函數綁定到這個單播委託上
DECLARE_DELEGATE(FMyDelegate);
//聲明模板函數
public:
template<typename UserClass>
void BindAndExecDel(UserClass* TarObj, typename TMemFunPtrType<false, UserClass, void(FString, int32)>::Type InMethod);
public:
FMyDelegate MyDelegate;
//實現模板函數,TMemFuncPtrType模板作爲參數類型,用於傳遞函數參數
template<typename UserClass>
void XXX::BindAndExecDel(UserClass* TarObj, typename TMemFunPtrType<false, UserClass, void(FString, int32)>::Type InMethod)
{
//因爲MyDelegate是一個無參委託,所以調用ExecuteIfBound時是不需要傳入參數的。但是綁定的函數卻是有參,所以我們可以在BindUObject函數調用,即綁定函數到委託時,固定參數。
MyDelegate.BindUObject(TarObj, InMethod, FString("Hello World!"), 1024);
MyDelegate.ExecuteIfBound();
}
//調用模板函數,這裏調用的時候,可以傳入函數地址
void Func(FString, int32);
XXX->BindAndExecDel(this, &XXX::Func); //允許傳入的函數爲返回類型void,參數爲FString和int
FMethodPtr
使用TMehtodPtr時,我們需要聲明一個委託,然後通過委託的靜態成員獲取到函數類型。這種方法可以用於函數委託綁定的傳參。下面也將介紹如何不用聲明委託,即可使用此方法傳遞函數。
使用這種方法傳入的函數,它的返回值和參數類型都是在委託中聲明的。
//聲明單播委託
DECLARE_DELEGATE_TwoParams(FMyDelegate, FString, int32)
//聲明模板函數
public:
//參數使用了委託,通過委託的靜態成員獲取函數聲明類型
template<typename UserClass>
void BindAndExecDel(UserClass* TarObj, typename FMyDelegate::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod);
public:
FMyDelegate MyDelegate;
//實現模板函數
template<typename UserClass>
void BindAndExecDel(UserClass* TarObj, typename FMyDelegate::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod)
{
//委託的參數是在調用ExecuteIfBound或者Broadcast時傳入的。此處注意與上面無參委託的區別。
MyDelegate.BindUObject(TarObj, InMethod);
MyDelegate.ExecuteIfBound(FString("Hellow, World!"), 1024);
}
void Func(FString, int32);
XXX->BindAndExecDel(this, &XXX::Func);
不聲明委託,使用FMethodPtr傳遞函數(泛型定義統一接口)
使用這種方法,基本可以傳遞任意返回值和參數的函數,只需要在調用函數時,按照需要的函數聲明委託即可。
函數模板定義:
public:
//上例中的委託類型被模板參數替代
template<typename DelegateType, typename UserClass, typename... VarTypes)
inline void PassFunc(UserClass* TarObj, typename DelegateType::template TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, VarTypes... Vars)
{
//這裏的委託聲明只是爲了測試函數的傳遞。
DECLARE_DELEGATE(FTempDelegate);
FTempDelegate TempDelegate;
TempDelegate.BindUObject(TarObj, InMethod, Vars...); //傳入多個參數,參數名後有...,不要忘記
TempDelegate.ExecuteIfBound();
}
=======================================================================================================
函數模板調用:
void Func(FString, int32);
//函數調用時,函數模板第一個參數傳入委託類型
DECALRE_DELEGATE_TwoParams(FTempDelegate, FString, int32);
XXX->PassFunc<FTempDelegate>(this, &XXX::Func, FString("Hello"), 1024);