文章目录
UE C++进阶 | 异步
潜伏事件
- 在UE的蓝图中存在一种可以实现异步执行的节点,
- 想要去实现我们自己的异步节点,首先我们需要了解
FPendingLatentAction
。
Unreal - FPendingLatentAction
- 中文翻译来说应该是待定的潜伏动作。
- 我们先来看看它的源码。
- 存在几个虚函数,看起来
UpdateOperation
是关键。 Response.DoneIf(true);
是该动作标志着工作做完了调用的,相当与需要 destory了。
class ENGINE_API FPendingLatentAction
{
public:
// 构造
FPendingLatentAction()
{
}
// 析构
virtual ~FPendingLatentAction()
{
}
// 动作完成后传入true,然后就会被销毁。
virtual void UpdateOperation(FLatentResponse& Response)
{
Response.DoneIf(true);
}
/* 让潜伏的动作知道,产生它的对象已经被垃圾回收了。
* 并且该动作将被销毁(不再有UpdateOperation的调用,并且CallbackTarget已经是NULL)
* 只有当对象在动作完成前离开时才会被调用。
*/
virtual void NotifyObjectDestroyed() {}
virtual void NotifyActionAborted() {}
#if WITH_EDITOR
// 返回对潜伏操作当前状态的可读描述。
virtual FString GetDescription() const;
#endif
};
Unreal - Delay节点
- 例如我们常用的Delay节点,当输入执行节点被触发后,输出执行节点直到对应时间耗尽后才会触发。
- 让我们先分析一下源码。
FDelayAction
- 继承自
FPendingLatentAction
。 - 声明有几个变量,其中有几个参数是为LatentInfo服务的。
- 重写了
UpdateOperation
,这个函数因为相当于Tick
函数,所以可以写一些计时的业务逻辑。 - 重写了
GetDescription
,为了使得在蓝图调试过程中打印在节点上。
class FDelayAction : public FPendingLatentAction
{
public:
float TimeRemaining; // 业务变量
FName ExecutionFunction; // LatentInfo服务
int32 OutputLink; // LatentInfo服务
FWeakObjectPtr CallbackTarget; // LatentInfo服务
// 有参构造
FDelayAction(float Duration, const FLatentActionInfo& LatentInfo)
: TimeRemaining(Duration)
, ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
{
}
// 业务逻辑
virtual void UpdateOperation(FLatentResponse& Response) override
{
// 定时器
TimeRemaining -= Response.ElapsedTime();
// 完成与触发,如果想要深入理解可以去看FLatentResponse,这个函数相当于DoneIf + TriggerLink。
Response.FinishAndTriggerIf(TimeRemaining <= 0.0f, ExecutionFunction, OutputLink, CallbackTarget);
}
#if WITH_EDITOR
// 蓝图调试过程中打印
virtual FString GetDescription() const override
{
static const FNumberFormattingOptions DelayTimeFormatOptions = FNumberFormattingOptions()
.SetMinimumFractionalDigits(3)
.SetMaximumFractionalDigits(3);
return FText::Format(NSLOCTEXT("DelayAction", "DelayActionTimeFmt", "Delay ({0} seconds left)"), FText::AsNumber(TimeRemaining, &DelayTimeFormatOptions)).ToString();
}
#endif
};
函数声明:
UFUNCTION(BlueprintCallable, Category="Utilities|FlowControl", meta=(Latent, WorldContext="WorldContextObject", LatentInfo="LatentInfo", Duration="0.2", Keywords="sleep"))
static void Delay(const UObject* WorldContextObject, float Duration, struct FLatentActionInfo LatentInfo );
- 头文件基本信息:
- 基类 -
UBlueprintFunctionLibrary
- ↳
UKismetSystemLibrary
-\\UE_5.1\Engine\Source\Runtime\Engine\Classes\Kismet\KismetSystemLibrary.h
- ↳ 类静态函数
Delay
。
- ↳ 类静态函数
- ↳
- 基类 -
- 用法:执行一个有延迟的潜伏动作(以秒为单位指定),在其倒计时时再次调用将被忽略。
- 宏标记解析:
- BlueprintCallable: 该函数可以在蓝图或关卡蓝图图表中执行。
- Category: 指定函数在编辑器中的显示分类层级。
- meta
- Lantent: 表示潜在的行动。
- WorldContext = “WorldContextObject”: 用来指示哪个参数确定操作正在发生的世界。
- LatentInfo = “LatentInfo”: 指示哪个参数是LatentInfo参数。
- Duration = “0.2”: 默认值。
- Keywords = “sleep”: 指定搜索此函数时可以使用的一组关键字。
- 参数:
- WorldContextObject - 世界上下文,已经由meta绑定。
- Duration - 延迟的长度(以秒为单位)
- LatentInfo - 潜在的动作信息,已经由meta绑定。
函数实现:
void UKismetSystemLibrary::Delay(const UObject* WorldContextObject, float Duration, FLatentActionInfo LatentInfo )
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
// 去获得潜伏动作管理器
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
// 从潜伏动作管理器查找该节点位置是否已经存在此类动作位置
if (LatentActionManager.FindExistingAction<FDelayAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == NULL)
{
// 找不到就加入新的潜伏动作
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FDelayAction(Duration, LatentInfo));
}
}
}
案例 - SustainedAxisTriggerAction (持续轴映射触发动作)
- 到这我们大致了解了需要了解虚幻的潜伏动作,开始写我们自己的异步节点。
- 这是一个我自己工作中实践出来的功能。
- 它可以接受手柄的Axis值然后进行连续触发动作。
- 具体:在接收手柄输入后,进行第一次触发,如果持续按住的状态下,在一定时间过后开始进行连续触发。
- 源码展示
// Copyright(c) 2023 TangYijun All rights reserved.
#pragma once
#include "LatentActions.h"
#include "Misc/EnumRange.h"
// 轴输入的几个阶段
UENUM(BlueprintType)
enum class ESustainedAxisState : uint8
{
EPress,
ELowPulseInterval,
ELowPulseTrigger,
EHighPulseInterval,
ELowToHighTransition,
EHighPulseTrigger,
ECount
};
// 轴映射的正反
UENUM(BlueprintType)
enum class ESustainedAxisExec : uint8
{
EPositive,
ENegative
};
// 创建枚举的计数
ENUM_RANGE_BY_COUNT(ESustainedAxisState, ESustainedAxisState::ECount);
// 基类
class FSustainedAxisTriggerActionBase : public FPendingLatentAction
{
public:
float Axis; // 轴输入
float AxisThreshold; // 创建枚举的计数
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
ESustainedAxisExec& ExecRef; // 节点的输出执行线
protected:
TBitArray<> AxisStateBits; // 轴输入的激活状态集
TMap<ESustainedAxisState, float> CountMap; // 计时器
bool bIsPress; // 是否摁下
public:
//构造
FSustainedAxisTriggerActionBase(float Axis, float AxisThreshold, const FLatentActionInfo& LatentInfo, ESustainedAxisExec& AxisExec)
: Axis(Axis)
, AxisThreshold(AxisThreshold)
, ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
, ExecRef(AxisExec)
{
// 初始化状态集
AxisStateBits = TBitArray<>(false, uint8(ESustainedAxisState::ECount));
}
// 业务更新
virtual void UpdateOperation(FLatentResponse& Response) override
{
// 判断轴输入
bIsPress = Axis > AxisThreshold || Axis < -AxisThreshold;
UpdateDoneIf(Response); // 更新执行线走向
UpdateTimer(Response); // 更新计时器
}
protected:
FORCEINLINE bool IsTimeOver(const ESustainedAxisState& State)
{
return CountMap[State] <= 0.f && AxisStateBits[uint8(State)];
}
FORCEINLINE void EnterState(const ESustainedAxisState& State)
{
AxisStateBits[uint8(State)] = true;
}
FORCEINLINE void ExitState(const ESustainedAxisState& State)
{
AxisStateBits[uint8(State)] = false;
}
FORCEINLINE void UpdateTimer(FLatentResponse& Response)
{
for (ESustainedAxisState AxisState : TEnumRange<ESustainedAxisState>())
{
if (CountMap.Contains(AxisState))
{
if (AxisStateBits[uint8(AxisState)])
{
CountMap[AxisState] -= Response.ElapsedTime();
}
}
}
}
FORCEINLINE void UpdateDoneIf(FLatentResponse& Response)
{
if (!bIsPress)
{
Response.DoneIf(!bIsPress);
}
else
{
if (Axis > AxisThreshold)
{
ExecRef = ESustainedAxisExec::EPositive;
}
else if (Axis < -AxisThreshold)
{
ExecRef = ESustainedAxisExec::ENegative;
}
}
}
};
// 一阶段持续轴触发动作,就是按下后一定时间开始保持持续触发状态。
class FOneStepSustainedAxisTriggerAction : public FSustainedAxisTriggerActionBase
{
public:
float PulseTriggerTime; // 脉冲触发时间 - 触发频率。
float PulseIntervalTime;// 脉冲间隔时间 - 从触发第一次到开始持续触发状态的间隔时间。
FOneStepSustainedAxisTriggerAction(float Axis, float AxisThreshold, float PulseTriggerTime, float PulseIntervalTime,
const FLatentActionInfo& LatentInfo, ESustainedAxisExec& AxisExec)
: FSustainedAxisTriggerActionBase(Axis, AxisThreshold, LatentInfo, AxisExec)
, PulseTriggerTime(PulseTriggerTime)
, PulseIntervalTime(PulseIntervalTime)
{
// 增加这两种状态的定时器
CountMap.Emplace(ESustainedAxisState::ELowPulseInterval, PulseIntervalTime);
CountMap.Emplace(ESustainedAxisState::ELowPulseTrigger, PulseTriggerTime);
}
virtual void UpdateOperation(FLatentResponse& Response) override
{
FSustainedAxisTriggerActionBase::UpdateOperation(Response);
// 简易状态机
if (bIsPress && !AxisStateBits[uint8(ESustainedAxisState::EPress)])
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
EnterState(ESustainedAxisState::EPress);
EnterState(ESustainedAxisState::ELowPulseInterval);
}
if (IsTimeOver(ESustainedAxisState::ELowPulseInterval))
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
ExitState(ESustainedAxisState::ELowPulseInterval);
EnterState(ESustainedAxisState::ELowPulseTrigger);
}
if (IsTimeOver(ESustainedAxisState::ELowPulseTrigger))
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
CountMap[ESustainedAxisState::ELowPulseTrigger] = PulseTriggerTime;
}
}
};
class FTwoStepSustainedAxisTriggerAction : public FSustainedAxisTriggerActionBase
{
public:
float LowPulseTriggerTime; // 低频脉冲触发时间 - 阶段一触发频率。
float LowPulseIntervalTime; // 低频脉冲间隔时间 - 从触发第一次到开始阶段一的间隔时间。
float HighPulseTriggerTime; // 高频脉冲触发时间 - 阶段二触发频率。
float HighPulseIntervalTime;// 高频脉冲间隔时间 - 从阶段一到阶段二的间隔时间。
FTwoStepSustainedAxisTriggerAction(float Axis, float AxisThreshold, float LowPulseTriggerTime,
float LowPulseIntervalTime, float HighPulseTriggerTime, float HighPulseIntervalTime,
const FLatentActionInfo& LatentInfo, ESustainedAxisExec& AxisExec)
: FSustainedAxisTriggerActionBase(Axis, AxisThreshold, LatentInfo, AxisExec)
, LowPulseTriggerTime(LowPulseTriggerTime)
, LowPulseIntervalTime(LowPulseIntervalTime)
, HighPulseTriggerTime(HighPulseTriggerTime)
, HighPulseIntervalTime(HighPulseIntervalTime)
{
// 增加这四种状态的定时器
CountMap.Emplace(ESustainedAxisState::ELowPulseInterval, LowPulseIntervalTime);
CountMap.Emplace(ESustainedAxisState::ELowPulseTrigger, LowPulseTriggerTime);
CountMap.Emplace(ESustainedAxisState::EHighPulseInterval, HighPulseIntervalTime);
CountMap.Emplace(ESustainedAxisState::EHighPulseTrigger, HighPulseTriggerTime);
}
virtual void UpdateOperation(FLatentResponse& Response) override
{
FSustainedAxisTriggerActionBase::UpdateOperation(Response);
// 简易状态机
if (bIsPress && !AxisStateBits[uint8(ESustainedAxisState::EPress)])
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
EnterState(ESustainedAxisState::EPress);
EnterState(ESustainedAxisState::ELowPulseInterval);
}
if (IsTimeOver(ESustainedAxisState::ELowPulseInterval))
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
ExitState(ESustainedAxisState::ELowPulseInterval);
EnterState(ESustainedAxisState::ELowPulseTrigger);
EnterState(ESustainedAxisState::EHighPulseInterval);
}
if (IsTimeOver(ESustainedAxisState::ELowPulseTrigger))
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
CountMap[ESustainedAxisState::ELowPulseTrigger] = LowPulseTriggerTime;
if (AxisStateBits[uint8(ESustainedAxisState::ELowToHighTransition)])
{
EnterState(ESustainedAxisState::EHighPulseTrigger);
ExitState(ESustainedAxisState::ELowPulseTrigger);
}
}
if (IsTimeOver(ESustainedAxisState::EHighPulseInterval))
{
if (CountMap[ESustainedAxisState::ELowPulseTrigger] <= 0)
{
EnterState(ESustainedAxisState::EHighPulseTrigger);
}
else
{
EnterState(ESustainedAxisState::ELowToHighTransition);
}
ExitState(ESustainedAxisState::EHighPulseInterval);
}
if (IsTimeOver(ESustainedAxisState::EHighPulseTrigger))
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
CountMap[ESustainedAxisState::EHighPulseTrigger] = HighPulseTriggerTime;
}
}
};
- 源码展示 (LatentFunctionLibrary.h)
// Copyright(c) 2023 TangYijun All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "PendingLatentAction/SustainedAxisTriggerAction.h"
#include "LatentFunctionLibrary.generated.h"
UCLASS()
class HECATONCHEIRES_API ULatentFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "UI|Trigger", meta = (ExpandEnumAsExecs = "AxisExec", Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo"))
static void OneStepSustainedAxisTrigger(const UObject* WorldContextObject, ESustainedAxisExec& AxisExec, float Axis, float AxisThreshold, float PulseTriggerTime,
float PulseIntervalTime, FLatentActionInfo LatentInfo);
UFUNCTION(BlueprintCallable, Category = "UI|Trigger", meta = (ExpandEnumAsExecs = "AxisExec", Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo"))
static void TwoStepSustainedAxisTrigger(const UObject* WorldContextObject, ESustainedAxisExec& AxisExec, float Axis, float AxisThreshold, float LowPulseTriggerTime,
float LowPulseIntervalTime, float HighPulseTriggerTime, float HighPulseIntervalTime, FLatentActionInfo LatentInfo);
};
- 源码展示 (LatentFunctionLibrary.cpp)
// Copyright(c) 2023 TangYijun All rights reserved.
#include "FunctionLibrary/LatentFunctionLibrary.h"
#include "Engine/LatentActionManager.h"
void ULatentFunctionLibrary::OneStepSustainedAxisTrigger(const UObject* WorldContextObject, ESustainedAxisExec& AxisExec, float Axis, float AxisThreshold,
float PulseTriggerTime, float PulseIntervalTime, FLatentActionInfo LatentInfo)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
FOneStepSustainedAxisTriggerAction* Action = LatentActionManager.FindExistingAction<FOneStepSustainedAxisTriggerAction>(LatentInfo.CallbackTarget, LatentInfo.UUID);
if (!Action)
{
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FOneStepSustainedAxisTriggerAction(Axis,
AxisThreshold, PulseTriggerTime, PulseIntervalTime, LatentInfo, AxisExec));
}
else
{
Action->Axis = Axis;
}
}
}
void ULatentFunctionLibrary::TwoStepSustainedAxisTrigger(const UObject* WorldContextObject, ESustainedAxisExec& AxisExec, float Axis, float AxisThreshold,
float LowPulseTriggerTime, float LowPulseIntervalTime, float HighPulseTriggerTime, float HighPulseIntervalTime, FLatentActionInfo LatentInfo)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
FTwoStepSustainedAxisTriggerAction* Action = LatentActionManager.FindExistingAction<FTwoStepSustainedAxisTriggerAction>(LatentInfo.CallbackTarget, LatentInfo.UUID);
if (!Action)
{
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FTwoStepSustainedAxisTriggerAction(Axis,
AxisThreshold, LowPulseTriggerTime, LowPulseIntervalTime, HighPulseTriggerTime, HighPulseIntervalTime, LatentInfo, AxisExec));
}
else
{
Action->Axis = Axis;
}
}
}
定时器
FTimerManager
绑定定时器
- BindLambda.
FTimerHandle TimerHandle;
const auto Exec = []()
{
// Implement...
};
World->GetTimerManager().SetTimer(TimerHandle, Exec, 1.0f, false);
- BindFunction.
FTimerHandle TimerHandle;
World->GetTimerManager().SetTimer(TimeHandler, this, &ThisClass::TimerFunction, 1.0f, false);
清除定时器
GetWorld()->GetTimerManager().ClearTimer(TimeHandler);
定时器的模式
延迟模式
- 延迟2秒后执行一次 Exec()。
World->GetTimerManager().SetTimer(TimerHandle, Exec, 2.0f, false);
循环模式
- 自设置时起就执行一次 Exec(),并开始每隔1秒就执行一次 Exec(),永不停歇,直到定时器被清除。
World->GetTimerManager().SetTimer(TimerHandle, Exec, 2.0f, true, 0.0f);
延迟循环模式
- 延迟2秒后第一次执行 Exec(),之后每隔1秒就执行一次 Exec(),永不停歇,直到定时器被清除。
World->GetTimerManager().SetTimer(TimerHandle, Exec, 1.0f, true, 2.0f);
异步蓝图节点
Unreal - UBlueprintAsyncActionBase
(TODO)