全局类分为下面两种:
(一)UBlueprintFunctionLibrary:UE4 的静态类,主要是给蓝图提供静态函数,如果只在 C++ 用不必要用这个。
关于上面这个全局类,读者可以阅读这篇文章进行了解:【UE4】Blueprint Function Libraries在c++和蓝图中的使用方式
该文章简单而又不失详细地讲解了蓝图函数库的 C++ 实现。当然,我们也可以直接创建其蓝图版本,然后在里面添加静态方法,不过这个不在本笔记的讨论范围之内。
(二)GEngine->GameSingleton:一个在 GEngine 下的 UObject 指针,UE4 专门提供出来作为全局变量给所有对象调用。可以在编辑器指定类型来使用它,UE4 建议放置不需要修改的数据在这个对象。
接下来我们来试着编写一下这个类。新建一个 C++ 的 Object 类,路径为 Public/Common,取名为 FWDataSingleton。老师打算在里面放置一些变量作为全局变量,供其他类访问。
FWDataSingleton.h
class UTexture2D; // 提前声明
UCLASS(Blueprintable, BlueprintType) // 分别代表可以以这个类为基类创建蓝图、可以作为蓝图里的一个变量类型
class FRAMECOURSE_API UFWDataSingleton : public UObject
{
GENERATED_BODY()
public:
UFWDataSingleton();
// 声明 4 种类型的、可供蓝图读写的测试用变量
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
TArray<UClass*> SourceBlueprints;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
UTexture2D* SourceT2D;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
FVector SourceVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
FString SourceFilePath;
};
FWDataSingleton.cpp
UFWDataSingleton::UFWDataSingleton()
{
SourceFilePath = "bilibala";
}
编译后,在 Blueprint 文件夹下创建名为 Common 的文件夹,在它里面创建一个以 FWDataSingleton 为基类的蓝图,取名为 FWDataSingleton_BP。
点开蓝图,可以看到细节面板的 Source File Path 的值为 bilibala。随意设置下刚刚在头文件里声明的 SourceVector 的值,随后到项目设置里把 Game Singleton Class 设置为 FWDataSingleton_BP。
来到 GameMode 里来测试是否可以获取到这个全局类的内容。
FWGameMode.h
protected:
virtual void BeginPlay() override;
FWGameMode.cpp
// 引入头文件
#include "Common/FWCommon.h"
#include "Common/FWDataSingleton.h"
void AFWGameMode::BeginPlay()
{
Super::BeginPlay();
UFWDataSingleton* DataSingleton = Cast<UFWDataSingleton>(GEngine->GameSingleton);
// Debug 语句,测试完后可以注释掉或者删掉
FWHelper::Debug(DataSingleton->SourceVector.ToString(), 500.f);
}
编译后运行游戏,可以看见左上角输出了 SourceVector 里的数据。(可以把 FWAffectWidget_BP 移开以便查看 Debug 输出)
需要注意的是,UE4 不建议在运行状态下修改全局类蓝图里面的变量。所以我们在运行时将其当成已经被赋值的常量就好了。
UE4 的接口指代基于 UInterface 创建的类;C++ 层面的接口则指代利用纯虚函数实现的像接口一样功能的类。尽管后者也可以用,不过老师还是推荐我们使用 UE4 提供的接口为佳,因为后者不能接受使用 UFUNCTION() 宏的方法,而前者可以。
在 Public/FWInter 路径下创建以下 C++ 文件:
新建一个 Unreal Interface 类,取名为 FWInterface。
再创建一个 Actor 类,取名为 FWInterActor。
可以看到 FWInterface 里有两个类,但实际上我们要用接口的时候只需要用下面的 IFWInterface。
并且可以看到两个类里面都有 GENERATED_BODY() 的宏。
梁迪老师说如果不修改这两个宏的话,我们只能用不带任何说明符的 UFUNCTION() 宏;需要修改成另外两个宏才能在 IFWInterface 里写带有 UFUNCTION() 宏和说明符的方法。
但是实际上在笔者的 4.26 版本,这个写法已经可以摒弃了。笔者这里会用注释标明旧版本的写法,实际运行时会使用新版的写法。
我们把四个方法都加上了 Event 的说明符,则四个方法都会拥有 Event 的蓝图节点。
FWInterface.h
UINTERFACE(MinimalAPI)
class UFWInterface : public UInterface
{
GENERATED_BODY() // 下面的旧版本写法的宏已经被整合进这个初始宏了
//GENERATED_UINTERFACE_BODY() // 旧版本写法,修改宏
};
class FRAMECOURSE_API IFWInterface
{
GENERATED_BODY() // 下面的旧版本写法的宏已经被整合进这个初始宏了
//GENERATED_IINTERFACE_BODY() // 旧版本写法,修改宏
public:
// 不需要在接口的 .cpp 里实现,留给继承这个接口的对象来实现;蓝图可调用
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "FrameWork")
void FWFunOne(const FString& HitResult);
// 只能在蓝图里实现;蓝图可调用
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "FrameWork")
void FWFunTwo(const FString& HitResult);
// 不需要在接口的 .cpp 里实现,让继承接口的对象来实现方法;蓝图不可调用
UFUNCTION(BlueprintNativeEvent, Category = "FrameWork")
void FWFunThree(const FString& HitResult);
// 蓝图里实现;蓝图不可以调用
UFUNCTION(BlueprintImplementableEvent, Category = "FrameWork")
void FWFunFour(const FString& HitResult);
};
FWInterface.cpp
// 此处旧版本写法才需要加这里的构造函数,新版本不需要
// 由于修改成了 GENERATED_UINTERFACE_BODY() 宏,其构造函数的参数要变成这个初始化器,并且要继承父类的初始化器
/*
UFWInterface::UFWInterface(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}
*/
关于 UInterface 的相关知识点,推荐读者阅读这篇文章:《UE4 Interface原理与使用》
文章清晰易懂地讲解了 UInterface 和 IInterface 之间的关系,以及使用它们的方法。
来到 FWInterActor,让它继承上面的 IFWInterface。
FWInterActor.h
#include "FWInter/FWInterface.h" // 引入头文件
#include "FWInterActor.generated.h"
UCLASS()
class FRAMECOURSE_API AFWInterActor : public AActor, public IFWInterface // 继承接口
{
GENERATED_BODY()
public:
AFWInterActor();
// C++ 实现接口的方法需要加后缀来跟蓝图的实现方法区分开来
virtual void FWFunOne_Implementation(const FString& HitResult) override;
virtual void FWFunThree_Implementation(const FString& HitResult) override;
};
FWInterActor.cpp
// 引入头文件
#include "Common/FWCommon.h"
void AFWInterActor::FWFunOne_Implementation(const FString& HitResult)
{
FWHelper::Debug("FWFunOne", 500.f);
}
void AFWInterActor::FWFunThree_Implementation(const FString& HitResult)
{
FWHelper::Debug("FWFunThree", 500.f);
}
编译后,在 Blueprint 路径下新建一个叫 Interface 的文件夹,在里面创建 FWInterActor 的蓝图,取名 FWInterActor_BP。
打开后来到蓝图脚本界面,可以看到我们声明在接口里的方法:
给 Event BeginPlay 蓝图节点连上 FWFunOne (Interface Call) 的节点,然后把 FWInterActor_BP 放进场景。
运行游戏,可以看见左上角输出了在 FWInterActor.cpp 里重写的 FWFunOne()
方法里的 Debug 语句。(注意,如果脚本编辑界面存在 FWFunOne 的 Event 节点,则游戏会优先执行 Event 节点的重写逻辑,导致 C++ 的 FWFunOne()
里面的 Debug 语句无法输出)
测试完了把 FWFunOne (Interface Call) 节点删除。
如果不想接口的方法可以作为蓝图里的 Event 节点,并不是不添加 Event 说明符就可以了。如果这样做的话编译器会报错。UE4 提供了一个 meta 说明符来限定这个情况,即 meta = (CannotImplementInterfaceInBlueprint)。下面会进行讲解:
在 Public/FWInter 路径下创建以下 C++ 文件:
新建一个 Unreal Interface 类,命名为 FWCallInter。(怎么小写 L 和大写 i 长一个样,要注意是 FWCa L L i nter)
新建一个 Actor 类,命名为 FWCallActor。
这里就不再写生成宏的旧写法了,直接用现在的写法。
FWCallInter.h
UINTERFACE(MinimalAPI, meta = (CannotImplementInterfaceInBlueprint)) // 添加这个 meta 说明符
class UFWCallInter : public UInterface
{
GENERATED_BODY()
};
class FRAMECOURSE_API IFWCallInter
{
GENERATED_BODY()
public:
// 不能再添加 Event 说明符,且必须变成纯虚函数
UFUNCTION(BlueprintCallable, Category = "FrameWork")
virtual void FWCallFun(const FString& HitResult) = 0;
};
来到 FWCallActor,继承接口并重写接口的方法。
FWCallActor.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "FWCallInter.h" // 引入头文件
#include "FWCallActor.generated.h"
UCLASS()
class FRAMECOURSE_API AFWCallActor : public AActor, public IFWCallInter // 继承接口
{
GENERATED_BODY()
public:
AFWCallActor();
// 重写
virtual void FWCallFun(const FString& HitResult) override;
};
FWCallActor.cpp
// 引入头文件
#include "Common/FWCommon.h"
void AFWCallActor::FWCallFun(const FString& HitResult)
{
FWHelper::Debug("FWCallFun", 500.f);
}
来到 Blueprint/Interface 目录下创建 FWCallActor 的蓝图,取名为 FWCallActor_BP。
打开蓝图,可以试出来,继承的接口的方法没有 Event 的节点了。让 Event BeginPlay 节点调用 FWCall Fun (Interface Call)节点,然后将这个蓝图对象放置到场景。
运行游戏,可见左上角输出了 FWCallActor 重写的 FWCallFun()
里的 Debug 语句。
测试完了把 FWCall Fun (Interface Call) 节点删除。
这个是下一节课开头才讲的,为了连贯性我把它放在这里。
其实就是获取场景里的对象,然后强转为接口,以带前缀的方式调用方法。
FWCallActor.cpp
// 引入头文件
#include "FWInter/FWInterActor.h"
#include "Kismet/GameplayStatics.h"
void AFWCallActor::BeginPlay()
{
Super::BeginPlay();
TArray<AActor*> ActArray;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AFWInterActor::StaticClass(), ActArray);
if (ActArray.Num() > 0) {
// 强制转换,因为 Actor 继承了接口
IFWInterface* ActorPtr = Cast<IFWInterface>(ActArray[0]);
// 注意,UE4 要求要以下面这样带前缀的方式调用接口方法,第一个参数是继承了接口的对象,第二个参数开始才是接口方法需要的参数
// 这条 Debug 语句测试完可以注释掉
ActorPtr->Execute_FWFunThree(ActArray[0], FString("ssss"));
}
}
编译后,确保场景里有 FWCallActor_BP 和 FWInterActor_BP 的实例。运行游戏,可以看到左上角输出了 FWInterActor 的 FWFunThree()
方法里的 Debug 语句。
关于委托,笔者在《UE4开发C++沙盒游戏教程笔记(二)》 也有给出过推荐文章:《UE4中的委托及实现原理》
相信从那个系列笔记看过来的读者,现在回头再来看,应该会有新的理解 : )
在 Public/DeleEvent 路径下创建以下 C++ 类:
创建两个 Actor,分别命名为 FWDEActor 和 FWReceActor。
FWDEActor.h
// 无参数单播委托,即只能绑定一个方法
DECLARE_DELEGATE(FWDE_Single_Zero)
// 两个参数的单播委托
DECLARE_DELEGATE_TwoParams(FWDE_Single_Two, FString, int32)
// 无参数多播委托,多播其实就是可以绑定多个方法
DECLARE_MULTICAST_DELEGATE(FWDE_Multi_Zero)
// 注意,动态委托的结尾需要加分号 ";"
// 无参数动态单播委托
DECLARE_DYNAMIC_DELEGATE(FWDE_Dy_Sl_Zero);
// 一个参数的动态多播委托,第三个参数是参数的名字
// 老师把 Multi 命名成 Sl 了,这里我改过来
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWDE_Dy_Multi_One, FString, InfoStr);
UCLASS()
class FRAMECOURSE_API AFWDEActor : public AActor
{
GENERATED_BODY()
public:
AFWDEActor();
// 委托变量作为参数
UFUNCTION(BlueprintCallable, Category = "FrameWork")
void RegFunDel(FWDE_Dy_Sl_Zero TargetFun);
public:
// 对于委托变量,用 UPROPERTY() 宏的必须是动态多播委托,其他的不行
// 老师起名有点乱= =我这里就按自己的起名来了
UPROPERTY(BlueprintAssignable, Category = "FrameWork")
FWDE_Dy_Multi_One FDMOFun;
};
FWDEActor.cpp
void AFWDEActor::RegFunDel(FWDE_Dy_Sl_Zero TargetFun)
{
TargetFun.ExecuteIfBound();
}
在 Blueprint 目录下创建名为 DeleEvent 的文件夹,在它里面创建一个 FWDEActor 的蓝图,命名为 FWDEActor_BP。
打开蓝图,输入 RegFun 或者 FDMO 可以看到这些不同的节点:
梁迪老师说 UE4 的函数传递跟 C++ Boost 里的函数传递是差不多的,只不过笔者对 Boost 了解不多,也没有找到比较好的文章,还请读者自行了解。
在 FWDEActor 添加一个函数指针和一个接受函数指针为参数的方法。二者的函数指针指向的函数无返回值,并且只接受一个 FString 类型的参数。
FWDEActor.h
public:
// TFunction 传递函数
void RegFunOne(TFunction<void(FString)> TargetFun);
private:
// 函数指针
TFunction<void(FString)> TFunOne;
RegFunOne()
要做的是将形参赋给自身类的函数指针 TFunOne,然后给它注入 FString 类型的 “RegFunOne” 文本。
FWDEActor.cpp
void AFWDEActor::RegFunOne(TFunction<void(FString)> TargetFun)
{
TFunOne = TargetFun;
// 这里直接赋值调用,方便测试,实际上你可以在任何地方获取并调用它
TFunOne(FString("RegFunOne"));
}
来到 FWReceActor,添加一个方法,其作用是接收 FString 类型的参数并输出它。
FWReceActor.h
public:
void EchoInfoOne(FString InfoStr);
在 BeginPlay()
注入本地方法到 FWDEActor 接收函数指针的方法。
FWReceActor.cpp
// 引入头文件
#include "Common/FWCommon.h"
#include "Kismet/GameplayStatics.h"
#include "DeleEvent/FWDEActor.h"
void AFWReceActor::BeginPlay()
{
Super::BeginPlay();
TArray<AActor*> ActArray;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AFWDEActor::StaticClass(), ActArray);
if (ActArray.Num() > 0) {
AFWDEActor* DEActor = Cast<AFWDEActor>(ActArray[0]);
// 方法一:TFunction 的函数传递(此处用到了 Lambda 表达式)
DEActor->RegFunOne([this](FString InfoStr) { EchoInfoOne(InfoStr); });
}
}
void AFWReceActor::EchoInfoOne(FString InfoStr)
{
FWHelper::Debug(InfoStr, 500.f);
}
关于 Lambda 表达式,此处推荐阅读这篇文章:一文深入了解C++ lambda(C++17) 初看可能会有点绕,读者可以多看几遍做下笔记。
编译后,在 Blueprint/DeleEvent 目录下创建 FWReceActor 的蓝图,取名 FWReceActor_BP。随后将它和 FWDEActor_BP 都放入场景中。
运行游戏,可以看见左上角输出了 FWDEActor 类的 RegFunOne()
方法里给函数指针传入的 FString 文本。注释着重标明的这句代码解析如下:
this.EchoInfoOne(InfoStr)
。RegFunOne()
的逻辑:整个 Lambda 表达式传入给了 FWDEActor 的函数指针 TFunOne,最后调用这个函数指针,传入 FString 类型的 “RegFunOne” 文本,所以这个文本传给了 Lambda 表达式的 InfoStr,InfoStr 又作为 EchoInfoOne()
的实参。于是最后便输出了 “RegFunOne” 的 Debug 语句。函数传递能在一定程度上解耦,这个技巧也是梁迪老师的框架要用到的一部分。
TMemFunPtrType 是 UE4 提供的一个成员函数指针类型,它也可以用来传递函数。这里我们声明一个模板方法来测试一下。
TMemFunPtrType 的第一个 bool 值代表该函数指针是否为 const 函数;第二个则是要传递函数的类;第三个就是要传递的函数的签名。
typename 的作用就是将意义模糊的对象当作一个类型。
FWDEActor.h
UCLASS()
class FRAMECOURSE_API AFWDEActor : public AActor
{
GENERATED_BODY()
public:
// TMemFunPtrType 传递函数
template<class UserClass>
void RegFunTwo(UserClass* TarObj, typename TMemFunPtrType<false, UserClass, void(FString, int32)>::Type InMethod);
};
template<class UserClass>
void AFWDEActor::RegFunTwo(UserClass* TarObj, typename TMemFunPtrType<false, UserClass, void(FString, int32)>::Type InMethod)
{
FWDE_Single_Zero ExeDel;
ExeDel.BindUObject(TarObj, InMethod, FString("HAHAHA"), 54);
ExeDel.ExecuteIfBound();
}
来到 FWReceActor,声明一个方法用于传递给 FWDEActor 那边。
FWReceActor.h
public:
void EchoInfoTwo(FString InfoStr, int32 Count);
FWReceActor.cpp
void AFWReceActor::BeginPlay()
{
if (ActArray.Num() > 0) {
AFWDEActor* DEActor = Cast<AFWDEActor>(ActArray[0]);
//DEActor->RegFunOne([this](FString InfoStr) { EchoInfoOne(InfoStr); });
// 方法二: TMemFunPtrType 传递函数
DEActor->RegFunTwo(this, &AFWReceActor::EchoInfoTwo);
}
}
void AFWReceActor::EchoInfoTwo(FString InfoStr, int32 Count)
{
FWHelper::Debug(InfoStr + FString(" --> ") + FString::FromInt(Count), 500.f);
}
运行游戏,可以看到左上角输出了 FWDEActor 的 RegFunTwo()
内的提供的实参,说明函数传递成功了。
FMethodPtr 也是 UE4 提供的一个函数指针,不过它总是伴随着委托一起出现。
方法二和方法三的模板方法里面的逻辑的区别在于,前者在绑定的时候就需要提供参数了;后者可以在执行的时候才提供参数。
FWDEActor.h
UCLASS()
class FRAMECOURSE_API AFWDEActor : public AActor
{
GENERATED_BODY()
public:
// FMethodPtr 传递函数
template<class UserClass>
void RegFunThree(UserClass* TarObj, typename FWDE_Single_Two::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod);
};
template<class UserClass>
void AFWDEActor::RegFunThree(UserClass* TarObj, typename FWDE_Single_Two::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod)
{
FWDE_Single_Two ExeDel;
ExeDel.BindUObject(TarObj, InMethod);
ExeDel.ExecuteIfBound(FString("I am Xilibei"), 36);
}
FWReceActor.cpp
void AFWReceActor::BeginPlay()
{
if (ActArray.Num() > 0) {
AFWDEActor* DEActor = Cast<AFWDEActor>(ActArray[0]);
//DEActor->RegFunTwo(this, &AFWReceActor::EchoInfoTwo);
// 方法三:FMethodPtr 传递函数
DEActor->RegFunThree(this, &AFWReceActor::EchoInfoTwo);
}
}
编译后运行游戏,可以看到左上角输出了 FWDEActor 的 RegFunThree()
方法里面提供的实参,说明函数传递成功。
其实还可以通过泛型来定义统一接口,这样就可以传过去任意数量的参数了。
FWDEActor.h
UCLASS()
class FRAMECOURSE_API AFWDEActor : public AActor
{
GENERATED_BODY()
public:
// 泛型定义统一接口 FMethodPtr
template<class DelegateType, class UserClass, typename... VarTypes>
void RegFunFour(UserClass* TarObj, typename DelegateType::template TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, VarTypes... Vars);
};
template<class DelegateType, class UserClass, typename... VarTypes>
void AFWDEActor::RegFunFour(UserClass* TarObj, typename DelegateType::template TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, VarTypes... Vars)
{
FWDE_Single_Zero ExeDel;
ExeDel.BindUObject(TarObj, InMethod, Vars...);
ExeDel.ExecuteIfBound();
}
FWReceActor.cpp
void AFWReceActor::BeginPlay()
{
if (ActArray.Num() > 0) {
AFWDEActor* DEActor = Cast<AFWDEActor>(ActArray[0]);
//DEActor->RegFunThree(this, &AFWReceActor::EchoInfoTwo);
// 方法四:FMethodPtr 扩展
DECLARE_DELEGATE_TwoParams(FTempDele, FString, int32)
// 其实还可以把传参的步骤放在 RegFunFour 里边,作为小练习
DEActor->RegFunFour<FTempDele>(this, &AFWReceActor::EchoInfoTwo, FString("RegFunFour"), 56);
}
}
编译后运行游戏,可以看到左上角输出了 FWReceActor 的 BeginPlay()
方法里面提供给泛型方法 RegFunFour 的实参,说明函数传递成功。
先来到 FWReceActor 这边声明一个要传递的函数。
FWReceActor.h
public:
bool EchoInfoThree(FString InfoStr, int32 Count);
FWDEActor.h
#include "Common/FWCommon.h" //引入头文件
#include "FWDEActor.generated.h"
UCLASS()
class FRAMECOURSE_API AFWDEActor : public AActor
{
GENERATED_BODY()
public:
// 泛型定义统一接口 TFunction
template<typename RetType, typename... VarTypes>
void RegFunFive(TFunction<RetType(VarTypes...)> TarFun);
};
template<typename RetType, typename... VarTypes>
void AFWDEActor::RegFunFive(TFunction<RetType(VarTypes...)> TarFun)
{
// 只有知道了传递的函数的签名才知道如何传参
if (TarFun(FString("RegFunFive"), 78)) FWHelper::Debug("return true", 500.f);
}
FWReceActor.cpp
void AFWReceActor::BeginPlay()
{
if (ActArray.Num() > 0) {
AFWDEActor* DEActor = Cast<AFWDEActor>(ActArray[0]);
/*
DECLARE_DELEGATE_TwoParams(FTempDele, FString, int32)
DEActor->RegFunFour(this, &AFWReceActor::EchoInfoTwo, FString("RegFunFour"), 56);
*/
// 方法五: TFunction 的扩展(这里也用到了 Lambda 表达式)
// 测试完毕后注释掉该语句
DEActor->RegFunFive<bool, FString, int32>([this](FString InfoStr, int32 Count) { return EchoInfoThree(InfoStr, Count); });
}
}
bool AFWReceActor::EchoInfoThree(FString InfoStr, int32 Count)
{
FWHelper::Debug(InfoStr + FString(" --> ") + FString::FromInt(Count), 500.f);
return true;
}
编译后运行游戏,可以看到左上角输出了 FWDEActor 的泛型方法 RegFunFive()
里面传给 TarFun
的实参和自己的 Debug 语句,说明函数传递成功。
方法五目前其实是有缺陷的,就是必须要先知道传递函数的返回类型与参数类型才能正确传递,后续老师会继续改进。