【UE4全反射松耦合框架筆記】第一章 UE4框架基礎(上)

第一章 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

開發時,注意的幾個點

  1. 聲明的函數,如果不是BlueprintImplementationEvent函數,請一定要定義,否則可能會編譯失敗
  2. 引用頭文件時,如果是此類的聲明頭文件,可以直接使用#include “XXX.h”,不用加路徑,而其它自己創建的頭文件,要注意加上路徑,但路徑中不用加Public。這個主要跟模塊設置有關。
  3. 因IDE的關係,編譯報錯的點不一定就是真正的問題所在,但可以從報錯的文件中查找錯誤

二、UMG與C++交互

文件目錄:

Game
Common
Gameplay
UI
HUD
Widget

在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的初始化過程:

  1. Actor的構造函數
  2. 初始化Components
  3. PostInitializeCompoents()
  4. BeginPlay()
  5. Tick(float DeltaSeconds)

UserWidget的初始化過程:

  1. 構造器函數 UUserWidget(const FObjectInitializer& ObjectInitializer);
  2. Initialize()
  3. NativeTick(const FGeometry& MyGeometry, float InDeltaTime);

世界的創建:

  1. GameInstance實例化,執行Init函數
  2. 執行關卡藍圖構造器
  3. 執行場景中Actors的構造器
  4. 執行GameMode構造器
  5. 執行關卡藍圖、場景中Actors及GameMode的PostInitializeComponents函數(子組件構造完成後執行)
  6. 執行Controller、HUD、Character等在GameSettings中設置的Gameplay框架類的構造器及PostInitializeComponents函數
  7. 執行場景中Actor、GameMode、關卡藍圖、Gameplay框架類的BeginPlay函數
  8. 若有Widget在執行HUD類的BeginPlay函數時被創建,將執行Widget的構造器及Initialize函數
  9. 執行所有已經初始化完成類的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的設置
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);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章