我们先看下Delay的使用和实现:
注意Delay和Window编程中的Sleep(5000)操作完全不同,Sleep是挂起当前的线程,把执行权交给其他线程,大约5秒后再唤醒这个线程。
Delay并没有挂起本线程,而是立即返回,5秒后再来执行Completed Pin后的节点。
Delay的内部逻辑很简单,向LatentActionManager实例中Add了一个FDelayAction的实例。我们看下FDelayAction的实现:
// FDelayAction
// A simple delay action; counts down and triggers it's output link when the time remaining falls to zero
class FDelayAction : public FPendingLatentAction
{
public:
float TimeRemaining;
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
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();
Response.FinishAndTriggerIf(TimeRemaining <= 0.0f, ExecutionFunction, OutputLink, CallbackTarget);
}
#if WITH_EDITOR
// Returns a human readable description of the latent operation's current state
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
};
构建FDelayAction传入的参数中,TimeRemaining就是5.0f,ExecutionFunction和OutputLink是Completed Pin后面节点的相关信息。CallbackTarget是指这个Delay操作是在哪个UObject里面的,因为我们是在关卡蓝图里调用的,所以这里的CallbackTarget是ALevelScriptActor实例。FDelayAction有个UpdateOperation操作,我们可以猜测到是游戏线程每帧都会更新的代码,通过每次减去DeltaTime并且判断时间TimeRemaining是否已经小于0,如果是则执行后面的节点。
UpdateOperation是如何调用的呢?通过调试我们可以知道是FLatentActionManager::ProcessLatentActions进行调用的,我们全局搜索发现调用ProcessLatentActions的有下面几个:
D:\Software\UE4\Engine\UE_4.19\Engine\Source\Runtime\Engine\Private\Components\ActorComponent.cpp(909): ComponentWorld->GetLatentActionManager().ProcessLatentActions(this, ComponentWorld->GetDeltaSeconds());
D:\Software\UE4\Engine\UE_4.19\Engine\Source\Runtime\Engine\Private\Actor.cpp(889): MyWorld->GetLatentActionManager().ProcessLatentActions(this, MyWorld->GetDeltaSeconds());
D:\Software\UE4\Engine\UE_4.19\Engine\Source\Runtime\Engine\Private\LevelTick.cpp(1468): CurrentLatentActionManager.ProcessLatentActions(NULL, DeltaSeconds);
D:\Software\UE4\Engine\UE_4.19\Engine\Source\Runtime\UMG\Private\UserWidget.cpp(1347): World->GetLatentActionManager().ProcessLatentActions(this, InDeltaTime);
上面几个调用都是在各自的Tick函数中执行的。
综上:当某个Object中有Delay操作执行时,会向FLatentActionManager中添加一个对应的Action并且立即返回,后续Level或者这个Object的Tick函数就会执行到这个Action的UpdateOperation中,FLatentActionManager的实现机制保证了UpdateOperation在本帧只会执行一次。Action的UpdateOperation就是Delay的相关逻辑判断,如果条件达到了就通知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);
};
```javascript
#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的成员函数实现。
DoneIf:结束UpdateOperation循环并销毁FPendingLatentAction派生类对象
FinishAndTiggerIf :回调并销毁
TriggerLink :回调
这里要注意,一定要在最后调用DoneIf或FinishAndTiggerIf,否则Latent节点内的循环会持续执行直到游戏结束。
执行结果
说在最后
多执行节点输出的设计不是必须的,像官方的Delay节点就只有一个输出。在这种情况下不需要多余的exec参数,使用默认参数直接回调即可,而Latent节点的默认输出名会变为Completed。读者朋友们可以尝试各种不同的组合,官方Delay可参考UE_4.18\Engine\Source\Runtime\Engine\Classes\Kismet\KismetSystemLibrary.h
参考链接:https://blog.youkuaiyun.com/flowersplug/article/details/80408493?utm_source=blogxgwz0