在UE4的蓝图中存在一种可以实现异步执行的节点,例如非常实用Delay节点,当输入执行节点被触发后,输出执行节点直到对应时间耗尽后才会触发。
这种节点在UE4中被称为LatentAction(潜在事件。。也许是这么翻译的,下文中暂且简称为Latent节点),它从Kismet时期演化而来。有些Latent节点可能会含有多个执行输出,例如用于创建会话的Create Session节点。它们的形式和Macro很相似,但实际上存在着很大的不同,最简单的区分方式是该类节点的右上角有一个时钟形状的小图标。
笔者对这类节点十分的好奇,但是网上的相关的资料较少,特别是中文资料,所以笔者觉得还是存在着一定的价值来向大家简要介绍一下此类节点的创建。读者对其的理解也较为粗浅,所以只从最简单的创建与多节点输入两个方面来向大家介绍。
例子节点的设计
为了方便大家的理解,笔者设计了一个非常简单的节点,它模仿的了Delay节点,不同之处在与它在时间经过一半时输出执行一次,在时间结束时输出执行一次,总共输出执行两次。
接下来开始实际操作,首先创建一个蓝图方法库LatentFunctionLibrary,将Latent节点以静态方法的形式放在方法库里。下面贴上完整的代码。
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Kismet/KismetSystemLibrary.h"
#include "LatentFunctionLibrary.generated.h"
/**
*
*/
UENUM(BlueprintType) //用于实现多节点输出
enum class DELAY_EXEC : uint8
{
HalfExec,
CompleteExec
};
UCLASS()
class LATENTACTION_API ULatentFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "exec", Latent, LatentInfo = "LatentInfo", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"))
static void TwiceDelay(UObject* WorldContextObject, DELAY_EXEC& exec, struct FLatentActionInfo LatentInfo, float Duration);
};
#include "LatentFunctionLibrary.h"
#include "Engine/LatentActionManager.h"
#include "Engine/Engine.h"
#include "LatentActions.h"
class FTwiceDelayAction : public FPendingLatentAction
{
public:
float TotalTime;
float TimeRemaining;
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
DELAY_EXEC& execRef;
bool bHalfTriggered = false;
FTwiceDelayAction(float Duration, const FLatentActionInfo& LatentInfo, DELAY_EXEC& exec)
:TotalTime(Duration)
, TimeRemaining(Duration)
, ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
, execRef(exec)
{
}
virtual void UpdateOperation(FLatentResponse& Response) override //每一帧都执行
{
TimeRemaining -= Response.ElapsedTime(); //官方的Delay也用了这种计算时间的方法,简单粗暴
if (TimeRemaining < TotalTime / 2.0f && !bHalfTriggered)
{
execRef = DELAY_EXEC::HalfExec;
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget); //回调
bHalfTriggered = true;
}
else if (TimeRemaining < 0.0f)
{
execRef = DELAY_EXEC::CompleteExec;
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget); //回调
Response.DoneIf(TimeRemaining < 0.0f); //终止Latent
}
}
};
void ULatentFunctionLibrary::TwiceDelay(UObject* WorldContextObject, DELAY_EXEC& exec, FLatentActionInfo LatentInfo, float Duration)
{
if (UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); //获取LatentActionManager
if (LatentActionManager.FindExistingAction<FTwiceDelayAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == NULL)
{
//在LatentActionManager中加入事件
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FTwiceDelayAction(Duration, LatentInfo, exec));
}
}
}
在声明时,UFUNCTION宏非常重要,它决定了函数是否能成为LatentAction,接下来分条详解。
(1)BlueprintCallable 很常见,用于被蓝图调用
(2)ExpandEnumAsExecs = "exec" 将枚举成员转换为可执行节点输出
(3)Latent, LatentInfo = "LatentInfo" 变身Latent的关键
(4)HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject" 隐藏参数
Latent, LatentInfo = "LatentInfo" 这一条是关键,Latent表明该函数为LatentAction,LatentInfo = "LatentInfo"对应函数参数FLatentActionInfo LatentInfo,二者名称必须相同。
在定义函数时,首先要获取世界的LatentActionManager,在LatentActionManager中加入自己的LatentAction。其他的工作都交给回调来完成。但是要怎么样设定Latent节点的执行逻辑呢,这里需要一个FPendingLatentAction类的派生类对象来完成。
复写FPendingLatentAction类中的UpdateOperation虚函数,该函数每帧执行一次,通过该函数的不断循环检查判断条件,若条件满足则回调使Latent节点触发输出。回调通过FLatentResponse的成员函数实现。
执行结果
说在最后
多执行节点输出的设计不是必须的,像官方的Delay节点就只有一个输出。在这种情况下不需要多余的exec参数,使用默认参数直接回调即可,而Latent节点的默认输出名会变为Completed。读者朋友们可以尝试各种不同的组合,官方Delay可参考UE_4.18\Engine\Source\Runtime\Engine\Classes\Kismet\KismetSystemLibrary.h