赞
踩
由于梁迪老师是写 Unity 游戏出身的,所以即便 UE4 有自带的 TimeManager 这样的延时系统,老师还是重新写了一个符合 Unity 开发习惯的延时系统。
在 DDTypes 里定义延时任务结构体,以及它要用到的一个委托。
DDTypes.h
#pragma region Invoke DECLARE_DELEGATE(FDDInvokeEvent) struct DDInvokeTask { // 延迟执行的时间 float DelayTime; // 是否循环 bool IsRepeat; // 循环时间间隔 float RepeatTime; // 是否在循环阶段 bool IsRepeatState; // 计时器 float TimeCount; // 方法委托 FDDInvokeEvent InvokeEvent; // 构造函数 DDInvokeTask(float InDelayTime, bool InIsRepeat, float InRepeatTime) { DelayTime = InDelayTime; IsRepeat = InIsRepeat; RepeatTime = InRepeatTime; IsRepeatState = false; TimeCount = 0.f; } // 帧更新操作函数 bool UpdateOperate(float DeltaSeconds) { TimeCount += DeltaSeconds; // 如果不循环的,到时间了执行一次就停止;否则执行一次后开启循环状态 if (!IsRepeatState) { if (TimeCount >= DelayTime) { InvokeEvent.ExecuteIfBound(); TimeCount = 0.f; if (IsRepeat) IsRepeatState = true; else return true; } } else { if (TimeCount >= RepeatTime) { InvokeEvent.ExecuteIfBound(); TimeCount = 0.f; } } return false; } }; #pragma endregion
我们依旧将延时系统放在 DDMessage 这里。
协程系统和延时系统的 3 个方法的逻辑几乎都是一样的,所以可以依葫芦画瓢地将代码复制一份过来然后更改。
DDMessage.h
public:
// 开始一个延时方法,返回 true 说明成功;返回 false 说明已经存在同对象名同任务名的任务
bool StartInvoke(FName ObjectName, FName InvokeName, DDInvokeTask* InvokeTask);
// 停止一个延时
bool StopInvoke(FName ObjectName, FName InvokeName);
// 停止某对象下的所有延时方法
void StopAllInvoke(FName ObjectName);
protected:
// 延时序列,第一个 FName 是对象名,第二个 FName 是延时任务名
TMap<FName, TMap<FName, DDInvokeTask*>> InvokeStack;
DDMessage.cpp
void UDDMessage::MessageTick(float DeltaSeconds) { // 处理延时系统 CompleteTask.Empty(); // 跟协程系统共用这个名字数组,所以要先清空 for (TMap<FName, TMap<FName, DDInvokeTask*>>::TIterator It(InvokeStack); It; ++It) { TArray<FName> CompleteNode; // 保存完成的延时任务名字 for (TMap<FName, DDInvokeTask*>::TIterator Ih(It->Value); Ih; ++Ih) { if (Ih->Value->UpdateOperate(DeltaSeconds)) { delete Ih->Value; CompleteNode.Push(Ih->Key); } } for (int i = 0; i < CompleteNode.Num(); ++i) It->Value.Remove(CompleteNode[i]); if (It->Value.Num() == 0) CompleteTask.Push(It->Key); } for (int i = 0; i < CompleteTask.Num(); ++i) InvokeStack.Remove(CompleteTask[i]); } bool UDDMessage::StartInvoke(FName ObjectName, FName InvokeName, DDInvokeTask* InvokeTask) { if (!InvokeStack.Contains(ObjectName)) { TMap<FName, DDInvokeTask*> NewTaskStack; InvokeStack.Add(ObjectName, NewTaskStack); } if (!(InvokeStack.Find(ObjectName)->Contains(InvokeName))) { InvokeStack.Find(ObjectName)->Add(InvokeName, InvokeTask); return true; } delete InvokeTask; return false; } bool UDDMessage::StopInvoke(FName ObjectName, FName InvokeName) { if (InvokeStack.Contains(ObjectName) && InvokeStack.Find(ObjectName)->Find(InvokeName)) { DDInvokeTask* InvokeTask = *(InvokeStack.Find(ObjectName)->Find(InvokeName)); InvokeStack.Find(ObjectName)->Remove(InvokeName); if (InvokeStack.Find(ObjectName)->Num() == 0) InvokeStack.Remove(ObjectName); delete InvokeTask; return true; } return false; } void UDDMessage::StopAllInvoke(FName ObjectName) { if (InvokeStack.Contains(ObjectName)) { for (TMap<FName, DDInvokeTask*>::TIterator It(*InvokeStack.Find(ObjectName)); It; ++It) delete It->Value; InvokeStack.Remove(ObjectName); } }
调用路线依旧是 DDMessage – DDModule – DDOO – 对象,所以补充完整这条调用链。
DDModule.h
public:
// 开始一个延时方法,返回 true 说明成功;返回 false 说明已经存在同对象名同任务名的任务
bool StartInvoke(FName ObjectName, FName InvokeName, DDInvokeTask* InvokeTask);
// 停止一个延时
bool StopInvoke(FName ObjectName, FName InvokeName);
// 停止某对象下的所有延时方法
void StopAllInvoke(FName ObjectName);
DDModule.cpp
bool UDDModule::StartInvoke(FName ObjectName, FName InvokeName, DDInvokeTask* InvokeTask)
{
return Message->StartInvoke(ObjectName, InvokeName, InvokeTask);
}
bool UDDModule::StopInvoke(FName ObjectName, FName InvokeName)
{
return Message->StopInvoke(ObjectName, InvokeName);
}
void UDDModule::StopAllInvoke(FName ObjectName)
{
Message->StopAllInvoke(ObjectName);
}
在 DDOO 里,需要将延时运行和延时循环运行分为两个方法。其余方法则跟协程系统一样只传递调用即可。
DDOO.h
protected: // 延时运行 template<class UserClass> bool InvokeDelay(FName InvokeName, float DelayTime, UserClass* UserObj, typename FDDInvokeEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod); // 延时循环运行,与上面这个方法的区别就是多传了一个循环间隔时长的 float 变量 template<class UserClass> bool InvokeRepeat(FName InvokeName, float DelayTime, float RepeatTime, UserClass* UserObj, typename FDDInvokeEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod); // 关闭延时方法 bool StopInvoke(FName InvokeName); // 关闭对象下所有延时方法 void StopAllInvoke(); }; template<class UserClass> bool IDDOO::InvokeDelay(FName InvokeName, float DelayTime, UserClass* UserObj, typename FDDInvokeEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod) { DDInvokeTask* InvokeTask = new DDInvokeTask(DelayTime, false, 0.f); InvokeTask->InvokeEvent.BindUObject(UserObj, InMethod); // 绑定委托 return IModule->StartInvoke(GetObjectName(), InvokeName, InvokeTask); } template<class UserClass> bool IDDOO::InvokeRepeat(FName InvokeName, float DelayTime, float RepeatTime, UserClass* UserObj, typename FDDInvokeEvent::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod) { DDInvokeTask* InvokeTask = new DDInvokeTask(DelayTime, true, RepeatTime); InvokeTask->InvokeEvent.BindUObject(UserObj, InMethod); return IModule->StartInvoke(GetObjectName(), InvokeName, InvokeTask); }
DDOO.cpp
bool IDDOO::StopInvoke(FName InvokeName)
{
return IModule->StopInvoke(GetObjectName(), InvokeName);
}
void IDDOO::StopAllInvoke()
{
IModule->StopAllInvoke(GetObjectName());
}
最后在 CoroActor.cpp 里调用循环延时方法进行测试。
CoroActor.cpp
void ACoroActor::DDEnable()
{
// 测试完后记得注释掉
InvokeRepeat("EchoInfo", 3.f, 2.f, this, &ACoroActor::EchoCoroInfo);
// TempStartCoroutine(CoroTestTwo());
//DDH::Debug() << "StartCoroutine --> " << StartCoroutine("CoroFunc", CoroFunc()) << DDH::Endl();
}
编译后运行,3 秒输出第一句,随后每 2 秒输出一次。
之前写的协程系统还有一点 Bug 需要解决,我们先复现一下问题:
CoroActor.h
protected:
DDCoroTask* CoroFixed(); // 使用协程的方法
void StopCoro(); // 负责调用停止协程的方法
CoroActor.cpp
void ACoroActor::DDEnable() { // 本节课结束后记得注释掉 StartCoroutine("CoroFixed", CoroFixed()); } DDCoroTask* ACoroActor::CoroFixed() { DDCORO_PARAM(ACoroActor); #include DDCORO_BEGIN() #include DDYIELD_READY() DDYIELD_RETURN_SECOND(5.f); // 挂起 5 秒 DDH::Debug() << "StopCoro" << DDH::Endl(); D->StopCoro(); // 在第一次挂起时停止协程 #include DDYIELD_READY() DDYIELD_RETURN_SECOND(3.f); // 挂起 3 秒 DDH::Debug() << "StopCoroComplete" << DDH::Endl(); #include DDCORO_END() } void ACoroActor::StopCoro() { StopCoroutine("CoroFixed"); }
编译后运行,项目应该在输出那一瞬间就崩溃了。崩溃的原因是:DDMessage.cpp 的逻辑里,Work()
方法调用了停止协程方法 StopCoroutine()
,将协程任务移出了容器;但是后续调用 IsFinish()
判断时依旧会访问这个协程任务原来在容器里的位置,导致访问到错误地址。
所以我们在协程任务结构体里面再添加一个 bool 值,保存协程任务实例是否被删除。这样在 StopCoroutine()
里就不再去进行 “将协程任务移除出容器” 的操作,而是直接更改这个 bool 值;实际的移除操作由 Tick()
全权负责。
DDTypes.h
struct DDCoroTask
{
// 是否销毁(老师拼写错了)
bool IsDestroy;
DDCoroTask(int32 CoroCount)
{
IsDestroy = false;
}
}
DDMessage.cpp
void UDDMessage::MessageTick(float DeltaSeconds) { // ... 省略 if (Ih->Value->IsFinish() || Ih->Value->IsDestroy) { // 添加多一个判断 delete Ih->Value; CompleteNode.Push(Ih->Key); } // ... 省略 } bool UDDMessage::StopCoroutine(FName ObjectName, FName CoroName) { if (CoroStack.Contains(ObjectName) && CoroStack.Find(ObjectName)->Find(CoroName)) { // 修改如下 (*(CoroStack.Find(ObjectName)->Find(CoroName)))->IsDestroy = true; return true; } return false; } void UDDMessage::StopAllCoroutine(FName ObjectName) { if (CoroStack.Contains(ObjectName)) { for (TMap<FName, DDCoroTask*>::TIterator It(*CoroStack.Find(ObjectName)); It; ++It) // 修改如下 It->Value->IsDestroy = true; } }
编译后运行,打印一条 “StopCoro” 语句后就不会打印下一条,游戏也没有崩溃,说明修改成功了。
依葫芦画瓢地改一下延时系统,因为延时系统是基本照搬协程系统的。
DDTypes.h
struct DDInvokeTask
{
// 是否销毁
bool IsDestroy;
DDInvokeTask(float InDelayTime, bool InIsRepeat, float InRepeatTime)
{
IsDestroy = false;
}
}
DDMessage.cpp
void UDDMessage::MessageTick(float DeltaSeconds) { // ... 省略 if (Ih->Value->UpdateOperate(DeltaSeconds) || Ih->Value->IsDestroy) { // 添加多一个判断 delete Ih->Value; CompleteNode.Push(Ih->Key); } // ... 省略 } bool UDDMessage::StopInvoke(FName ObjectName, FName InvokeName) { if (InvokeStack.Contains(ObjectName) && InvokeStack.Find(ObjectName)->Find(InvokeName)) { (*(InvokeStack.Find(ObjectName)->Find(InvokeName)))->IsDestroy = true; return true; } return false; } void UDDMessage::StopAllInvoke(FName ObjectName) { if (InvokeStack.Contains(ObjectName)) { for (TMap<FName, DDInvokeTask*>::TIterator It(*InvokeStack.Find(ObjectName)); It; ++It) It->Value->IsDestroy = true; } }
下面这段文字截取自梁迪老师准备的 DataDriven 文档:
UE4的按键绑定需要调用 ACharactor
下的 SetupPlayerInputComponent()
,或者 APlayerController
下的 SetupInputComponent()
进行按键事件的绑定,如果要实现 “按下 Esc 键弹出菜单” 的功能,就需要获取 UI 对象的指针与添加头文件来进行绑定,这样的话耦合程度较高。因此 DataDriven 框架提供一套自己的按键绑定系统,可以在任何对象下进行按键事件的绑定,并且提供多按键事件绑定功能。
按键绑定系统的功能包括:绑定 Axis 按键、触摸按键、单个按键和多个按键(同时按下)。
来到 DDMessage,它首先要获得玩家控制器,通过 UDDCommon 就可以获取。这样就可以通过玩家控制器访问绑定按钮的函数。
四个模板方法对应上面列出的 4 种绑定按键的类型。
DDMessage.h
#include "GameFramework/PlayerController.h" // 引入头文件 #include "DDMessage.generated.h" UCLASS() class DATADRIVEN_API UDDMessage : public UObject, public IDDMM { GENERATED_BODY() public: // 绑定 Axis 按键事件 template<class UserClass> FInputAxisBinding& BindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName); // 绑定触摸事件 template<class UserClass> FInputTouchBinding& BindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent); // 绑定 Action 按键事件 template<class UserClass> FInputActionBinding& BindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent); // 绑定单个按键事件 template<class UserClass> FInputKeyBinding& BindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent); protected: // PlayerController 指针 APlayerController* PlayerController; }; template<class UserClass> FInputAxisBinding& UDDMessage::BindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName) { return PlayerController->InputComponent->BindAxis(AxisName, UserObj, InMethod); } template<class UserClass> FInputTouchBinding& UDDMessage::BindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent) { return PlayerController->InputComponent->BindTouch(KeyEvent, UserObj, InMethod); } template<class UserClass> FInputActionBinding& UDDMessage::BindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent) { return PlayerController->InputComponent->BindAction(ActionName, KeyEvent, UserObj, InMethod); } template<class UserClass> FInputKeyBinding& UDDMessage::BindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent) { return PlayerController->InputComponent->BindKey(Key, KeyEvent, UserObj, InMethod); }
DDMessage.cpp
void UDDMessage::MessageBeginPlay()
{
// 从 UDDCommon 获取 Controller
PlayerController = UDDCommon::Get()->GetController();
}
依旧是建立 DDMessage – DDModule – DDOO – 对象 的调用链。
DDModule.h
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class DATADRIVEN_API UDDModule : public USceneComponent { GENERATED_BODY() public: // 绑定 Axis 按键事件 template<class UserClass> FInputAxisBinding& BindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName); // 绑定触摸事件 template<class UserClass> FInputTouchBinding& BindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent); // 绑定 Action 按键事件 template<class UserClass> FInputActionBinding& BindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent); // 绑定单个按键事件 template<class UserClass> FInputKeyBinding& BindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent); }; template<class UserClass> FInputAxisBinding& UDDModule::BindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName) { return Message->BindAxis(UserObj, InMethod, AxisName); } template<class UserClass> FInputTouchBinding& UDDModule::BindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent) { return Message->BindTouch(UserObj, InMethod, KeyEvent); } template<class UserClass> FInputActionBinding& UDDModule::BindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent) { return Message->BindAction(UserObj, InMethod, ActionName, KeyEvent); } template<class UserClass> FInputKeyBinding& UDDModule::BindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent) { return Message->BindInput(UserObj, InMethod, Key, KeyEvent); }
DDOO.h
class DATADRIVEN_API IDDOO { GENERATED_BODY() protected: // 绑定 Axis 按键事件 template<class UserClass> FInputAxisBinding& BindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName); // 绑定触摸事件 template<class UserClass> FInputTouchBinding& BindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent); // 绑定 Action 按键事件 template<class UserClass> FInputActionBinding& BindAction(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName ActionName, const EInputEvent KeyEvent); // 绑定单个按键事件 template<class UserClass> FInputKeyBinding& BindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent); }; template<class UserClass> FInputAxisBinding& IDDOO::BindAxis(UserClass* UserObj, typename FInputAxisHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FName AxisName) { return IModule->BindAxis(UserObj, InMethod, AxisName); } template<class UserClass> FInputTouchBinding& IDDOO::BindTouch(UserClass* UserObj, typename FInputTouchHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const EInputEvent KeyEvent) { return IModule->BindTouch(UserObj, InMethod, KeyEvent); } template<class UserClass> FInputActionBinding& IDDOO::BindAction(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::BindInput(UserClass* UserObj, typename FInputActionHandlerSignature::TUObjectMethodDelegate<UserClass>::FMethodPtr InMethod, const FKey Key, const EInputEvent KeyEvent) { return IModule->BindInput(UserObj, InMethod, Key, KeyEvent); }
最后来简单测试一下绑定单个按键事件的方法。
CoroActor.h
protected:
void BKeyEvent();
CoroActor.cpp
void ACoroActor::DDEnable()
{
BindInput(this, &ACoroActor::BKeyEvent, EKeys::B, IE_Pressed);
}
void ACoroActor::BKeyEvent()
{
DDH::Debug() << "BKeyEvent" << DDH::Endl();
}
来到项目的 .Build.cs 文件,需要添加对 Slate 的依赖。
RaceCarFrame.Build.cs
// 需要添加对 Slate 的依赖,否则会报错
PrivateDependencyModuleNames.AddRange(new string[] {
"Slate",
"SlateCore",
});
PublicDefinitions.Add("HMD_MODULE_INCLUDED=1");
编译后运行,此时按一次 B 键可以让左上角输出一次 “BKeyEvent”。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。