Load wrapper
Now we've got our module in place to do the loading, lets flesh out the actual API.
Firstly, we need to define a class that's going to hold our load requests. These are essentially an additional wrapper for the
FStreamableHandle class, with additional payloads to tell the API what type we're requesting, as well as what to call when we've loaded the file.
LoadRequest.h - NEW FILE
// AirEngine 2020 - Tom Shinton
#pragma once
#include <Runtime/Engine/Classes/Engine/StreamableManager.h>
struct FBaseLoad
{
virtual bool TryResolve() { return false; }
};
template<class T>
struct FLoadRequest : public FBaseLoad
{
public:
FLoadRequest()
: Handle()
, Callback()
{};
FLoadRequest(const TSharedPtr<FStreamableHandle>& InHandle, const TFunction<void(T&)>& InCallback)
: Handle(InHandle)
, Callback(InCallback)
{};
virtual bool TryResolve() override
{
if(Callback != nullptr)
{
if(UObject* LoadedObject = Handle->GetLoadedAsset())
{
Callback(*CastChecked<T>(LoadedObject));
return true;
}
}
return false;
}
private:
TSharedPtr<FStreamableHandle> Handle;
TFunction<void(T&)> Callback;
};
We can see here that we've actually got two classes - lets break them down, and why they're necessary.
FBaseLoad
This is practically empty. It gives us the chance to sort of
exploit the polymorphism that comes with pointers, and store an array of templated types. For instance -
- - FLoadRequest templated to UStaticMesh
- - FLoadRequest templated to UTexture2D
- - FLoadRequest templated to AExplodingBarrel
Are all different types, and cannot be coerced into a single array. However, if we keep an array of pointers to their
base type, we can fudge the compiler into doing what we want. Nice.
FLoadRequest::FBaseLoad
This is main class, and does a few simple things. Firstly, it holds a reference to the streaming handle.
Secondly, it holds a
correctly typed callback, thanks to the templating.
The TryResolve is basically determining whether or not this class's desired asset load has been fulfilled. We could call Resolve directly on the StreamHandle, but that would cause a sync load on the main thread and load the asset right then and there - precisely not what we're after. The GetLoadedAsset call, however, will return nullptr if the asset isnt in memory. If the streaming manager is in the process of loading it, this field will be null, and thus a reliable metric as to whether or not this request can be fulfilled.
Lets take a look at the actual loader class, the final peice to this puzzle -
ASyncLoader.h - NEW FILE
// AirEngine 2020 - Tom Shinton
#pragma once
#include "Runtime/Threading/Public/LoadRequest.h"
#include "Runtime/Threading/Public/ThreadingInterface.h"
#include <Runtime/Engine/Classes/Engine/StreamableManager.h>
DEFINE_LOG_CATEGORY_STATIC(ASyncLoaderLog, Log, Log)
namespace AsyncLoader
{
//Chuck this into a namespace - making FName isnt cheap
const FName ThreadingModuleName = "Threading";
}
//SharedFromThis allows the garbage collector to make more informed decisions when dealing with the collection of any stored load requests
class FASyncLoader : public TSharedFromThis<FASyncLoader>
{
public:
FASyncLoader()
: Requests()
, Manager()
{
};
//Statically accessible version of the loader, prevents the user having to grab all of this fluff themselves
template<class T>
static void Load(const FSoftObjectPath& InPath, const TFunction<void(T&)>& InCallback)
{
if(!InPath.IsNull())
{
if(InCallback != nullptr)
{
//Grab a reference to the statically initialised version of this class in the Threading module
IThreadingInterface& ThreadingInterface = FModuleManager::LoadModuleChecked<IThreadingInterface>(AsyncLoader::ThreadingModuleName);
if(TSharedPtr<FASyncLoader> Loader = ThreadingInterface.GetStaticLoader())
{
Loader->LoadInternal<T>(InPath, InCallback);
}
}
else
{
UE_LOG(ASyncLoaderLog, Error, TEXT("Cannot request Async load - No valid callback provided"));
}
}
else
{
UE_LOG(ASyncLoaderLog, Error, TEXT("Cannot request ASync load - FilePath is null"));
}
}
void Reset()
{
Requests.Reset();
}
private:
template<class T>
void LoadInternal(const FSoftObjectPath& InPath, const TFunction<void(T&)>& InCallback)
{
if(FBaseLoad* NewLoadRequest = new FLoadRequest<T>(Manager.RequestAsyncLoad(InPath, FStreamableDelegate::CreateSP(this, &FASyncLoader::OnLoaded)), InCallback))
{
Requests.Add(NewLoadRequest);
}
}
void OnLoaded()
{
//SOMETHING has loaded - lets find out what it is
for(int32 Request = Requests.Num() - 1; Request >= 0; --Request)
{
if(Requests[Request]->TryResolve())
{
//Loop backwards - this array may change scope mid-loop
Requests.RemoveAt(Request);
}
}
}
//Polymorphed load requests
TArray<FBaseLoad*> Requests;
//Single instantiation of FStreamableManager
FStreamableManager Manager;
};
We can see here the supremely unhelp way in which the StreamableManager wants to signal a load - with a single callback with no data. By binding to the OnLoad, we can loop over all the incoming requests and ask each one - "has your request been fulfilled?".
If yes, we can "fulfil" it ourselves by grabbing the loaded asset and triggering its callback with an appropriate cast.
That's about it! Lets break down that usecase -
FASyncLoader::Load<UStaticMesh>(MeshPath, [this](UStaticMesh& InLoadedMesh)
{
UE_LOG(LogTemp, Log, TEXT("Loaded %s"), *InLoadedMesh.GetName());
});
We call that nice Static Load function inside the FAsyncLoader class. That in turn grabs the module that's acting as our singleton, and queues up a request. As soon as that request is fulfulled, we come back into the body of the lambda and log out "Some asset has been loaded". Very simple, very easy to use and a lot of the dirty innards are snaffled away into the engine.
There's a great deal of scope for expansion with this API, and would actually make a great candidate for plugin-ification. Array support should probably be added at some point (ie Load me these 10 textures, yadda yadda).