UE C++进阶 | 异步

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节点,当输入执行节点被触发后,输出执行节点直到对应时间耗尽后才会触发。
    Delay节点
  • 让我们先分析一下源码。

FDelayAction

  • 继承自FPendingLatentAction
  • 声明有几个变量,其中有几个参数是为LatentInfo服务的。
  • 重写了UpdateOperation,这个函数因为相当于 Tick函数,所以可以写一些计时的业务逻辑。
  • 重写了GetDescription,为了使得在蓝图调试过程中打印在节点上。
    Delay节点的打印
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)

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在 Unreal Engine 5 中,你可以使用 C++ 来实现异步线程执行。下面是一个简单的示例代码: ```cpp #include "Async/Async.h" // 定义一个异步任务 class MyAsyncTask : public FNonAbandonableTask { public: // 构造函数传入需要执行的参数 MyAsyncTask(int32 InParam) : Param(InParam) {} // 在异步线程中执行的任务 void DoWork() { // 执行一些耗时操作,比如计算或者加载资源等等 // ... // 任务执行结束后可以将结果回调到主线程 FGraphEventRef GameThreadTask = FFunctionGraphTask::CreateAndDispatchWhenReady([&]() { // 在主线程中执行回调操作,可以更新UI或者其他逻辑 // ... }, TStatId(), nullptr, ENamedThreads::GameThread); } // 返回任务名称 static const TCHAR* Name() { return TEXT("MyAsyncTask"); } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(MyAsyncTask, STATGROUP_ThreadPoolAsyncTasks); } private: int32 Param; }; // 启动异步任务 void StartAsyncTask(int32 Param) { // 创建异步任务并提交到线程池中执行 MyAsyncTask* AsyncTask = new MyAsyncTask(Param); AsyncTask->StartBackgroundTask(); } ``` 在上面的示例代码中,我们定义了一个名为 `MyAsyncTask` 的异步任务,它继承自 `FNonAbandonableTask`,并实现了 `DoWork` 方法来执行异步线程中的任务。在 `DoWork` 方法中,你可以执行一些耗时操作,并在任务结束后通过 `FFunctionGraphTask` 将结果回调到主线程进行处理。 要启动异步任务,你可以调用 `StartAsyncTask` 函数,并传入需要执行的参数。该函数会创建一个 `MyAsyncTask` 实例,并提交到线程池中执行。 请注意,异步任务的执行是在一个单独的线程中进行的,因此你需要确保在任务中不会访问或修改与主线程共享的对象或数据,以避免潜在的竞态条件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Flame老唐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值