在蓝图中创建变量时,有四个选项可供选择:
变量类型与C++分别对应的是:
AActor* //对象引用
TSubclassOf //类类型
TSoftObjectPtr //对象软引用
TSoftClassPtr //类类型软引用
TAssetPtr //资源引用,对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
开发时,注意的几个点
文件目录:
在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的初始化过程:
UserWidget的初始化过程:
世界的创建:
提前预置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模板变量
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);});
使用这种方法传递的函数,函数的返回值和传入参数都已经在函数参数指明,所以传入时,只需要按照函数要求传入即可。
//声明单播委托,这里主要是测试传递函数,所以将接收的函数绑定到这个单播委托上
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
使用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);