赞
踩
上节课实现了按键绑定系统的 4 种基础绑定,这节课来实现多按键事件的绑定。
我们为多按键绑定额外编写一个类 InputBinder
。
DDMessage.h
// 多按键输入绑定类 #pragma region InputBinder DECLARE_DELEGATE(FDDInputEvent) // 用于绑定多按键的目标方法 UCLASS() class DATADRIVEN_API UDDInputBinder : public UObject { GENERATED_BODY() public: UDDInputBinder(); void PressEvent(); // 按下事件(多按键的每个按键都要绑定它) void ReleaseEvent(); // 松开事件(同上) public: uint8 InputCount; // 目前正在响应的按键数量 uint8 TotalCount; // 触发事件所需要响应的按键数量 // 游戏暂停时是否执行按键事件 uint8 bExecuteWhenPause; FDDInputEvent InputDele; // 委托句柄 public: // 模板初始化方法 template<class UserClass> void InitBinder(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, uint8 InCount) { TotalCount = InCount; InputDele.BindUObject(UserObj, InMethod); } }; #pragma endregion UCLASS() class DATADRIVEN_API UDDMessage : public UObject, public IDDMM { GENERATED_BODY() public: // 绑定多个按键 template<class UserClass> UDDInputBinder& BindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, TArray<FKey>& KeyGroup, FName ObjectName); // 解绑对象的所有按键事件 void UnBindInput(FName ObjectName); protected: // 绑定按键事件序列,键是按键事件名,值是按键事件的数组 TMap<FName, TArray<UDDInputBinder*>> BinderGroup; }; template<class UserClass> UDDInputBinder& UDDMessage::BindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, TArray<FKey>& KeyGroup, FName ObjectName) { UDDInputBinder* InputBinder = NewObject<UDDInputBinder>(); InputBinder->InitBinder(UserObj, InMethod, KeyGroup.Num()); InputBinder->AddToRoot(); // 避免被 GC // 给所有目标按钮绑定按下和抬起的方法 for (int i = 0; i < KeyGroup.Num(); ++i) { PlayerController->InputComponent->BindKey(KeyGroup[i], IE_Pressed, InputBinder, &UDDInputBinder::PressEvent).bExecuteWhenPaused = true; PlayerController->InputComponent->BindKey(KeyGroup[i], IE_Released, InputBinder, &UDDInputBinder::ReleaseEvent).bExecuteWhenPaused = true; } if (!BinderGroup.Contains(ObjectName)) { TArray<UDDInputBinder*> BinderList; BinderGroup.Add(ObjectName, BinderList); } BinderGroup.Find(ObjectName)->Push(InputBinder); return *InputBinder; }
DDMessage.cpp
UDDInputBinder::UDDInputBinder() { InputCount = 0; bExecuteWhenPause = false; } void UDDInputBinder::PressEvent() { InputCount++; // 如果 InputCount 与 TotalCount 相等,说明所有按键都按下了 if (InputCount == TotalCount) { // 如果允许在暂停时执行 if (bExecuteWhenPause) InputDele.ExecuteIfBound(); else if (!bExecuteWhenPause && !UDDCommon::Get()->IsPauseGame()) InputDele.ExecuteIfBound(); } } void UDDInputBinder::ReleaseEvent() { InputCount--; } void UDDMessage::UnBindInput(FName ObjectName) { if (!BinderGroup.Contains(ObjectName)) return; TArray<UDDInputBinder*> BinderList = *BinderGroup.Find(ObjectName); for (int i = 0; i < BinderList.Num(); ++i) { BinderList[i]->RemoveFromRoot(); // 移出 Root 以便被 GC BinderList[i]->ConditionalBeginDestroy(); // 申请销毁 } BinderGroup.Remove(ObjectName); }
依旧是部署好 DDMessage – DDModule – DDOO – 对象 这条调用链。
DDModule.h
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class DATADRIVEN_API UDDModule : public USceneComponent { GENERATED_BODY() public: // 绑定多个按键 template<class UserClass> UDDInputBinder& BindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, TArray<FKey>& KeyGroup, FName ObjectName); // 解绑对象的所有按键事件 void UnBindInput(FName ObjectName); }; template<class UserClass> UDDInputBinder& UDDModule::BindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, TArray<FKey>& KeyGroup, FName ObjectName) { return Message->BindInput(UserObj, InMethod, KeyGroup, ObjectName); }
DDModule.cpp
void UDDModule::UnBindInput(FName ObjectName)
{
Message->UnBindInput(ObjectName);
}
DDOO.h
class DATADRIVEN_API IDDOO { GENERATED_BODY() protected: // 给之前的绑定方法的名字都添加 DD 前缀,避免与 UE4 原生的方法名重复 template<class UserClass> FInputAxisBinding& DDBindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName); template<class UserClass> FInputTouchBinding& DDBindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent); template<class UserClass> FInputActionBinding& DDBindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent); template<class UserClass> FInputKeyBinding& DDBindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent); // 绑定 2 个按键(注意绑定多个按键时传入的形参不一样了) template<class UserClass> UDDInputBinder& DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II); // 绑定 3 个按键,下面的也差不多,就是多传了一两个按键形参 template<class UserClass> UDDInputBinder& DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III); template<class UserClass> UDDInputBinder& DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III, FKey Key_IV); template<class UserClass> UDDInputBinder& DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III, FKey Key_IV, FKey Key_V); // 解绑该对象的所有多个按键 void UnBindInput(); }; // 模板方法的实现里,名字也同步添加前缀 template<class UserClass> FInputAxisBinding& IDDOO::DDBindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName) { return IModule->BindAxis(UserObj, InMethod, AxisName); } template<class UserClass> FInputTouchBinding& IDDOO::DDBindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent) { return IModule->BindTouch(UserObj, InMethod, KeyEvent); } template<class UserClass> FInputActionBinding& IDDOO::DDBindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent) { return IModule->BindAction(UserObj, InMethod, ActionName, KeyEvent); } template<class UserClass> FInputKeyBinding& IDDOO::DDBindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent) { // 绑定单个按钮和多按钮时,调用的 BindInput() 不一样 return IModule->BindInput(UserObj, InMethod, Key, KeyEvent); } // 下面的方法都是绑定多按键的模板方法 template<class UserClass> UDDInputBinder& IDDOO::DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II) { TArray<FKey> KeyGroup; KeyGroup.Push(Key_I); KeyGroup.Push(Key_II); // 从传入的参数可以看出来 return IModule->BindInput(UserObj, InMethod, KeyGroup, GetObjectName()); } template<class UserClass> UDDInputBinder& IDDOO::DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III) { TArray<FKey> KeyGroup; KeyGroup.Push(Key_I); KeyGroup.Push(Key_II); KeyGroup.Push(Key_III); return IModule->BindInput(UserObj, InMethod, KeyGroup, GetObjectName()); } template<class UserClass> UDDInputBinder& IDDOO::DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III, FKey Key_IV) { TArray<FKey> KeyGroup; KeyGroup.Push(Key_I); KeyGroup.Push(Key_II); KeyGroup.Push(Key_III); KeyGroup.Push(Key_IV); return IModule->BindInput(UserObj, InMethod, KeyGroup, GetObjectName()); } template<class UserClass> UDDInputBinder& IDDOO::DDBindInput(UserClass* UserObj, typename FDDInputEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, FKey Key_I, FKey Key_II, FKey Key_III, FKey Key_IV, FKey Key_V) { TArray<FKey> KeyGroup; KeyGroup.Push(Key_I); KeyGroup.Push(Key_II); KeyGroup.Push(Key_III); KeyGroup.Push(Key_IV); KeyGroup.Push(Key_V); return IModule->BindInput(UserObj, InMethod, KeyGroup, GetObjectName()); }
为了避免出现忘记注销事件指针导致访问错误位置的情况,我们在 DDRelease()
里注销前面写的三个系统的所有事件。
DDOO.cpp
void IDDOO::DDRelease()
{
// 注销所有协程,延时以及按键事件(也可以分别在其他地方调用)
StopAllCoroutine();
StopAllInvoke();
UnBindInput();
}
void IDDOO::UnBindInput()
{
IModule->UnBindInput(GetObjectName());
}
最后我们来验证一下多按键事件绑定是否正常运作。
CoroActor.h
protected:
void MultiKeyEvent();
CoroActor.cpp
void ACoroActor::DDEnable()
{
// 加前缀,但不运行这个单按键绑定
//DDBindInput(this, &ACoroActor::BKeyEvent, EKeys::B, IE_Pressed);
DDBindInput(this, &ACoroActor::MultiKeyEvent, EKeys::J, EKeys::K, EKeys::L);
}
void ACoroActor::MultiKeyEvent()
{
DDH::Debug() << "MultiKeyEvent" << DDH::Endl();
}
编译运行,同时按 J、K、L 三个键,左上角才会输出 “MultiKeyEvent”。说明多按键绑定功能也编写好了。
下图截取自梁迪老师的 DataDriven 说明文档:
关于资源加载方式,读者可以回顾课程的第 8 集和第 9 集,里面讲到了资源的同异步加载。
开发资源加载系统,我们首先要定义一个专用于保存资源数据的数据类,以及与之配套的结构体。目前我们先考虑生成以下 3 种类型的对象:Object、Actor 和 Widget。
DDTypes.h
#include "Engine/DataAsset.h" // 引入头文件 #include "DDTypes.generated.h" #pragma region Wealth USTRUCT() // 用这个宏说明结构体可与蓝图交互 struct FWealthItem // 资源基类结构体 { GENERATED_BODY() public: // 对象名 UPROPERTY(EditAnywhere) FName ObjectName; // 类名 UPROPERTY(EditAnywhere) FName ClassName; }; USTRUCT() struct FWealthObject : public FWealthItem // Object 类型资源的结构体 { GENERATED_BODY() public: UPROPERTY(EditAnywhere) TSubclassOf<UObject> WealthClass; }; USTRUCT() struct FWealthActor : public FWealthItem // Actor 类型资源的结构体 { GENERATED_BODY() public: UPROPERTY(EditAnywhere) TSubclassOf<AActor> WealthClass; UPROPERTY(EditAnywhere) FTransform Transform; }; USTRUCT() struct FWealthWidget : public FWealthItem // Widget 类型资源的结构体 { GENERATED_BODY() public: UPROPERTY(EditAnywhere) TSubclassOf<UUserWidget> WealthClass; }; UCLASS() class DATADRIVEN_API UWealthData : public UDataAsset // 继承自原生的数据类 { GENERATED_BODY() public: // 模组名字,这个 DataAsset 下的资源生成的对象默认注册到 ModuleName 对应的模组 // 如果为空(None)则说明该 Asset 使用于多个模组下,自动生成的对象注册到该 Asset 放置的模组下 UPROPERTY(EditAnywhere) FName ModuleName; // 自动生成的 Object UPROPERTY(EditAnywhere) TArray<FWealthObject> AutoObjectData; // 自动生成的 Actor UPROPERTY(EditAnywhere) TArray<FWealthActor> AutoActorData; // 自动生成的 Widget UPROPERTY(EditAnywhere) TArray<FWealthWidget> AutoWidgetData; }; #pragma endregion
要在 C++ 内获取到蓝图里的内容,则需要让 DDModule 拥有一个暴露给蓝图的资源组,这样就可以在编辑器内对资源组配置目标对象。
DDModule.h
public:
// 暴露给蓝图的资源组
UPROPERTY(EditAnywhere, Category = "DataDriven")
TArray<UWealthData*> WealthData;
TArray<UDDModule*> ChildrenModule;
// 不再将 ModuleIndex 暴露给蓝图
int32 ModuleIndex;
自动生成资源的逻辑放在 DDWealth 里,所以需要将资源组从 DDModule 赋值给 DDWealth。
DDWealth.h
public:
// 指定资源组
void AssignData(TArray<UWealthData*>& InWealthData);
protected:
// 资源组
TArray<UWealthData*> WealthData;
// Widget 类型对象专属,保存 Widget 指针,放止被 GC
UPROPERTY()
TArray<UUserWidget*> GCWidgetGroup;
由于生成 Actor 对象需要获取世界,所以我们在 DDMM 里添加一个获取世界的方法。
DDMM.h
protected:
// 获取世界
UWorld* GetDDWorld() const;
DDMM.cpp
UWorld* IDDMM::GetDDWorld() const
{
if (IDriver)
return IDriver->GetWorld();
return NULL;
}
在 DDWealth.cpp 里补全资源生成逻辑,即遍历所配置的资源组并生成里面的 3 种类型的所有对象。
DDWealth.cpp
// 引入头文件 #include "DDObject/DDOO.h" #include "Blueprint/UserWidget.h" void UDDWealth::WealthBeginPlay() { // 遍历自动生成对象 for (int i = 0; i < WealthData.Num(); ++i) { // 生成 Object 对象 for (int j = 0; j < WealthData[i]->AutoObjectData.Num(); ++j) { // 根据获取到的 UClass 生成指定的对象 UObject* NewObj = NewObject<UObject>(this, WealthData[i]->AutoObjectData[j].WealthClass); NewObj->AddToRoot(); IDDOO* InstPtr = Cast<IDDOO>(NewObj); // 注册到框架 if (InstPtr) { InstPtr->RegisterToModule( WealthData[i]->ModuleName.IsNone() ? IModule->GetFName() : WealthData[i]->ModuleName, WealthData[i]->AutoObjectData[j].ObjectName, WealthData[i]->AutoObjectData[j].ClassName ); } } // 生成 Actor 对象 for (int j = 0; j < WealthData[i]->AutoActorData.Num(); ++j) { AActor* NewAct = GetDDWorld()->SpawnActor<AActor>(WealthData[i]->AutoActorData[j].WealthClass, WealthData[i]->AutoActorData[j].Transform); IDDOO* InstPtr = Cast<IDDOO>(NewAct); if (InstPtr) { InstPtr->RegisterToModule( WealthData[i]->ModuleName.IsNone() ? IModule->GetFName() : WealthData[i]->ModuleName, WealthData[i]->AutoActorData[j].ObjectName, WealthData[i]->AutoActorData[j].ClassName ); } } // 生成 Widget 对象 for (int j = 0; j < WealthData[i]->AutoWidgetData.Num(); ++j) { UUserWidget* NewWidget = CreateWidget<UUserWidget>(GetDDWorld(), WealthData[i]->AutoWidgetData[j].WealthClass); // 避免回收(AddToRoot() 不适用于 Widget,即便加了也会被回收) GCWidgetGroup.Push(NewWidget); IDDOO* InstPtr = Cast<IDDOO>(NewWidget); if (InstPtr) { InstPtr->RegisterToModule( WealthData[i]->ModuleName.IsNone() ? IModule->GetFName() : WealthData[i]->ModuleName, WealthData[i]->AutoWidgetData[j].ObjectName, WealthData[i]->AutoWidgetData[j].ClassName ); } } } } void UDDWealth::AssignData(TArray<UWealthData*>& InWealthData) { WealthData = InWealthData; }
在 DDModule 的 BeginPlay()
里给 DDWealth 指定资源组。
DDModule.cpp
void UDDModule::ModuleBeginPlay()
{
// 给 Wealth 指定资源
Wealth->AssignData(WealthData);
// ... 省略
}
编译如果没有问题的话,我们将验证部分留到下一节课。
接下来准备一下将要生成在场景中的资源。我们打算生成资源的种类有:Object、Actor、Pawn(实际上也是 Actor)和 Widget。由于 Object 是不允许直接生成在场景中的,我们需要创建它的蓝图然后将其生成在场景中。
创建一个以 DDObject 为基类的 C++ 类,目标模组选项目,命名为 WealthCallObject,直接在默认路径创建。显示自动热重载失败,不用管,直接点 No。
创建一个以 DDActor 为基类的 C++ 类,命名为 TestWealthActor,负责调用被生成对象带有的,用于输出 Debug 语句的蓝图方法。
WealthCallObject.h
UCLASS(Blueprintable, BlueprintType) // 添加两个说明符,以便能跟蓝图交互
class RACECARFRAME_API UWealthCallObject : public UDDObject
{
GENERATED_BODY()
};
TestWealthActor.h
UCLASS()
class RACECARFRAME_API ATestWealthActor : public ADDActor
{
GENERATED_BODY()
public:
virtual void DDEnable() override;
protected:
DDOBJFUNC(CallWealth);
};
TestWealthActor.cpp
void ATestWealthActor::DDEnable()
{
Super::DDEnable();
}
编译后,在 Blueprint 文件夹内创建蓝图,它们都是待会要生成的对象:
以 DDActor 为基类创建一个蓝图,命名为 WealthCallActor
以 DDPawn 为基类创建一个蓝图,命名为 WealthCallPawn。
以 DDUserWidget 为基类创建一个蓝图,命名为 WealthCallWidget。
以 WealthCallObject 为基类创建一个蓝图,命名为 WealthCallObject。
随后给它们添加一些方便观察是否正常生成的模型或者输出 Debug 语句的方法:(图片可能会有些糊,两个模型也可以随便选,重要的是 4 个输出的方法)
在 Blueprint 文件夹下创建两个 DataAsset,都以 WealthData 为基类,分别命名为 PlayerData 和 HUDData。
然后对两个 DataAsset 设置一下内容,再配置到 GameDriver_BP 的两个模组上:
在 TestWealthActor.cpp 里的 DDEnable()
里补充对 4 种资源输出方法的调用。
TestWealthActor.cpp
void ATestWealthActor::DDEnable()
{
Super::DDEnable();
// 调用 4 个生成对象的输出方法(测试完了需要注释掉)
CallWealth((int32)ERCGameModule::Player, "WealthCallObject", "CallObject");
CallWealth((int32)ERCGameModule::Player, "WealthCallActor", "CallActor");
CallWealth((int32)ERCGameModule::Player, "WealthCallPawn", "CallPawn");
CallWealth((int32)ERCGameModule::HUD, "WealthCallWidget", "CallWidget");
}
编译后,在 Blueprint 文件夹下创建一个以 TestWealthActor 为基类的蓝图,命名为 TestWealthActor_BP。给其细节面板修改如下:
将它放到场景中,运行游戏,可见左上角输出了 4 条 Debug 语句,并且场景中也生成了目标 Actor 和 Pawn,界面上也正确生成了两个按钮。
我们来梳理下脉络:GameDriver_BP 下的 Center 模组内有 Player 模组和 HUD 模组,这两个模组内分别配置了 PlayerData 和 HUDData,里面存储了要生成的对象。两个模组将各自的 DataAsset 传给各自的 DDWealth 来执行生成逻辑。
资源对象生成后,Object、Actor、Pawn 这三个对象属于 Player 模组,Widget 属于 HUD 模组,而已经出现在场景内的 TestWealthActor_BP (属于 Center 模组)通过框架的反射事件系统调用这 4 个对象各自的蓝图 Debug 方法。由此已经可以看出梁迪老师的框架功能是比较齐全的。
前面截取的图片里提到,按资源类型分类也有两种情况:
所以我们接下来准备写通过 UObject 和 UClass 这两种方式生成资源的逻辑。
下图截取自梁迪老师的 DataDriven 文档:
我们先从数据结构开始写起:
DDTypes.h
#pragma region Wealth // Object 资源结构体 USTRUCT() struct FObjectWealthEntry { GENERATED_BODY() public: // 资源名 UPROPERTY(EditAnywhere) FName WealthName; // 资源种类名 UPROPERTY(EditAnywhere) FName WealthKind; // 资源链接 UPROPERTY(EditAnywhere) FStringAssetReference WealthPath; // 加载出来的对象,如果有重复生成的情况就直接引用它,而不是生成多个 UPROPERTY() UObject* WealthObject; }; // UClass 类型枚举 UENUM() enum class EWealthType : uint8 { Object, Actor, Widget }; // Class 资源结构体 USTRUCT() struct FClassWealthEntry { GENERATED_BODY() public: // 资源类别 UPROPERTY(EditAnywhere) EWealthType WealthType; // 资源名 UPROPERTY(EditAnywhere) FName WealthName; // 资源种类名 UPROPERTY(EditAnywhere) FName WealthKind; // 资源链接 UPROPERTY(EditAnywhere) TSoftClassPtr<UObject> WealthPtr; // 加载出来的对象 UPROPERTY() UClass* WealthClass; }; // 纯获取链接结构体,不进行同异步加载 USTRUCT() struct FWealthURL { GENERATED_BODY() public: // 资源名 UPROPERTY(EditAnywhere) FName WealthName; // 资源种类名 UPROPERTY(EditAnywhere) FName WealthKind; // 资源链接 UPROPERTY(EditAnywhere) FStringAssetReference WealthPath; // 资源链接 UPROPERTY(EditAnywhere) TSoftClassPtr<UObject> WealthPtr; }; // 声明 3 个上面的结构体的数组 UCLASS() class DATADRIVEN_API UWealthData : public UDataAsset { GENERATED_BODY() public: // Object 资源链接集合 UPROPERTY(EditAnywhere) TArray<FObjectWealthEntry> ObjectWealthData; // Class 资源链接集合 UPROPERTY(EditAnywhere) TArray<FClassWealthEntry> ClassWealthData; // 资源链接集合 UPROPERTY(EditAnywhere) TArray<FWealthURL> WealthURL; }; #pragma endregion
剩下的逻辑留到后续课程。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。