[UE4] use StreamableManager to load resources

Keywords: Game Development

[UE4] method of loading resources (VI) using StreamableManager for loading

References & original links

TAssetPtr and FStreamableManager of AssetManager series

UE4 resource loading method

Unreal4 load resources asynchronously

(ue4.20) UE4 synchronous loading and asynchronous loading uobject ------------- loadobject, loadclass, fstreamablemanager

UE4 asynchronous resource loading

StreamableManager and asynchronous loading

introduce

/** A native class for managing streaming assets in and keeping them in memory. AssetManager is the global singleton version of this with blueprint access */
struct ENGINE_API FStreamableManager : public FGCObject

A native class used to manage flow assets and save them in memory. AssetManager is a global singleton version with blueprint access.

First, you need to create an FStreamableManager. I suggest that you put it in a global game singleton object, such as the object specified in DefaultEngine.ini using GameSingletonClassName.

PS: if you have other singletons to use, I suggest you get all singletons into GameInstance, because only one singleton class can be specified in the engine. In addition to specifying it as a singleton in the engine, there is another singleton implementation method, which is written in GameInstance.

First inherit GameInstance, and then create a static FStreamableManager pointer in it. After introducing the header file in CPP, initialize FStreamableManager to nullptr, then rewrite the Init method of GameInstance, give FStreamableManager new in it, and then assign it to the FStreamableManager pointer variable, Get GameInstance when necessary, and click GameInstance directly to use it. Other classes that need to be singletons are also common. If you have the opportunity to talk about it in an article, I won't repeat it here.

PS: it is found in version 4.26 that it is already a global singleton. This is a special way to write, because there is a streamable manager in the latest AssetManager, which we can use directly. I don't know which big man found it. It can be written like this --:

UAssetManager::GetStreamableManager()

It is suggested to use this writing method, which is convenient and fast.

Once you get the StreamableManager object, you can pass FSoftObjectPath to it and start loading. Synchronous load makes a simple block load and returns the object.

Load a single resource synchronously

	/** 
	 * Synchronously load the referred asset and return the loaded object, or nullptr if it can't be found. This can be very slow and may stall the game thread for several seconds.
	 * 
	 * @param Target				Specific asset to load off disk
	 * @param bManageActiveHandle	If true, the manager will keep the streamable handle active until explicitly released
	 * @param RequestHandlePointer	If non-null, this will set the handle to the handle used to make this request. This useful for later releasing the handle
	 */
	UObject* LoadSynchronous(const FSoftObjectPath& Target, bool bManageActiveHandle = false, TSharedPtr<FStreamableHandle>* RequestHandlePointer = nullptr);

Parameter interpretation:

  • Synchronously load the referenced resource and return the loaded object. If not found, return nullptr. This can be very slow and may pause the game thread for a few seconds.
  • Target the specific asset to load the disk.
  • bManageActiveHandle if true, the manager will keep the flow handle active until it is explicitly released.
  • RequestHandlePointer if not empty, this sets the handle to the handle that issued the request. This is useful for releasing handles later.

Handle introduction:

/** A handle to a synchronous or async load. As long as the handle is Active, loaded assets will stay in memory*/
/**Handle loaded synchronously or asynchronously. As long as the handle is Active, the loaded resources remain in memory.*/

You can control the release of assets by controlling the display release of handles.

It has several wrapper loading functions:

/** Typed wrappers */
template< typename T >
T* LoadSynchronous(const FSoftObjectPath& Target, bool bManageActiveHandle = false, TSharedPtr<FStreamableHandle>* RequestHandlePointer = nullptr);

template< typename T >
T* LoadSynchronous(const TSoftObjectPtr<T>& Target, bool bManageActiveHandle = false, TSharedPtr<FStreamableHandle>* RequestHandlePointer = nullptr)
	
template< typename T >
TSubclassOf<T> LoadSynchronous(const TSoftClassPtr<T>& Target, bool bManageActiveHandle = false, TSharedPtr<FStreamableHandle>* RequestHandlePointer = nullptr)

Example code:

void UWC_TestUI::OnBtnClickCommonBtn_SmSyncOne()
{
FSoftObjectPath Path = FString(TEXT("Texture2D'/Game/UI/Images/InterestingUE4.InterestingUE4'"));
		UTexture2D* Img = UAssetManager::GetStreamableManager().LoadSynchronous<UTexture2D>(Path,false,nullptr);
		if (Img_SmSync)
		{
			Img_SmSync->SetBrushFromTexture(Img);
		}
}

This method may be suitable for small objects, but it may cause the main thread to stagnate for a long time. In this case, you will need to use RequestAsyncLoad, which will load a group of resources asynchronously and invoke the delegate after completion.

In fact, FStreamableManager provides two interfaces: RequestSyncLoad (synchronous) and requestasyncload (asynchronous). The difference between them is that the synchronous method loads immediately, so there is no callback function that has completed loading, but it will return a tsharedptr < fstreamablehandle >, and the asynchronous method will return a loaded callback function in addition to tsharedptr < fstreamablehandle >, which can be passed in when calling the asynchronous method using Lambda expression.

In addition to LoadSynchronous, you can also use requestsynchronous:

void UWC_TestUI::OnBtnClickCommonBtn_SmSyncOne()
{
	FSoftObjectPath Path = FString(TEXT("Texture2D'/Game/UI/Images/InterestingUE4.InterestingUE4'"));
	//Note: before the resource is loaded, the code will pause in this line to wait for the resource to load.
	TSharedPtr<FStreamableHandle> SyncStreamableHandle = UAssetManager::GetStreamableManager().RequestSyncLoad(Path);
	if (SyncStreamableHandle)
	{
		UTexture2D * UImg2D = Cast<UTexture2D>(SyncStreamableHandle->GetLoadedAsset());
		if (UImg2D)
		{
			Img_SmSync->SetBrushFromTexture(UImg2D);
		}
	}
}

Note that when using this method to load a single resource, use GetLoadedAsset to obtain the resource returned in FStreamableHandle.

Load resource groups synchronously

Synchronously loads a set of resources and returns a handle. This can be very slow and may pause the game thread for a few seconds.

First look at the statement:

	/** 
	 * Synchronously load a set of assets, and return a handle. This can be very slow and may stall the game thread for several seconds.
	 * 
	 * @param TargetsToStream		Assets to load off disk
	 * @param bManageActiveHandle	If true, the manager will keep the streamable handle active until explicitly released
	 * @param DebugName				Name of this handle, will be reported in debug tools
	 */
	TSharedPtr<FStreamableHandle> RequestSyncLoad(TArray<FSoftObjectPath> TargetsToStream, bool bManageActiveHandle = false, FString DebugName = TEXT("RequestSyncLoad Array"));
	TSharedPtr<FStreamableHandle> RequestSyncLoad(const FSoftObjectPath& TargetToStream, bool bManageActiveHandle = false, FString DebugName = TEXT("RequestSyncLoad Single"));

Parameter interpretation:

  • TargetsToStream the asset disk to load.
  • bManageActiveHandle if true, the manager will keep the flow handle active until it is explicitly released.
  • DebugName the name of this handle, which will be reported in the debugging tool.

Example code:

void UWC_TestUI::OnBtnClickCommonBtn_SmSyncGroup()
{
	TArray<FSoftObjectPath> Paths;
	Paths.AddUnique(FString(TEXT("Texture2D'/Game/UI/Images/InterestingUE4.InterestingUE4'")));
	Paths.AddUnique(FString(TEXT("Texture2D'/Game/UI/Images/VS.VS'")));
	//Note: before the resource is loaded, the code will pause in this line to wait for the resource to load.
	TSharedPtr<FStreamableHandle> SyncStreamableHandle = UAssetManager::GetStreamableManager().RequestSyncLoad(Paths);
	if (SyncStreamableHandle)
	{
		TArray<UObject *>LoadedAssets;
		SyncStreamableHandle->GetLoadedAssets(LoadedAssets);
		if (LoadedAssets.Num() > 0)
		{
			UTexture2D * UImg2D;
			for (int32 i = 0 ; i < LoadedAssets.Num() ; i ++)
			{
				UImg2D = Cast<UTexture2D>(LoadedAssets[i]);
				if (UImg2D)
				{
					//Img's name
					FName ImgName = FName( *FString(TEXT("Img_") + FString::FromInt(i)));
					//Create an Image dynamically
					UImage* Image = WidgetTree->ConstructWidget<UImage>(UImage::StaticClass(),ImgName);
					Image->SetBrushFromTexture(UImg2D);
					HB_SmAsyncGroup->AddChild(Image);
				}
			}
		}
	}
}

Note that when using this method to load multiple resources, use GetLoadedAssets to obtain the resources returned in FStreamableHandle, and pass in the array to be returned.

Load a single resource asynchronously

Example code:

void UWC_TestUI::OnBtnClickCommonBtn_SmAsyncOne()
{
	FSoftObjectPath SmSyncGroupTexturePath = TEXT("Texture2D'/Game/UI/Images/VS.VS'");
	TSharedPtr<FStreamableHandle> OneHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(SmSyncGroupTexturePath);
	if (OneHandle)
	{
		UTexture2D * UImg2D = Cast<UTexture2D>(OneHandle->GetLoadedAsset());
		if (UImg2D)
		{
			Img_SmAsync->SetBrushFromTexture(UImg2D);
		}
	}
}

Load resource groups asynchronously

This is the main streaming operation. The request flow for one or more target objects. When complete, the delegate function is called. Returns a flowable handle.

Old rules, or just code:

/** 
	 * This is the primary streamable operation. Requests streaming of one or more target objects. When complete, a delegate function is called. Returns a Streamable Handle.
	 *
	 * @param TargetsToStream		Assets to load off disk
	 * @param DelegateToCall		Delegate to call when load finishes. Will be called on the next tick if asset is already loaded, or many seconds later
	 * @param Priority				Priority to pass to the streaming system, higher priority will be loaded first
	 * @param bManageActiveHandle	If true, the manager will keep the streamable handle active until explicitly released
	 * @param bStartStalled			If true, the handle will start in a stalled state and will not attempt to actually async load until StartStalledHandle is called on it
	 * @param DebugName				Name of this handle, will be reported in debug tools
	 */
TSharedPtr<FStreamableHandle> RequestAsyncLoad(TArray<FSoftObjectPath> TargetsToStream, FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = DefaultAsyncLoadPriority, bool bManageActiveHandle = false, bool bStartStalled = false, FString DebugName = TEXT("RequestAsyncLoad ArrayDelegate"));
TSharedPtr<FStreamableHandle> RequestAsyncLoad(const FSoftObjectPath& TargetToStream, FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = DefaultAsyncLoadPriority, bool bManageActiveHandle = false, bool bStartStalled = false, FString DebugName = TEXT("RequestAsyncLoad SingleDelegate"));

Parameter interpretation:

  • TargetsToStream the asset disk to load.
  • The DelegateToCall delegate is called when the load is complete. Will be called at the next Tick if the asset has been loaded, or many seconds later.
  • Priority priority is passed to the flow system, and the high priority will be loaded first.
  • bManageActiveHandle if true, the manager will keep the flow handle active until it is explicitly released.
  • bStartStalled if true, the handle will start in a stalled state and the actual asynchronous load will not be attempted before calling StartStalledHandle.
  • DebugName the name of this handle, which will be reported in the debugging tool.

Similarly, it also has packaging, but it is packaged with Lambda:

/** Lambda Wrappers. Be aware that Callback may go off multiple seconds in the future. */
TSharedPtr<FStreamableHandle> RequestAsyncLoad(TArray<FSoftObjectPath> TargetsToStream, TFunction<void()>&& Callback, TAsyncLoadPriority Priority = DefaultAsyncLoadPriority, bool bManageActiveHandle = false, bool bStartStalled = false, FString DebugName = TEXT("RequestAsyncLoad ArrayLambda"));
TSharedPtr<FStreamableHandle> RequestAsyncLoad(const FSoftObjectPath& TargetToStream, TFunction<void()>&& Callback, TAsyncLoadPriority Priority = DefaultAsyncLoadPriority, bool bManageActiveHandle = false, bool bStartStalled = false, FString DebugName = TEXT("RequestAsyncLoad SingleLambda"));

Note: after loading the resource, use Get to Get it. The second call to Get will cancel the reference:

	/**
	 * Dereference the soft pointer.
	 *
	 * @return nullptr if this object is gone or the lazy pointer was null, otherwise a valid UObject pointer
	 */
	FORCEINLINE T* Get() const
	{
		return dynamic_cast<T*>(SoftObjectPtr.Get());
	}

Example code:

//.h
public:
	UPROPERTY(EditAnywhere,BlueprintReadWrite, Category = "Test")
	TArray<TSoftObjectPtr<UTexture2D>> ObjectPtrs;

//.cpp
void UWC_TestUI::OnBtnClickCommonBtn_SmAsyncGroup()
{
	if (ObjectPtrs.Num() <= 0)
	{
		return;
	}
	TArray<FSoftObjectPath> SmSyncGroupTexturePaths;
	for (auto item:ObjectPtrs)
	{
		SmSyncGroupTexturePaths.AddUnique(item.ToSoftObjectPath());
	}
	UAssetManager::GetStreamableManager().RequestAsyncLoad(SmSyncGroupTexturePaths,FStreamableDelegate::CreateUObject(this,&UWC_TestUI::OnStreamableManagerAsyncLoadCompleted));
}

void UWC_TestUI::OnStreamableManagerAsyncLoadCompleted()
{
	UTexture2D * UImg2D;
	for (int32 i = 0 ; i < ObjectPtrs.Num() ; i ++)
	{
		//If the type is specified earlier, there is no need to force conversion here
		//UTexture2D * UImg2D = Cast<UTexture2D>(item.Get());
		//Note: use Get to Get the reference of the resource
		UImg2D = ObjectPtrs[i].Get();
		if (UImg2D)
		{
			//Img's name
			FName ImgName = FName( *FString(TEXT("Img_") + FString::FromInt(i)));
			//Create an Image dynamically
			UImage* Image = WidgetTree->ConstructWidget<UImage>(UImage::StaticClass(),ImgName);
			Image->SetBrushFromTexture(UImg2D);
			HB_SmSyncGroup->AddChild(Image);
		}
	}
}

ObjectPtrs are exposed to the outside, and the asset path is manually specified by themselves.

characteristic

This loading method is characterized by:

  • It also supports synchronous and asynchronous loading, and the selection and control are flexible.
  • The amount of code is slightly larger than the previous methods, but it is not difficult to understand, and the structure is relatively clear.
  • Synchronous loading uses the returned tsharedptr < fstreamablehandle > handle and GetLoadedAsset to obtain synchronously loaded resources.
  • Asynchronous loading is to obtain the loaded resources in the form of callback function and Get().

Article label

Game development, game development foundation, Unreal Engine, UE resource loading.

Posted by Loriq on Sun, 07 Nov 2021 12:59:00 -0800