A Sensible ASync Loading Wrapper
Loading in Unreal typically takes place on the main thread - that's the same thread as all the import juicy stuff. If we wait a couple milliseconds to load a massive mesh, that's a couple milliseconds of juicy important stuff that’s not happening. Luckily, Unreal provides us a method of pushing that load onto another thread - the FStreamableManager. The StreamableManager is a struct that requests assets to be loaded and returns a callback when then they're in memory. If they're already in memory, the callback is fired immediately. However, I find the FStreamableManager unwieldy and cumbersome to use regularly. It has to be initialised SOMEWHERE, and all it does is provide a callback when it's done. It doesn’t say what it's done processing, it just lets us know it's done processing something.. What I want is a function, where I pass in the reference to the thing I want to load, and then some kind of callback that returns me the loaded object of the correct type. So that's what this change does - provides a globally accessible framework to just load your stuff. No frills - its usecase is a single static function call with a lambda for the callback. Easy peasy.
   
    FASyncLoader::Load<UStaticMesh>(MeshPath, [this](UStaticMesh& InLoadedMesh)
    {
    	UE_LOG(LogTemp, Log, TEXT("Loaded %s"), *InLoadedMesh.GetName());
    });
Why do I want an async loader? Im getting along just fine. You probably arent. Say you have a Player that has a texture referenced as a UProperty in it's class defaults. Every time you load that Player, YOU'RE ALSO LOADING THAT TEXTURE. If you're struggling with abhorent editor performance, strongly referenced assets is probably the cause. There's no reasonable reason for a 4k texture to load when you open up a blueprint. This one's a bit hairy, so i've broken up it's implementation into sections.
New Module
First of all, we'll want a module to hold this framework. As we're spinning up another Thread, lets call it the Threading module. This module is where we keep our persistant FStreamableManager - no more initialising one in every bloody class, just use the same one all over. To a new Threading folder inside Engine/Runtime, add the following -
ThreadingModule.h - NEW FILE
   
// AirEngine 2020 - Tom Shinton
#include "Runtime/Threading/Public/ThreadingInterface.h"
#include "Runtime/Threading/Public/ASyncLoader.h"

DEFINE_LOG_CATEGORY_STATIC(ThreadingModuleLog, Log, Log)

class FThreadingModule : public IThreadingInterface
{	
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;

    //IThreadingInterface
    THREADING_API virtual TSharedPtr<FASyncLoader> GetStaticLoader() override final;
    //~IThreadingInterface

private:

    TSharedPtr<FASyncLoader> Loader;
};

IMPLEMENT_MODULE(FThreadingModule, Threading);
ThreadingModule.cpp - NEW FILE
       
    // AirEngine 2020 - Tom Shinton
    #include "Runtime/Threading/Public/ThreadingModule.h"
    
    void FThreadingModule::StartupModule()
    {
        UE_LOG(ThreadingModuleLog, Log, TEXT("Spinning up Threading module"));
    
        Loader = MakeShareable<FASyncLoader>(new FASyncLoader);
    }
    
    void FThreadingModule::ShutdownModule()
    {
        UE_LOG(ThreadingModuleLog, Log, TEXT("Shutting down Threading module"));
    }
    
    TSharedPtr<FASyncLoader> FThreadingModule::GetStaticLoader()
    {
        return Loader;
    }   
Threading.build.cs - NEW FILE
   
// AirEngine 2020 - Tom Shinton
using UnrealBuildTool;

public class Threading: ModuleRules
{
    public Threading(ReadOnlyTargetRules Target) : base(Target)
    {
        PrivateDependencyModuleNames.AddRange(new string[] 
        {
            "Core",
            "CoreUObject",
            "Engine"
        });

        PrivateIncludePaths.AddRange(new string[] 
        {
            "Runtime/Threading/Private"
        });
        
        PublicIncludePaths.AddRange(new string[]
        {
            "Runtime/Threading/Public"
        });
    }
}
You can see this framework is fairly lightweight, and fairly standard boilerplate for a new module - it only leans on the holy trinity of base modules; Core, CoreUObject and Engine. Now we've got a module in place, we'll need a custom interface to access it. Any of the static accessors unreal provides will want to return this as an IModuleInterface&. Not normally an issue, but want access to that loader, so lets make one that gives us access -
ThreadingInterface.h - NEW FILE
   
// AirEngine 2020 - Tom Shinton
#pragma once

class FASyncLoader;

class IThreadingInterface : public IModuleInterface
{
public:
    
    virtual TSharedPtr<FASyncLoader> GetStaticLoader() = 0;
};     
We need to signal to the engine somewhere that we've got a new module that needs building. To get it roped into the compile, simply make the following change
UnrealEd.build.cs - change
   
PrivateDependencyModuleNames.AddRange(new string[]
{
    /... Existing modules .../

    //start @AirEngine Add custom engine modules
    , "Threading"
    //end @AirEngine
}
Thats it! We've now got a single FStreamableManager that we can more or less access from anywhere, assuming the module we're currently working in imports the Threading API by simply adding it to the appropriate build.cs.
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).