Sure!
Note: I’ve changed the names of many of my classes to conceal the purpose of this code, and I may have accidently chosen some that are already in use. They’re generic and stupid, so please pick better ones for your own code! Also, this example is a little too boilerplate - it’s how I did it, not how you /could/ do it in a general sense, so don’t feel the need to copy this approach directly (wrapper structs etc).
I have a struct that wraps my callback (it has some data that the worker uses, for now let’s assume that the in-data is a TArray of FStrings.
struct FAsyncDataRequest
{
FAsyncDataRequest() {}
// callback is set manually, not by constructor (long story)
FAsyncDataRequest(TArray<FString> a_meshIDs)
: m_meshIDs(a_meshIDs) {}
TArray<FString> m_meshIDs;
FOnAsyncDataLoaded m_callback;
};
I pass this in to a worker that inherits from FNonAbandonableTask.
class FAsyncDataWorker : public FNonAbandonableTask
{
public:
FOnAsyncDataLoaded OnLoadFinishedCallback; // This is the delegate called on the main thread when the data has been loaded.
public:
FAsyncDataWorker ();
~FAsyncDataWorker ();
// The owner of this worker pulled the data from the request and passed it in directly
FAsyncDataWorker (TArray<FString> a_meshIDs, FString a_dataFilesDir);
void DoWork();
TStatId GetStatId() const;
private:
// this is the function actually called by the taskgraph system to hop from the worker thread back to the main
// the data result struct just holds the results of this worker's DoWork (in my case, a TArray of structs).
void OnLoadFinished(FAsyncDataResult a_dataLoadResult);
TArray<FString> m_meshIDs;
FString m_dataFilesDir;
};
I also have a quick typedef, since you need to have an FAsyncTask template with your worker class that inherits from FNonAbandonableTask (it doesn’t have to inherit, but it’s easier using this stub I suppose).
typedef FAsyncTask<FAsyncDataWorker > FAsyncDataTask;
This is how the actual work begins:
// You can pass arguments into this constructor that are given to the FAsyncDataWorker's constructor
// since they're using a templated 'pass-through' constructor. Pretend that you're constructing the worker
// directly
FAsyncDataTask* asyncTask = new FAsyncDataTask(a_asyncDataRequest.m_meshIDs, m_baseDataPath);
// I bind the callback that will be executed on the main thread after load
asyncTask->GetTask().OnLoadFinishedCallback = a_asyncDataRequest.m_callback;
asyncTask->StartBackgroundTask();
This will start the FAsyncDataWorker::DoWork() function, which essentially does this:
void FAsyncDataWorker:: DoWork()
{
FAsyncDataResult loadResult;
MyDataLoaderClass dataLoader(m_dataFilesDir);
// the data loader returns my TArray of structs by reference
dataLoader.LoadMyData(m_meshIDs, loadResult.TheTArrayOfStructs);
FGraphEventRef ref = FSimpleDelegateGraphTask::CreateAndDispatchWhenReady
(
// works fine, but requires us to specify and create the delegate here, rather than
// elsewhere and passing it in.
FSimpleDelegateGraphTask::FDelegate::CreateRaw(this, &FAsyncDataWorker::OnLoadFinished, loadResult),
TStatId(),
nullptr,
ENamedThreads::GameThread
);
}
The delegate created above will be called back on the main thread, and will have the data load result as an argument. This ‘OnLoadFinished’ is essentially a function internal to our worker. All it does is the following:
void FAsyncDataWorker::OnLoadFinished(FAsyncDataResult a_loadResult)
{
if (!OnLoadFinishedCallback.ExecuteIfBound(a_loadResult))
{
UE_LOG(LogTemp, Warning, TEXT("Failed to execute delegate when finished loading data in FAsyncDataWorker."));
}
}
Now my original delegate is execute on the main thread, bound to a class about which this worker does not know, and I get the data I’ve loaded onto the main thread!
Remember, I had to change the names here due to it otherwise being obvious what I’m working on. Some of these names might be taken, so pick new ones intelligently I may have screwed them up a little, too, so let me know.