UE4 synchronous and asynchronous loading

UE4 synchronous and asynchronous loading

1. Resource reference

Resource reference is divided into resource soft reference and resource hard reference. Soft references are usually references that only store the resource path of resource objects without coupling with resources (soft references are loaded into memory, reference objects will not be loaded into memory, and will be loaded into memory only when necessary). Hard reference is the actual member variable of the resource object, which is directly coupled with the resource object (if the hard reference is loaded into memory, the referenced object resources will also be loaded into memory).

Performance of soft reference and hard reference in blueprint:

// Hard reference: has been loaded into memory
UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Frame")
AActor* ActorObject;

UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Frame")
TSubclassOf<AActor> ActorClass;


The commonly used soft reference types are:

  • TSoftObjectPtr
  • TSoftClassPtr
  • FSoftObjectPath
  • FSoftClassPath

TSoftObjectPtr and TSoftClassPtr are smart pointers. For the concept and usage of smart pointer, please check my blog post C + + smart pointer_ WLSTLA-CSDN blog . TSoftObjectPtr and TSoftClassPtr are encapsulation of FSoftObjectPtr.

//TSoftObjectPtr source code
template<class T=UObject>
struct TSoftObjectPtr
{
public:
	...
	FORCEINLINE T* Get() const //Get the real resource object pointer
	{
		return dynamic_cast<T*>(SoftObjectPtr.Get());
	}
    ...
	FORCEINLINE const FSoftObjectPath& ToSoftObjectPath() const //Get FSfotObejctPath
	{
		return SoftObjectPtr.GetUniqueID();
	}
private:
	FSoftObjectPtr SoftObjectPtr;//Packaged FSoftObjectPtr
};

Get the real resource reference pointer through Get(), and ToSoftObjectPath() get the resource path of FSoftObjectPath type.

TSoftClassPtr also encapsulates FSoftObjectPtr. At the same time, it can contain subclasses like TSubclassOf. At the same time, it also has Get() and ToSoftObjectPath() functions.

UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Frame")
TSoftObjectPtr<AActor> softActorobj;

UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Frame")
TSoftClassPtr<AActor> softActorClass;

Expression in blueprint:

FSoftObjectPath and FSoftClassPath are resource soft reference paths. FSoftObjectPath is mainly used to save soft reference types inherited from UObject non blueprint resources: for example (UStaticMesh, UMaterial, UTexture...). FSoftClassPath is a derived class of FSoftObjectPath, which is used to store a type of soft reference (such as blueprint class resource).

UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="AssetRef",meta=(AllowedClasses="StaticMesh"))
FSoftObjectPath AsetObjectPath;

UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="AssetRef")
FSoftClassPath AsetClassPath;


In addition to the above common reference types, there are actually the following two types:

  • FStringAssetReference
  • TAssetPtr<Type>

FStringAssetReference is actually an alias of FSoftObjectPath. They are of the same type:

typedef FSoftObjectPath FStringAssetReference;

Tassetptr < type > is an alias for tsotobjectptr:

template<class T=UObject>
using TAssetPtr = TSoftObjectPtr<T>;

When we use it, we'd better use only the four reference types mentioned above, and discard the two.

Difference between hard reference and soft reference:

  • Too many hard references will lead to long program startup time and process blocking caused by loading a large number of resources.
  • Soft reference has less resident memory because there are few loading resources and the resources are loaded only when they are used.

2. Resource loading

2.1 static loading

Static loading refers to the loading method that can only be performed in the constructor. Use ConstructorHelpers::FClassFinder() \ ConstructorHelpers::FObjectFinder(). The former is used to load blueprint resources and the latter is used to load non blueprint resources. Header file #include "Object/ConstructorHelpers.h"

#include "UObject/ConstructorHelpers.h"   
AResourceActor::AResourceActor()
{
   PrimaryActorTick.bCanEverTick = true;
   ConstructorHelpers::FObjectFinder<UStaticMesh> meshAsset(TEXT("/Game/Meshes/1M_Cube.1M_Cube"));
   if(meshAsset.Succeeded())
   {
      UE_LOG(LogTemp,Warning,TEXT("Mesh Name%s"),*(meshAsset.Object->GetName()));
   }

   ConstructorHelpers::FClassFinder<AActor> blueprintAsset(TEXT("/Game/CPP/BlueprintTest.BlueprintTest_C"));
   if(blueprintAsset.Succeeded())
   {
      UE_LOG(LogTemp,Warning,TEXT("Blueprint Name%s"),*blueprintAsset.Class->GetName());
   }
}

Note that when loading the Blueprint class, the road strength needs to be added at last_ C. For example, the Blueprint path is: Blueprint '/ Game/Blueprints/Test',
After the suffix is added, it is: Blueprint '/ Game/Blueprints/Test_C’.

2.2 dynamic loading

The limitation of static loading is that the road strength of resources must be written dead, which leads to that once the road strength of resources changes, the code must be modified and can only be called in the constructor. Static loading is very inconvenient for us to load resources, so dynamic loading is introduced.

Before understanding synchronous loading, we first need to know how the resource object in UE4 exists. There are two ways for resource objects to exist in UE4: one is that they have been loaded into memory (use FindObject() to load resources), and the other is that they are in the hard disk (this requires the LoadClass()\LoadObject() method to load). Then, when serializing and deserializing, you also need to deal with the reference of object assets: for example, object A references object B. when instantiating object A, the loading method of object B depends on whether the reference of object B is A hard reference (pointer reference) or A soft reference (road reference).

Next, synchronous and asynchronous loading in dynamic loading are introduced:

2.2.1 dynamic synchronous loading

The following methods are generally used for synchronous loading:

// Load directly from memory
- FindObject();

// Load from hard disk
- LoadObject();
- LoadClass();
- StaticLoadObject();
- StaticLoadClass();

When the resources we need have been loaded into memory, we can directly use findobject (a layer of encapsulation of StaticFindObject) to load resources (such as the built-in resource of the engine: StartContent...): Here we load a StaticMesh from the FirstPerson package, and then Ctrl+C copy the resource path.

UStaticMesh* MeshPtr = FindObject<UStaticMesh>(ANY_PACKAGE,TEXT("StaticMesh'/Game/Meshes/1M_Cube.1M_Cube'"));
if(IsValid(MeshPtr))
{
UE_LOG(LogTemp,Warning,TEXT("Get Mesh %s"),*MeshPtr->GetName());
}

LoadObject() and LoadClass() are a layer of encapsulation for StaticLoadObject() and StaticLoadClass() respectively:

template< class T > 
inline T* LoadObject( UObject* Outer, const TCHAR* Name, const TCHAR* Filename=nullptr, uint32 LoadFlags=LOAD_None, UPackageMap* Sandbox=nullptr )
{
	return (T*)StaticLoadObject( T::StaticClass(), Outer, Name, Filename, LoadFlags, Sandbox );
}

Therefore, when we use it, we'd better use the LoadObject() and LoadClass() encapsulated for us. StaticLoadObject() and StaticLoadClass() are not applicable.

LoadObject is used to load non blueprint resources. It is generally used in conjunction with FindObject. When the resource is not loaded into memory, it is loaded from the hard disk:

// Load directly from memory
UStaticMesh* meshPtr = FindObject<UStaticMesh>(nullptr,TEXT("StaticMesh'/Game/HorrorEngine/Meshes/Book.Book'"));
if(!meshPtr)
{
	GEngine->AddOnScreenDebugMessage(1,50.0f,FColor(255,0,0),TEXT("Not Found Mesh In Memeory"));
     // Load from hard disk
	meshPtr = LoadObject<UStaticMesh>(nullptr,TEXT("StaticMesh'/Game/HorrorEngine/Meshes/Book.Book'"));
	if(meshPtr)
	{
		GEngine->AddOnScreenDebugMessage(2,50.0f,FColor(255,0,0),TEXT("Load Mesh From Hard Disk"));
	}
}
else
{
	GEngine->AddOnScreenDebugMessage(1,50.0f,FColor(255,0,0),TEXT("Found Mesh In Memeory"));
}

LoadClass is used to load blueprint class objects inherited from AActor:

UClass* actorClass = LoadClass<AActor>(nullptr,TEXT("/Game/CPP/MySpawnActor.MySpawnActor_C"));
if(actorClass)
{
    // Generate Actor
    //GetWorld()->SpawnActor<AActor>(actorClass,FVector::ZeroVector,FRotator::ZeroRotator);
    UE_LOG(LogTemp,Warning,TEXT("Get Blueprint Actor %s"),*actorClass->GetName());
}

If you want to load multiple resource objects into memory, it will be inconvenient to always load class. We can use UE4 to provide us with a way to create a resource registry (DataAsset). In the blueprint, set the road strength of resources and load resources in C + +.

UDataAsset

USTRUCT()
struct FResourceData
{
	GENERATED_BODY()
	UPROPERTY(EditAnywhere,Category="Resource")
	FString ResourceName;

	UPROPERTY(EditAnywhere,Category="Resource")
	FSoftObjectPath ResourcePath;
};


UCLASS()
class REFLECTIONFRAME_API UResourceAssetData:public UDataAsset
{
	GENERATED_BODY()
public:
	UPROPERTY(EditAnywhere)
	TArray<FResourceData> DataArray;

	UPROPERTY(EditAnywhere)
	TArray<UTexture2D*> TextureArray;
};

First, we need to create a derived class inherited from UDataAsset to store the registry of our resources. Then we set it in the blueprint.

Create a DataAsset in the blueprint and inherit the C + + class we just created. Then we can set our resource soft reference in this class.

UPROPERTY(EditAnywhere,Category="DataAsset")
UResourceAssetData* Data;

void AResourceActor::UpdateMesh()
{
	if(Data&&Data->DataArray.Num()>0)
	{
		MeshIndex = ++MeshIndex>=Data->DataArray.Num()?0:MeshIndex;
		UStaticMesh* meshPtr = LoadObject<UStaticMesh>(nullptr,*Data->DataArray[MeshIndex].ResourcePath.ToString());
		Mesh->SetStaticMesh(meshPtr);
	}
}

Then we can write a test function to cycle and switch the StaticMesh in our DataAsset, and load the StaticMesh resources by obtaining the resource strength (tostring() in ResourcePath).

UObjectLibrary

We can also use UObjectLibrary to save all resources of the same type in a certain way to UObjectLibrary, and then Load the resources into memory.

For example, the resources to be imported are all textures under Content/HorrorEngine/Textures.

So we can write the following code:

#include "Engine/ObjectLibrary.h"
class UObjectLibrary* Library;

if(!Library)
{
	Library = UObjectLibrary::CreateLibrary(UObject::StaticClass(),false,false);
	Library->AddToRoot();	//Prevent from being GC
}
Library->LoadAssetDataFromPath(TEXT("/Game/HorrorEngine/Textures"));
// To load blueprint classes, you need to change to LoadBlueprintAssetDataFromPath, and set bInHasBlueprintClasses to true

TArray<FAssetData> AssetDatas;
Library->GetAssetDataList(AssetDatas);
if(AssetDatas.Num()>0)
{
	for(auto const& Asset:AssetDatas)
	{
		UE_LOG(LogTemp,Warning,TEXT("Asset Name %s"),*Asset.GetAsset()->GetName());
		// Asset.ToSoftObjectPath(); Get SoftObjectPath
	}	
}

CreateLibrary() creates a library. Binhasblueprintclasses means whether this class is a blueprint class. If you need to pass in a blueprint class, you need to set it to True. Binuseweek whether to enable weak pointers.

UObjectLibrary* UObjectLibrary::CreateLibrary(UClass* InBaseClass, bool bInHasBlueprintClasses, bool bInUseWeak)

We can use synchronous loading to load a small amount of lightweight resources, but if there are more and more resources, continuing synchronous loading will lead to the blocking of the game process. At this time, we need to use asynchronous loading.

2.2.2 dynamic asynchronous loading

For asynchronous loading, we need to use the FStreamableManager class. First, we need to create this class. It is recommended to set it as a global singleton. There are two ways to create it. One is to make a creation statement by ourselves, and the other is to use a global singleton FStreamableManager that UE4 has created for us.

#include "Engine/StreamableManager.h"
FStreamableManager Manager; //We created it ourselves
UAssetManager::Get().GetStreamableManager(); //Use FStreamableManager in UAssetManager

FStreamableManager common API s:

APIdescribe
TSharedPtr<FStreamableHandle> FStreamableManager.RequestAsyncLoad()Asynchronous loading
TSharedPtr<FStreamableHandle> FStreamableManager.RequestSyncLoad()Synchronous loading
UObject* FStreamableManager.LoadSynchronous()One layer encapsulation of RequesSyncLoad(), synchronous loading

FStreamableHandle is the returned FStreamableManager handle, which can be used to obtain loaded resources and some expansion operations.

FStreamableHandle common API s:

APIdescribe
FStreamableHandle.GetLoadedAssets(TArray<UObject *>& LoadedAssets)Get loaded resources
UObject* FStreamableHandle.GetLoadedAsset()Gets the first resource loaded
float FStreamableHandle.GetProgress()Get loading progress (0-1)
bool FStreamableHandle.BindCancelDelegate(FStreamableDelegate NewDelegate)Call the proxy when the binding load is cancelled
bool FStreamableHandle.BindCompleteDelegate(FStreamableDelegate NewDelegate)The proxy is called when the binding is loaded
bool FStreamableHandle.BindUpdateDelegate(FStreamableDelegate NewDelegate)Call the proxy when the binding is loaded

We can use UObjectLibrary with FStreamableManager to write the following code. Similarly, you can also use UAssetData:

TArray<FSoftObjectPath> AssetPaths; //Resource StringReference
TSharedPtr<FStreamableHandle> ManagerHandle; //Handle returned by FStreamable


void AResourceActor::AsyncLoadObjectReview()
{
	if(!Library)
	{
		Library = UObjectLibrary::CreateLibrary(UObject::StaticClass(),false,false);
		Library->AddToRoot();
	}
	Library->LoadAssetDataFromPath(TEXT("/Game/Meshes"));
	TArray<FAssetData> Assets;
	Library->GetAssetDataList(Assets);
	if(Assets.Num()>0)
	{
		for(auto Asset:Assets)
		{
			AssetPaths.AddUnique(Asset.ToSoftObjectPath());
		}	
	}
	ManagerHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(AssetPaths);
	FStreamableDelegate DelegateFinished = FStreamableDelegate::CreateUObject(this,&AResourceActor::AsyncLoadObjectReviewFinished);
	FStreamableUpdateDelegate DelegateUpdate = FStreamableUpdateDelegate::CreateUObject(this,&AResourceActor::AsyncLoadObjectReviewUpdate);
	ManagerHandle->BindCompleteDelegate(DelegateFinished);
	ManagerHandle->BindUpdateDelegate(DelegateUpdate);
}


void AResourceActor::AsyncLoadObjectReviewFinished()
{
	TArray<UObject*> objectArray;
	ManagerHandle->GetLoadedAssets(objectArray);
	if(objectArray.Num()>0)
	{
		for(auto const& object:objectArray)
		{
			UE_LOG(LogTemp,Warning,TEXT("Load Object %s"),*object->GetName());
		}
	}else
	{
		UE_LOG(LogTemp,Warning,TEXT("Can't Load Object"));
	}
	
}

void AResourceActor::AsyncLoadObjectReviewUpdate(TSharedRef<struct FStreamableHandle> Handle)
{
	UE_LOG(LogTemp,Warning,TEXT("current progress %f"),Handle->GetProgress());
}

Keywords: C++ Game Development UE4 unreal

Added by cybercog on Sun, 28 Nov 2021 21:43:20 +0200