UE4运用C++和框架开发坦克大战教程笔记(七)(第20~22集)
20. 框架对象类
接下来我们要创建剩余的所有要用到的框架 Object 类。
选择目标模块为 DataDriven (Runtime),在 Public/DDObject/ 路径下创建以下 C++ 类:(如果有弹窗则选 No)
创建 ActorComponent,取名为 DDActorComponent。
创建 AIController,取名为 DDAIController。
创建 CameraActor,取名为 DDCameraActor。
创建 Character,取名为 DDCharacter。
创建 GameInstance,取名为 DDGameInstance。
创建 GameModeBase,取名为 DDGameModeBase。
创建 GameStateBase,取名 DDGameStateBase。
创建 HUD,取名 DDHUD。
创建 LevelScriptActor,取名 DDLevelScriptActor。
创建 Pawn,取名 DDPawn。
创建 PlayerCameraManager,取名 DDPlayerCameraManager。
创建 PlayeController,取名 DDPlayerController。
创建 PlayerState,取名 DDPlayerState。
创建 SceneComponent,取名 DDSceneComponent。
创建 WheeledVehicle,取名 DDWheeledVehicle。
创建完以上所有类后,不用急着编译代码,为了让它们变成受框架控制的类,给它们执行如下操作:
- 引入 DDOO 头文件,继承 DDOO 接口。
- 去掉
Tick()
相关的函数,添加构造函数并在里面关闭原生 Tick(如果原本有Tick()
相关内容的话)。 - 添加三个 FName 变量。
- 在
BeginPlay()
里自动注册。(没有BeginPlay()
就重写一个) - 重写
DDRelease()
(这一步取决于类是否需要自动销毁)。
DDActorComponent 类的示例如下:
DDActorComponent.h
#include "DDOO.h" // 引入头文件
#include "DDActorComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DATADRIVEN_API UDDActorComponent : public UActorComponent, public IDDOO // 继承接口
{
GENERATED_BODY()
public:
UDDActorComponent();
// 重写释放函数
virtual void DDRelease() override;
public:
// 模组名字,如果为空,说明要手动指定,不为空就是自动指定
UPROPERTY(EditAnywhere, Category = "DataDriven")
FName ModuleName;
// 对象名字,如果为空,说明要手动指定,不为空就是自动指定
UPROPERTY(EditAnywhere, Category = "DataDriven")
FName ObjectName;
// 类名字,如果为空,说明要手动指定,不为空就是自动指定
UPROPERTY(EditAnywhere, Category = "DataDriven")
FName ClassName;
// 去掉 TickComponent()
};
DDActorComponent.cpp
UDDActorComponent::UDDActorComponent()
{
PrimaryComponentTick.bCanEverTick = false; // 关闭原生 Tick
// 设置允许销毁(只有 Component 类需要)
bAllowAnyoneToDestroyMe = true;
}
void UDDActorComponent::BeginPlay()
{
Super::BeginPlay();
// 自动注册
RegisterToModule(ModuleName, ObjectName, ClassName);
}
void UDDActorComponent::DDRelease()
{
IDDOO::DDRelease();
// 从组件中删除自己,并标记为准备被 gc 回收(只有 Component 类需要)
DestroyComponent();
}
// 删除 TickComponent()
不过有些类在上面的操作上有差异,以下是注意事项:
- 只有 Component 类需要在构造函数里设置允许销毁;在
DDRelease()
里调用DestroyComponent()
。 - 除了 Component 以外的类,它们的
DDRelease()
内的释放语句替换如下:
// 能调用这个方法那么一定是注册到了框架,获取的世界一定不为空
GetDDWorld()->DestroyActor(this);
- 需要重写
DDRelease()
的类:DDActorComponent、 DDCameraActor、DDCharacter、DDPawn、DDSceneComponent、DDWheeledVehicle 。其他的不需要。(需要注意的是类似 DDObject、DDUserWidget 等先前已经写过的类就不列出来了) - 除了 Component 以外的类,关闭原生 Tick 的调用对象是 PrimaryActorTick,示例如下:
PrimaryActorTick.bCanEverTick = false;
- DDCharacter 和 DDPawn 除了
Tick()
要去掉以外,绑定输入的SetupPlayerInputComponent()
也要去掉,因为后续会专门写一套绑定方法。 - DDGameInstance 只需要引入 DDOO 头文件并继承 DDOO 接口就行了。
- DDPlayerController 需要开启 Tick,示例如下:
DDPlayerController.cpp
ADDPlayerController::ADDPlayerController()
{
// 必须开启 Controller 或者 Character 的帧函数才能够检测按键,本框架只开启 Controller 的
PrimaryActorTick.bCanEverTick = true;
}
上面的内容如果读者不放心的话建议查看 DataDriven 源码或者根据视频对比着填写。
刚刚创建的载具类依赖 PhysXVehicles 模块,但是在 4.26~4.27 版本里这个模块即将被 ChaosVehicles 取代,不过后者在 UE4 处于 Beta 版,可能会在功能上出现不稳定性。
此外,笔者发现有用户遇到过 4.26~4.27 版本使用 ChaosVehicles 时出现问题:UE4.26 UE4.27 Chaos Vehicle 车辆运动组件无法控制车轮 不能正常使用的问题
需要在源码版引擎才能解决。衡量之下笔者决定还是采用旧版的 PhysXVehicles。
来到插件的 .Build.cs 文件,添加以下依赖:
DataDriven.Build.cs
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"UMG",
// 添加下面三个模块依赖
"AIModule",
"GameplayTasks",
"PhysXVehicles",
}
);
因为 DataDriven 本身是一个插件,PhysXVehicles 也是插件,插件间互相引用需要在 .uplugin 添加说明代码:
DataDriven.uplugin
{
"Modules": [
{
"Name": "DataDriven",
"Type": "Runtime",
"LoadingPhase": "Default"
}
], // 记得加这个逗号
// 添加下面这些代码
"Plugins": [
{
"Name": "PhysXVehicles",
"Enabled": true
}
]
}
PlayerController 经常会被调用到,我们可以把 DDPlayerController 注册到 DDCommon,以便可以在任何地方获取到。
再添加两个暂停游戏相关的方法。
DDCommon.h
class APlayerController; // 声明类
UCLASS()
class DATADRIVEN_API UDDCommon : public UObject
{
GENERATED_BODY()
public:
void InitController(APlayerController* InController);
APlayerController* GetController();
// 暂停游戏
void SetPauseGame(bool IsPause);
// 获取是否暂停了游戏
const bool IsPauseGame() const;
private:
APlayerController* PlayerController;
};
DDCommon.cpp
void UDDCommon::InitController(APlayerController* InController)
{
PlayerController = InController;
}
APlayerController* UDDCommon::GetController()
{
return PlayerController;
}
void UDDCommon::SetPauseGame(bool IsPause)
{
PlayerController->SetPause(IsPause);
}
const bool UDDCommon::IsPauseGame() const
{
return PlayerController->IsPaused();
}
为了让 DDPlayerController 和 DDGameInstance 这类 Gameplay 部分能够运用到游戏中,我们需要让 Driver 来执行注册。
DDDriver.h
protected:
// 注册 GamePlay 框架到 DataDriven
void RegisterGamePlay();
DDDriver.cpp
// 引入头文件
#include "Kismet/GameplayStatics.h"
void ADDDriver::BeginPlay()
{
Super::BeginPlay();
// 注册 GamePlay 到框架
RegisterGamePlay();
Center->IterModuleInit(Center);
}
void ADDDriver::RegisterGamePlay()
{
// 获取 GameInstance
UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(GetWorld());
// 如果存在并且继承自 IDDOO,就注册进 Center,类名和对象名都是 GameInstance
if (GameInstance && Cast<IDDOO>(GameInstance))
// 老师这里把 GameInstance 拼写错了
Cast<IDDOO>(GameInstance)->RegisterToModule("Center", "GameInstance", "GameInstance");
// 获取 Controller 并且注册到 DDCommon
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
// 注册到 Common
if (!PlayerController)
DDH::Debug() << "No PlayerController" << DDH::Endl();
else
UDDCommon::Get()->InitController(PlayerController);
}
如果编译通过则说明没有问题(笔者尝试的时候编译了两次以上才成功)。后面的课程我们会使用现有的框架内容搭建游戏内容。
21. 模组反射方法调用
下图截取自梁迪老师的 DataDriven 文档。
上面的反射事件调用过程参考之前 FrameCourse 项目里 ADecActor::RunFunThree()
里面写的逻辑。
接下来我们构建这样一套反射事件系统,先试试让框架对象调用所属模组的方法。
来到 DDTypes,定义一个枚举,代表调用反射事件的结果。
再声明三个结构体,一个用来作为通信时携带参数和调用结果的载体,另外两个分别是模组和对象的通信协议。
DDTypes.h
// 调用结果,项目开发时请确保每次都能调用成功
UENUM()
enum class ECallResult : uint8
{
NoModule = 0, // 缺失模组
LackObject, // 缺失部分对象(不要改 No)
NoFunction, // 缺失方法
Succeed // 调用成功
};
// 通信参数结构体基类
struct DDParam
{
public:
// 调用结果
ECallResult CallResult;
// 参数指针
void* ParamPtr;
};
// 通信协议,Module 方法
struct DDModuleAgreement
{
public:
// 模组 ID
int32 ModuleIndex;
// 方法名
FName FunctionName;
};
// 通信协议,DDOO 方法
struct DDObjectAgreement
{
public:
// 模组 ID
int32 ModuleIndex;
// 协议类型
EAgreementType AgreementType;
// 对象组名
TArray<FName> ObjectGroup;
// 方法名
FName FunctionName;
};
在 DDModule 添加一个方法用于执行模组内的对应方法。
顺便添加一个测试用的方法,供对象调用。
DDModule.h
public:
// 调用模组方法
void ExecuteFunction(DDModuleAgreement Agreement, DDParam* Param);
// 临时方法,测试反射事件系统
// bool 传引用的意义是充当返回值,后面才会用到
UFUNCTION()
void TestReflect(int32 Counter, FString InfoStr, bool& BackResult);
DDModule.cpp
void UDDModule::ExecuteFunction(DDModuleAgreement Agreement, DDParam* Param)
{
// 调用 Module 的 UFunction
UFunction* ExeFunc = FindFunction(Agreement.FunctionName);
// 如果方法存在
if (ExeFunc) {
// 设置调用成功
Param->CallResult = ECallResult::Succeed;
// 调用方法
ProcessEvent(ExeFunc, Param->ParamPtr);
}
else {
// 设置方法不存在
Param->CallResult = ECallResult::NoFunction;
}
}
void UDDModule::TestReflect(int32 Counter, FString InfoStr, bool& BackResult)
{
DDH::Debug() << Counter << " ; " << InfoStr << " ; " << GetFName() << DDH::Endl();
}
对象通过反射调用所属模组的方法
给接口类 DDOO 添加一个方法,只要对象接收到传过来的模组序号跟自己的所属模组序号一样,就传递执行所属模组的对应方法。
DDOO.h
protected:
// 执行反射方法
void ExecuteFunction(DDModuleAgreement Agreement, DDParam* Param);
DDOO.cpp
void IDDOO::ExecuteFunction(DDModuleAgreement Agreement, DDParam* Param)
{
if (Agreement.ModuleIndex == ModuleIndex)
IModule->ExecuteFunction(Agreement, Param);
}
来到 LifeCallActor,定义一个继承自 DDParam 的结构体,在里面定义一个 “包含调用方法所需的变量” 的匿名结构体后,在结构体的构造函数中让继承自父结构体 DDParam 的 void*
参数指针指向这个匿名结构体,使 ParamPtr
作为参数载体。
再定义一个方法来装载各种变量,最后通过 DDOO 传递方法执行,然后销毁临时的 Param 对象。
LifeCallActor.h
protected:
struct TestReflectParam : DDParam
{
struct
{
int32 Counter;
FString InfoStr;
bool BackResult;
} Parameter;
int32 Counter() { return Parameter.Counter; }
FString InfoStr() { return Parameter.InfoStr; }
bool BackResult() { return Parameter.BackResult; }
TestReflectParam() { ParamPtr = &Parameter; }
};
// 笔者编译时 ModuleIndex 这个名字跟 DDOO 的变量重名了,无法通过编译,所以作此修改
void TestReflect(int32 ModuleIndex1, FName FunctionName, int32 Counter, FString InfoStr, bool BackResult)
{
DDModuleAgreement Agreement;
Agreement.ModuleIndex = ModuleIndex1; // 作修改,后面笔记的都是如此
Agreement.FunctionName = FunctionName;
TestReflectParam* Param = new TestReflectParam();
Param->Parameter.Counter = Counter;
Param->Parameter.InfoStr = InfoStr;
Param->Parameter.BackResult = BackResult;
ExecuteFunction(Agreement, Param); // 调用 DDOO 的方法
delete Param;
}
准备工作都做好了,直接在 DDEnable()
这里调用来测试一下。
LifeCallActor.cpp
void ALifeCallActor::DDEnable()
{
TestReflect(ModuleIndex, "TestReflect", 13, "Happy", true);
}
编译后运行游戏,可以看到左上角 Debug 语句,DDEnable 之后输出了 ALifeCallActor::DDEnable()
提供的参数,说明对象通过反射调用所属模组的方法成功了。
22. 模组反射系统宏定义
通过引用形参来充当返回值
前面说到 bool 的传引用是为了充当返回值,因为通过反射调用的模组的方法是不可以带返回值的,不过我们可以在方法里面直接对引用形参进行修改。
给 DDModule 的这个测试方法结尾添加一个对 bool 引用的赋值语句。
DDModule.cpp
void UDDModule::TestReflect(int32 Counter, FString InfoStr, bool& BackResult)
{
DDH::Debug() << Counter << " ; " << InfoStr << " ; " << GetFName() << DDH::Endl();
BackResult = false; // 赋值
}
来到 LifeCallActor,定义一个带返回值的方法。
LifeCallActor.h
protected:
// 跟上一个方法的区别在于有返回类型,以及最后是返回 Param 而不是删除
TestReflectParam* TestReflectRT(int32 ModuleIndex1, FName FunctionName, int32 Counter, FString InfoStr, bool BackResult)
{
DDModuleAgreement Agreement;
Agreement.ModuleIndex = ModuleIndex1;
Agreement.FunctionName = FunctionName;
TestReflectParam* Param = new TestReflectParam();
Param->Parameter.Counter = Counter;
Param->Parameter.InfoStr = InfoStr;
Param->Parameter.BackResult = BackResult;
ExecuteFunction(Agreement, Param);
return Param;
}
既然是带返回值的,那就声明一个变量来接收。
传入 bool 值为 true,并且添加一条 Debug 语句方便查看效果。并且最后要删除掉接收的变量,释放资源。
LifeCallActor.cpp
void ALifeCallActor::DDEnable()
{
// 修改原来的调用语句如下
TestReflectParam* ResultParam = TestReflectRT(ModuleIndex, "TestReflect", 13, "Happy", true);
DDH::Debug() << "ResultParam --> " << ResultParam->BackResult() << DDH::Endl();
delete ResultParam;
}
编译后运行,可见最后的输出结果是 false,说明用引用来充当返回值的方法成功了。
对象调用所属模组以外的模组的方法
前面我们尝试的两个情况都是 “对象调用所属模组的方法”,接下来我们试一下通过指定目标模组来调用它的方法。
LifeCallActor.cpp
void ALifeCallActor::DDEnable()
{
// 修改原来的调用语句如下
// 通过枚举值转整型来选中目标模组 HUD
TestReflectParam* ResultParam = TestReflectRT((int32)ERCGameModule::HUD, "TestReflect", 13, "Happy", true);
DDH::Debug() << "ResultParam --> " << ResultParam->BackResult() << DDH::Endl();
delete ResultParam;
}
对象只属于一个模组,如果要让对象调用模组的同级,那就要经过上一层的中央模组来作中介。不过接口 DDOO 只保存了所属模组和 Driver,所以我们可以通过 Driver 调用中央模组,再调用目标模组的方法。
来到中央模组,声明一个执行反射过程的方法。
DDCenterModule.h
public:
// 执行反射方法
void AllotExecuteFunction(DDModuleAgreement Agreement, DDParam* Param);
DDCenterModule.cpp
void UDDCenterModule::AllotExecuteFunction(DDModuleAgreement Agreement, DDParam* Param)
{
// 如果传进来的模组序号 不超过 中央模组保存的模组数量 并且 目标模组存在
if (Agreement.ModuleIndex < ModuleGroup.Num() && ModuleGroup[Agreement.ModuleIndex])
ModuleGroup[Agreement.ModuleIndex]->ExecuteFunction(Agreement, Param);
// 否则就返回调用结果为 缺失模组
else
Param->CallResult = ECallResult::NoModule;
}
来到 Driver 添加一个传递执行反射的方法。
DDDriver.h
public:
// 执行反射方法
void ExecuteFunction(DDModuleAgreement Agreement, DDParam* Param);
DDDriver.cpp
void ADDDriver::ExecuteFunction(DDModuleAgreement Agreement, DDParam* Param)
{
Center->AllotExecuteFunction(Agreement, Param);
}
在 DDOO 补充执行反射方法的逻辑:如果传进来的模组不是所属模组,就让 Driver 调用执行反射的方法。
DDOO.cpp
void IDDOO::ExecuteFunction(DDModuleAgreement Agreement, DDParam* Param)
{
if (Agreement.ModuleIndex == ModuleIndex)
IModule->ExecuteFunction(Agreement, Param);
// 补充
else
IDriver->ExecuteFunction(Agreement, Param);
}
编译后运行游戏,可以看见原来 Debug 输出的 “13 ; Happy ; Center” 变成了 “13 ; Happy ; HUD”。说明对象通过 Driver -> 中央模组 -> 目标模组 的反射调用成功了。
定义更加通用的宏来优化反射方法
前面我们在 LifeCallActor 里专门声明了一个结构体和方法来支持执行反射方法,看起来还是比较繁琐的。所以我们接下来尝试利用宏来让这个过程变得更通用。
来到 DDDefine 里面定义这个宏。
DDDefine.h
UCLASS()
class DATADRIVEN_API UDDDefine : public UObject
{
GENERATED_BODY()
};
// 宏定义必须是一行的,为了不至于太长,可以在每行结尾都加个反斜杠来承接下一行
// FuncName 代表函数名称,ParamType 代表参数类型,ParamName 代表参数名字
// FuncName##Param 代表函数名跟这个 Param 连接起来,如果 FuncName 是 AddOn 那等同于 AddOnParam
#define DDMODFUNC_THREE(FuncName, ParamType1, ParamName1, ParamType2, ParamName2, ParamType3, ParamName3); \
struct FuncName##Param : DDParam \
{ \
struct \
{ \
ParamType1 ParamName1; \
ParamType2 ParamName2; \
ParamType3 ParamName3; \
} Parameter; \
ParamType1 ParamName1() { return Parameter.ParamName1; } \
ParamType2 ParamName2() { return Parameter.ParamName2; } \
ParamType3 ParamName3() { return Parameter.ParamName3; } \
FuncName##Param() { ParamPtr = &Parameter; } \
}; \
FuncName##Param* FuncName##RT(int32 ModuleIndex1, FName FunctionName, ParamType1 ParamName1, ParamType2 ParamName2, ParamType3 ParamName3) \
{ \
DDModuleAgreement Agreement; \
Agreement.ModuleIndex = ModuleIndex1; \
Agreement.FunctionName = FunctionName; \
FuncName##Param* Param = new FuncName##Param(); \
Param->Parameter.ParamName1 = ParamName1; \
Param->Parameter.ParamName2 = ParamName2; \
Param->Parameter.ParamName3 = ParamName3; \
ExecuteFunction(Agreement, Param); \
return Param; \
} \
void FuncName(int32 ModuleIndex1, FName FunctionName, ParamType1 ParamName1, ParamType2 ParamName2, ParamType3 ParamName3) \
{ \
DDModuleAgreement Agreement; \
Agreement.ModuleIndex = ModuleIndex1; \
Agreement.FunctionName = FunctionName; \
FuncName##Param* Param = new FuncName##Param(); \
Param->Parameter.ParamName1 = ParamName1; \
Param->Parameter.ParamName2 = ParamName2; \
Param->Parameter.ParamName3 = ParamName3; \
ExecuteFunction(Agreement, Param); \
delete Param; \
}
LifeCallActor.h
protected:
DDMODFUNC_THREE(TestReflect, int32, Counter, FString, InfoStr, bool, BackResult);
// 去掉原来的一大段反射调用代码
编译后运行游戏,如果左上角输出跟上一次运行游戏一样,就说明代码无误,利用宏简化执行反射方法成功。
接下来我们试一下定义一个不传参数的宏。
DDDefine.h
#define DDMODFUNC(FuncName); \
struct FuncName##Param : DDParam \
{ \
FuncName##Param() { ParamPtr = NULL; } \
}; \
FuncName##Param* FuncName##RT(int32 ModuleIndex1, FName FunctionName) \
{ \
DDModuleAgreement Agreement; \
Agreement.ModuleIndex = ModuleIndex1; \
Agreement.FunctionName = FunctionName; \
FuncName##Param* Param = new FuncName##Param(); \
ExecuteFunction(Agreement, Param); \
return Param; \
} \
void FuncName(int32 ModuleIndex1, FName FunctionName) \
{ \
DDModuleAgreement Agreement; \
Agreement.ModuleIndex = ModuleIndex1; \
Agreement.FunctionName = FunctionName; \
FuncName##Param* Param = new FuncName##Param(); \
ExecuteFunction(Agreement, Param); \
delete Param; \
}
在模组类添加一个不带参数的、供调用的方法。
DDModule.h
public:
UFUNCTION()
void TestNoParam();
DDModule.cpp
void UDDModule::TestNoParam()
{
DDH::Debug() << "No Param" << DDH::Endl();
}
在 LifeCallActor 来调用这个宏。
LifeCallActor.h
protected:
DDMODFUNC(HappyFunc);
LifeCallActor.cpp
void ALifeCallActor::DDEnable()
{
//TestReflectParam* ResultParam = TestReflectRT((int32)ERCGameModule::HUD, "TestReflect", 13, "Happy", true);
//DDH::Debug() << "ResultParam --> " << ResultParam->BackResult() << DDH::Endl();
//delete ResultParam;
HappyFunc((int32)ERCGameModule::HUD, "TestNoParam");
}
编译后运行游戏,可以看到原来 Debug 语句输出 “13 ; Happy ; HUD” 的位置变成了 “No Param”,说明通过反射调用不带参数的方法也成功了。
给 DDDefine 添加剩余的代码。不过内容太多,笔者这里就不贴出来了。建议是复制老师的 Github 项目代码 或者是备份项目里的代码。复制完毕后大概是 547 行左右。