UE5Coro
This library implements basic C++20 coroutine support for Unreal Engine.
Getting started
Obviously you'll need to switch your project to C++20. In your Build.cs file, add or change this line:
CppStandard = CppStandardVersion.Cpp20;
If you're using a prebuilt 5.x from the future you might also need these:
PCHUsage = PCHUsageMode.NoSharedPCHs;
PrivatePCHHeaderFile = "YourPCH.h";
Add "UE5Coro"
to your dependency module names, enable the plugin, and you're ready to go!
Examples
Generators
Generators can be used to return an arbitrary number of items from a function without having to pass them through temp arrays, etc. You can even make a function co_yield
infinite elements and have the caller decide how many it wants!
using namespace UE5Coro;
TGenerator<int> CountToThree()
{
for (int i = 1; i <= 3; ++i)
co_yield i;
}
You can run the generator manually with full control over when it resumes:
TGenerator<int> G1 = CountToThree(); // Runs to co_yield i; with i==1
do
{
// This check can be omitted if you're guaranteed at least one
// co_yield from the generator.
if (G1)
int Value = G1.Current();
// Resume() continues the function until the next co_yield or until
// control leaves scope. As a convenience it returns operator bool().
} while (G1.Resume());
You can use generators as UE-style iterators:
TGenerator<int> G2 = CountToThree();
for (auto It = G2.CreateIterator(); It; ++It)
int Value = *It;
They also work with range-based for (or STL algorithms):
TGenerator<int> G3 = CountToThree();
for (int Value : G3)
DoSomethingWith(Value);
Your caller can stop you at any point so feel free to go wild:
TGenerator
Every64BitPrime()
{
// You probably don't want to run this on tick...
for (uint64 i =
2; i <= UINT64_MAX; ++i)
if (
IsPrime(i))
co_yield i;
}
TGenerator
Primes = Every64BitPrime();
uint64 Two = Primes.Current();
Primes.Resume();
uint64 Three = Primes.Current();
Primes.Resume();
uint64 Five = Primes.Current();
// Done! Primes going out of scope will destroy the coroutine.
This is also fine, fetch as many values as you want:
TGenerator
InfiniteString()
{
// Warranty void on integer overflow
for (
int i =
1;
/*forever*/; ++i)
{
UE_LOG(LogTemp, Display,
TEXT(
"%d parking space(s) served.", i);
co_yield
TEXT(
'🅿️');
}
}
// Only 10 needed for the company fleet
TGenerator
AllTheParkingSpaces = InfiniteString();
for (
int i =
0; i <
10; ++i)
{
TCHAR P = AllTheParkingSpaces.
Current();
AllTheParkingSpaces.
Resume();
}
If you're used to Unity coroutines or just regular .NET IEnumerable
, do note that unlike C# iterators these generators run immediately when called, not at the first Resume()
. This behavior fits the semantics of C++ iterators better that expect begin()
to already be on the first element.
Latent actions
FLatentCoroutine::Start
in namespace UE5Coro
can be used to start any function including lambdas as a latent action so you don't need to author entire types per UFUNCTION. Returning UE5Coro::FLatentCoroutine
makes your function coroutine-enabled:
// .h
UFUNCTION(BlueprintCallable, Meta = (Latent, LatentInfo = "LatentInfo"))
static void Foo(int EpicPleaseFixUE22342, FLatentActionInfo LatentInfo);
// .cpp
using namespace UE5Coro;
void UExampleFunctionLibrary::Foo(int, FLatentActionInfo LatentInfo)
{
// Add -> FLatentCoroutine to your lambda to make it coroutine enabled!
FLatentCoroutine::Start(LatentInfo, []() -> FLatentCoroutine
{
// You're now in the next tick.
co_await Latent::Ticks(10);
// An additional 10 ticks later...
co_await Latent::Seconds(1.0f);
// 1 second later, regardless of FPS...
// As you return from this lambda, your BP node will fire its
// latent exec pin.
});
}
See LatentAwaiters.h
for a list of built-in awaiters and how to add your own. Just like regular latent actions, these all run on the game thread.
Async tasks
Similarly to latent actions, returning UE5Coro::FAsyncCoroutine
from a function makes it coroutine-enabled and usable by, e.g. AsyncTask()
. You can use co_await
to easily hop threads:
using namespace UE5Coro;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, []() -> FAsyncCoroutine)
{
// You're now in the thread that was given to AsyncTask.
DoExpensiveThing();
co_await Async::MoveToThread(ENamedThreads::GameThread);
// You're now on the game thread!
co_await Async::MoveToThread(ENamedThreads::AnyHiPriThreadHiPriTask);
// You can move in and out as you wish.
if (bShouldSwitchThreadsAgain)
co_await Async::MoveToThread(ENamedThreads::GameThread);
// Runtime-decided thread!
});
// No need to start with AsyncTask:
FAsyncCoroutine DoBackgroundThing()
{
// This part works just like any regular function call.
// You're in your caller's thread.
co_await Async::MoveToThread(ENamedThreads::AnyBackgroundThreadNormalTask);
// You're now in an AsyncTask()
co_await Async::MoveToThread(ENamedThreads::GameThread);
// You're back on the game thread in another AsyncTask()
}
// Use like this, the return value does not need to be stored:
DoBackgroundThing();
Behind the scenes, each co_await
will run the rest of your coroutine in a new AsyncTask()
on the specified ENamedThreads
. This is important because any local UObject that you might make in a coroutine is technically living in a "vanilla C++" struct without a UPROPERTY and can be eligible for garbage collection while you're on another thread:
FAsyncCoroutine DontDoThisAtHome()
{
UObject* Obj = NewObject
();
co_await
Async::MoveToThread(ENamedThreads::AnyBackgroundThreadNormalTask);
// You're no longer synchronously running on the game thread,
// Obj *IS* eligible for garbage collection!
co_await
Async::MoveToThread(ENamedThreads::GameThread);
// You're back on the game thread, but Obj could be a dangling
// pointer by now. Use TStrongObjectPtr or another method of keeping
// UObjects alive if you find yourself in this scenario.
}