// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "UObject/StrongObjectPtr.h" #include "Engine/AssetManager.h" #include "Async/Future.h" #include "Containers/Ticker.h" class FAsyncCondition; class UPrimaryDataAsset; DECLARE_DELEGATE_OneParam(FStreamableHandleDelegate, TSharedPtr) //TODO I think we need to introduce a retention policy, preloads automatically stay in memory until canceled // but what if you want to preload individual items just using the AsyncLoad functions? I don't want to // introduce individual policies per call, or introduce a whole set of preload vs asyncloads, so would // would rather have a retention policy. Should it be a member and actually create real memory when // you inherit from AsyncMixin, or should it be a template argument? //enum class EAsyncMixinRetentionPolicy : uint8 //{ // Default, // KeepResidentUntilComplete, // KeepResidentUntilCancel //}; /** * The FAsyncMixin allows easier management of async loading requests, to ensure linear request handling, to make * writing code much easier. The usage pattern is as follows, * * First - inherit from FAsyncMixin, even if you're a UObject, you can also inherit from FAsyncMixin. * * Then - you can make your async loads as follows. * * CancelAsyncLoading(); // Some objects get reused like in lists, so it's important to cancel anything you had pending doesn't complete. * AsyncLoad(ItemOne, CallbackOne); * AsyncLoad(ItemTwo, CallbackTwo); * StartAsyncLoading(); * * You can also include the 'this' scope safely, one of the benefits of the mix-in, is that none of the callbacks * are ever out of scope of the host AsyncMixin derived object. * e.g. * AsyncLoad(SomeSoftObjectPtr, [this, ...]() { * * }); * * * What will happen is first we cancel any existing one(s), e.g. perhaps we are a widget that just got told to represent * some new thing. What will happen is we'll Load ItemOne and ItemTwo, *THEN* we'll call the callbacks in the order you * requested the async loads - even if ItemOne or ItemTwo was already loaded when you request it. * * When all the async loading requests complete, OnFinishedLoading will be called. * * If you forget to call StartAsyncLoading(), we'll call it next frame, but you should remember to call it * when you're done with your setup, as maybe everything is already loaded, and it will avoid a single frame * of a loading indicator flash, which is annoying. * * NOTE: The FAsyncMixin also makes it safe to pass [this] as a captured input into your lambda, because it handles * unhooking everything if either your owner class is destroyed, or you cancel everything. * * NOTE: FAsyncMixin doesn't add any additional memory to your class. Several classes currently handling async loading * internally allocate TSharedPtr members and tend to hold onto SoftObjectPaths temporary state. The * FAsyncMixin does all of this internally with a static TMap so that all of the async request memory is stored temporarily * and sparsely. * * NOTE: For debugging and understanding what's going on, you should add -LogCmds="LogAsyncMixin Verbose" to the command line. */ class ASYNCMIXIN_API FAsyncMixin : public FNoncopyable { protected: FAsyncMixin(); public: virtual ~FAsyncMixin(); protected: /** Called when loading starts. */ virtual void OnStartedLoading() { } /** Called when all loading has finished. */ virtual void OnFinishedLoading() { } protected: /** Async load a TSoftClassPtr, call the Callback when complete. */ template void AsyncLoad(TSoftClassPtr SoftClass, TFunction&& Callback) { AsyncLoad(SoftClass.ToSoftObjectPath(), FSimpleDelegate::CreateLambda(MoveTemp(Callback))); } /** Async load a TSoftClassPtr, call the Callback when complete. */ template void AsyncLoad(TSoftClassPtr SoftClass, TFunction)>&& Callback) { AsyncLoad(SoftClass.ToSoftObjectPath(), FSimpleDelegate::CreateLambda([SoftClass, UserCallback = MoveTemp(Callback)]() mutable { UserCallback(SoftClass.Get()); }) ); } /** Async load a TSoftClassPtr, call the Callback when complete. */ template void AsyncLoad(TSoftClassPtr SoftClass, const FSimpleDelegate& Callback = FSimpleDelegate()) { AsyncLoad(SoftClass.ToSoftObjectPath(), Callback); } /** Async load a TSoftObjectPtr, call the Callback when complete. */ template void AsyncLoad(TSoftObjectPtr SoftObject, TFunction&& Callback) { AsyncLoad(SoftObject.ToSoftObjectPath(), FSimpleDelegate::CreateLambda(MoveTemp(Callback))); } /** Async load a TSoftObjectPtr, call the Callback when complete. */ template void AsyncLoad(TSoftObjectPtr SoftObject, TFunction&& Callback) { AsyncLoad(SoftObject.ToSoftObjectPath(), FSimpleDelegate::CreateLambda([SoftObject, UserCallback = MoveTemp(Callback)]() mutable { UserCallback(SoftObject.Get()); }) ); } /** Async load a TSoftObjectPtr, call the Callback when complete. */ template void AsyncLoad(TSoftObjectPtr SoftObject, const FSimpleDelegate& Callback = FSimpleDelegate()) { AsyncLoad(SoftObject.ToSoftObjectPath(), Callback); } /** Async load a FSoftObjectPath, call the Callback when complete. */ void AsyncLoad(FSoftObjectPath SoftObjectPath, const FSimpleDelegate& Callback = FSimpleDelegate()); /** Async load an array of FSoftObjectPath, call the Callback when complete. */ void AsyncLoad(const TArray& SoftObjectPaths, TFunction&& Callback) { AsyncLoad(SoftObjectPaths, FSimpleDelegate::CreateLambda(MoveTemp(Callback))); } /** Async load an array of FSoftObjectPath, call the Callback when complete. */ void AsyncLoad(const TArray& SoftObjectPaths, const FSimpleDelegate& Callback = FSimpleDelegate()); /** Given an array of primary assets, it loads all of the bundles referenced by properties of these assets specified in the LoadBundles array. */ template void AsyncPreloadPrimaryAssetsAndBundles(const TArray& Assets, const TArray& LoadBundles, const FSimpleDelegate& Callback = FSimpleDelegate()) { TArray PrimaryAssetIds; for (const T* Item : Assets) { PrimaryAssetIds.Add(Item); } AsyncPreloadPrimaryAssetsAndBundles(PrimaryAssetIds, LoadBundles, Callback); } /** Given an array of primary asset ids, it loads all of the bundles referenced by properties of these assets specified in the LoadBundles array. */ void AsyncPreloadPrimaryAssetsAndBundles(const TArray& AssetIds, const TArray& LoadBundles, TFunction&& Callback) { AsyncPreloadPrimaryAssetsAndBundles(AssetIds, LoadBundles, FSimpleDelegate::CreateLambda(MoveTemp(Callback))); } /** Given an array of primary asset ids, it loads all of the bundles referenced by properties of these assets specified in the LoadBundles array. */ void AsyncPreloadPrimaryAssetsAndBundles(const TArray& AssetIds, const TArray& LoadBundles, const FSimpleDelegate& Callback = FSimpleDelegate()); /** Add a future condition that must be true before we move forward. */ void AsyncCondition(TSharedRef Condition, const FSimpleDelegate& Callback = FSimpleDelegate()); /** * Rather than load anything, this callback is just inserted into the callback sequence so that when async loading * completes this event will be called at the same point in the sequence. Super useful if you don't want a step to be * tied to a particular asset in case some of the assets are optional. */ void AsyncEvent(TFunction&& Callback) { AsyncEvent(FSimpleDelegate::CreateLambda(MoveTemp(Callback))); } /** * Rather than load anything, this callback is just inserted into the callback sequence so that when async loading * completes this event will be called at the same point in the sequence. Super useful if you don't want a step to be * tied to a particular asset in case some of the assets are optional. */ void AsyncEvent(const FSimpleDelegate& Callback); /** Flushes any async loading requests. */ void StartAsyncLoading(); /** Cancels any pending async loads. */ void CancelAsyncLoading(); /** Is async loading current in progress? */ bool IsAsyncLoadingInProgress() const; private: /** * The FLoadingState is what actually is allocated for the FAsyncMixin in a big map so that the FAsyncMixin itself holds no * no memory, and we dynamically create the FLoadingState only if needed, and destroy it when it's unneeded. */ class FLoadingState : public TSharedFromThis { public: FLoadingState(FAsyncMixin& InOwner); virtual ~FLoadingState(); /** Starts the async sequence. */ void Start(); /** Cancels the async sequence. */ void CancelAndDestroy(); void AsyncLoad(FSoftObjectPath SoftObject, const FSimpleDelegate& DelegateToCall); void AsyncLoad(const TArray& SoftObjectPaths, const FSimpleDelegate& DelegateToCall); void AsyncPreloadPrimaryAssetsAndBundles(const TArray& PrimaryAssetIds, const TArray& LoadBundles, const FSimpleDelegate& DelegateToCall); void AsyncCondition(TSharedRef Condition, const FSimpleDelegate& Callback); void AsyncEvent(const FSimpleDelegate& Callback); bool IsLoadingComplete() const { return !IsLoadingInProgress(); } bool IsLoadingInProgress() const; bool IsLoadingInProgressOrPending() const; bool IsPendingDestroy() const; private: void CancelOnly(bool bDestroying); void CancelStartTimer(); void TryScheduleStart(); void TryCompleteAsyncLoading(); void CompleteAsyncLoading(); private: void RequestDestroyThisMemory(); void CancelDestroyThisMemory(bool bDestroying); /** Who owns the loading state? We need this to call back into the owning mix-in object. */ FAsyncMixin& OwnerRef; /** * Did we need to pre-load bundles? If we didn't pre-load bundles (which require you keep the streaming handle * around or they will be destroyed), then we can safely destroy the FLoadingState when everything is done loading. */ bool bPreloadedBundles = false; class FAsyncStep { public: FAsyncStep(const FSimpleDelegate& InUserCallback); FAsyncStep(const FSimpleDelegate& InUserCallback, const TSharedPtr& InStreamingHandle); FAsyncStep(const FSimpleDelegate& InUserCallback, const TSharedPtr& InCondition); ~FAsyncStep(); void ExecuteUserCallback(); bool IsLoadingInProgress() const { return !IsComplete(); } bool IsComplete() const; void Cancel(); bool BindCompleteDelegate(const FSimpleDelegate& NewDelegate); bool IsCompleteDelegateBound() const; private: FSimpleDelegate UserCallback; bool bIsCompletionDelegateBound = false; // Possible Async 'thing' TSharedPtr StreamingHandle; TSharedPtr Condition; }; bool bHasStarted = false; int32 CurrentAsyncStep = 0; TArray> AsyncSteps; TArray> AsyncStepsPendingDestruction; FTSTicker::FDelegateHandle StartTimerDelegate; FTSTicker::FDelegateHandle DestroyMemoryDelegate; }; const FLoadingState& GetLoadingStateConst() const; FLoadingState& GetLoadingState(); bool HasLoadingState() const; bool IsLoadingInProgressOrPending() const; private: static TMap> Loading; }; /** * Sometimes a mix-in just doesn't make sense. Perhaps the object has to manage many different jobs * that each have their own async dependency chain/scope. For those situations you can use the FAsyncScope. * * This class is a standalone Async dependency handler so that you can fire off several load jobs and always handle them * in the proper order, just like with combining FAsyncMixin with your class. */ class ASYNCMIXIN_API FAsyncScope : public FAsyncMixin { public: using FAsyncMixin::AsyncLoad; using FAsyncMixin::AsyncPreloadPrimaryAssetsAndBundles; using FAsyncMixin::AsyncCondition; using FAsyncMixin::AsyncEvent; using FAsyncMixin::CancelAsyncLoading; using FAsyncMixin::StartAsyncLoading; using FAsyncMixin::IsAsyncLoadingInProgress; }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ enum class EAsyncConditionResult : uint8 { TryAgain, Complete }; DECLARE_DELEGATE_RetVal(EAsyncConditionResult, FAsyncConditionDelegate); /** * The async condition allows you to have custom reasons to hault the async loading until some condition is met. */ class FAsyncCondition : public TSharedFromThis { public: FAsyncCondition(const FAsyncConditionDelegate& Condition); FAsyncCondition(TFunction&& Condition); virtual ~FAsyncCondition(); protected: bool IsComplete() const; bool BindCompleteDelegate(const FSimpleDelegate& NewDelegate); private: bool TryToContinue(float DeltaTime); FTSTicker::FDelegateHandle RepeatHandle; FAsyncConditionDelegate UserCondition; FSimpleDelegate CompletionDelegate; friend FAsyncMixin; };