写这篇文字之前一直有两个问题:
1. Delay操作是如何实现的?
2. 是否可以使用Delay的实现机制来处理一些需要异步处理的东西,比如一些读写数据?
我们先看下Delay的使用和实现:
按下1键,5秒后会打印出文字。需要说明的是:调用完Delay节点后,Completed的pin在5秒后才会调用,所以应该把这一帧需要执行的操作放在delay节点前。
注意Delay和Window编程中的Sleep(5000)操作完全不同,Sleep是挂起当前的线程,把执行权交给其他线程,大约5秒后再唤醒这个线程。Delay并没有挂起本线程,而是立即返回,5秒后再来执行Completed Pin后的节点。
了解了Delay的使用后,我们看下Delay的实现:
/**
* Perform a latent action with a delay (specified in seconds). Calling again while it is counting down will be ignored.
*
* @param WorldContext World context.
* @param Duration length of delay (in seconds).
* @param LatentInfo The latent action.
*/
UFUNCTION(BlueprintCallable, Category="Utilities|FlowControl", meta=(Latent, WorldContext="WorldContextObject", LatentInfo="LatentInfo", Duration="0.2", Keywords="sleep"))
static void Delay(UObject* WorldContextObject, float Duration, struct FLatentActionInfo LatentInfo );
void UKismetSystemLibrary::Delay(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));
}
}
}
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
};
Delay的实现都在这里了,我们是Delay了5秒并且是在关卡蓝图里调用的,构建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后面的节点执行即可。
那么我们现在看下上面提到的两个问题:
1. Delay操作是如何实现的?这个上面一段话已经解释了。
2. 是否可以使用Delay的实现机制来处理一些需要异步处理的东西,比如一些读写数据?这个得看下具体要实现什么,如果Action的UpdateOperation中执行一些需要时间的逻辑(磁盘读数据,网络等数据,计算时间很长的),那么是不可以的,因为UpdateOperation阻塞或者等待其实会导致游戏线程的Tick阻塞和等待,会导致游戏画面卡死等问题,UpdateOperation应该执行一些不会等待的东西,比如可以查看某些状态是否已经完成了等等。