寫這篇文字之前一直有兩個問題:
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應該執行一些不會等待的東西,比如可以查看某些狀態是否已經完成了等等。