This plugin offers an implementation of C++ coroutines that doesn't depend on co_await and co_return, which are not available or not fully supported on some compilers still used in game development.
A notable feature of this plugin compared to the alternatives is the inclusion of a visual debugger to assist development and troubleshooting.
It's verified to compile and work on:
- UE Versions:
- 4.27 (only up to version 1.1.0 of the plugin, since some C++ 20 features are now used in some parts of the code)
- 5.1 - 5.7
- Platforms:
- Win64
- PS5
- Xbox Series
It is also quite simple to extend with new building blocks.
The nomenclature and feature set of this plugin was inspired by SkookumScript and the use we gave it during development of The Eternal Cylinder. In that game we used SkookumScript to create the AI of the very diverse creatures that populated its ecosystem as a collection of modular "behavior tree"-like code.
Since development of SkookumScript was stopped after Agog Labs was acquired by Epic to develop the Verse language, that workflow became very difficult to maintain. So we were forced to find a way to preserve the benefits we saw from that workflow without depending on another language.
This system is currently being used to program the AI and other heavily asynchronous systems in The Mound: Omen of Cthulhu.
As with any other C++ module, you need to add the "ACETeam_Coroutines" module to the list of dependencies of your module's .Build.cs file
It's recommended that you include the following headers:
- CoroutineElements.h to have access to the building blocks for your coroutines.
- CoroutinesWorldSubsystem.h to run your coroutine in the context of a specific world. Coroutines executed using this subsystem will only update if that world is not paused.
For simplicity you can add a "using namespace" declaration so you don't have to preface coroutine elements with the ACETeam_Coroutines namespace.
Many of the features of the system are explored in the following code sample:
...
#include "CoroutineElements.h"
#include "CoroutinesWorldSubsystem.h"
using namespace ACETeam_Coroutines;
...
FCoroutineNodeRef _CoroutineTest(UWorld* World, FString TextToLog)
{
//Coroutine variables are values that can be used to share data between different execution branches,
// or different steps in your coroutine
//They're guaranteed to share the same lifetime as the code that's using them.
//They function the same as a shared pointer. They must be dereferenced to read or modify their value
auto SharedValue = CoroVar<int>(0);
//_Seq concatenates coroutine elements in a sequence. It runs one after the other until
// one of them fails
//If one of them finishes during a frame, the next in the sequence will be evaluated
// in the same frame
return _Seq(
//_Race starts an execution branch for each of its contained elements, in the declared order.
// Aborts any running branches when one finishes its execution (successfully or unsuccessfully).
_Race(
//a _Loop will run its contained element once per frame, until it fails or is aborted
_Loop(
//_Weak creates an element that will only evaluate its lambda if the passed in
// object is valid on evaluation
_Weak(World, [=] { DrawDebugPoint(World, FVector::ZeroVector, 10, FColor::White); })
),
//_LoopSeq is a shortcut for a _Loop containing a _Seq
_LoopSeq(
//_Wait will pause this execution branch for the specified time
_Wait(1.0f),
[=] {
UE_LOG(LogTemp, Log, TEXT("This text will print once per second"));
UE_LOG(LogTemp, Log, TEXT("This parameter was captured by value %s"), *TextToLog);
},
//mutable lambdas can be used to have local variables that share their lifetime
[Counter = 0] () mutable {
//When this returns false, the containing loop will finish
return ++Counter <= 10;
},
[=] {
//If one execution branch wants to pass data to a different
//branch, you can use shared variables
*SharedValue += FMath::RandHelper(10);
}
)
),
[=] {
UE_LOG(LogTemp, Log, TEXT("This log will appear after the race above finishes"));
UE_LOG(LogTemp, Log, TEXT("The final value stored in SharedValue is %d"), *SharedValue);
}
);
}
void UTestObject::RunTestCoroutine()
{
UCoroutinesWorldSubsystem::Get(this).StartCoroutine( _CoroutineTest(GetWorld(), "<test string>") );
}The prefixed _ before each Coroutine is a convention inspired by SkookumScript, as are the names of some of the primitives such as _Race and _Sync.
_Seq: Concatenates coroutine elements in a sequence. It runs one after the other until one of them fails. If one fails, it propagates failure._SeqNoFail|_OptionalSeq: Same as_Seqbut doesn't propagate failure._Select: Analogous to a selector in a behavior tree. Evaluates each contained element in order, until one succeeds. If all fail, propagates failure._Race: Spawns an execution branch for each of its contained elements, in the declared order. Aborts any running branches when one finishes its execution (successfully or unsuccessfully)._Sync: Spawns an execution branch for each of its contained elements, in the declared order. Waits for all of them to finish their execution. If one of them failed during execution, the end result will be a failure._Wait: Pauses execution of its branch for the specified time._WaitFrames: Pauses execution of its branch for the specified number of frames._Loop: Evaluates its contained element once per frame, until it fails._LoopSeq: Shortcut for_Loopcontaining a_Seq_Scope: Executes the lambda contained within its first set of parentheses when the contained execution branch terminates for any reason. This lambda cannot have a return type, but it can optionally receive an EStatus argument, which indicates the reason this branch terminated.
_Scope([]{ UE_LOG(LogTemp, Log, TEXT("Scope exit"); })
(
...
)_ScopeWeak: Same as_Scope, but only evaluates its exit lambda if its owningUObjectis still valid_Not: Negates the return value of its branch, i.e. success is failure, failure is success._Catch: Prevents failure of its branch from propagating._CaptureReturn: Same as_Catchbut also stores branch return value into a shared bool variable._Fork: Spawns an independent execution branch for the contained element. This means if the original branch is aborted, it will not affect this spawned branch._Weak: Used to indicate a lambda should not be evaluated if an associatedUObjectis no longer valid. Returns a failure if the object is stale._NamedScope: No effect on behavior, only used to improve readability when using the Visual Debugger. Uses square brackets as delimiter instead of parentheses so it's easier to visually distinguish which part the scope refers to
_NamedScope(TEXT("Named coroutine block"))
[
...
]_Error: Fails immediately. Useful as a return value for lambdas that return coroutines._Nop: Same as_Errorbut succeeds immediately._WaitForever: Remains in a suspended state forever. Can be useful in a_Racecontext for dummy execution branches._ConvertLambda: Converts a lambda into its corresponding coroutine node. This is the same conversion that's usually done when receiving a lambda argument in one of the above functions.
- A coroutine node (which is also the return type of these blocks)
- A lambda with no return value.
- A lambda with boolean return value. This is especially useful for terminating
_Loopblocks - A lambda that returns a coroutine node. This allows deferring of the creation of a subcoroutine until execution reaches this lambda.
None of the lambdas should receive arguments. You can omit the () except in the case of mutable lambdas, where it's required by the compiler.
IMPORTANT: You should only use capture by copy in these lambdas unless you're absolutely certain the lifetime of an object captured by reference will completely overlap the lifetime of your entire coroutine's execution.
Other headers that expose additional features:
- CoroutinesSubsystem.h for a simple way to run your coroutine. The UCoroutinesSubsystem can execute coroutines in any circumstance. Even in the editor while it's not in play mode.
- CoroutineEvents.h grants access to
MakeEvent<...>and_WaitForwhich will allow you to make events that optionally broadcast values, and have your coroutines wait for them and receive those values. This is useful for communicating between different coroutine branches, or to receive input from other systems. Events with no parameters can even be exposed to Blueprints, with the wrapper in CoroutineEventBPWrapper.h - CoroutineAsync.h grants access to
_Asyncto run code blocks on other threads. - CoroutineTween.h to tween values as part of a coroutine. Supports typical easing functions out of the box, as well as custom easing functions.
- CoroutineSemaphores.h has
MakeSemaphoreand the_Semaphorescope that lets you have coroutines wait to access a resource with a limited amount of concurrent users.
Unreal Insights traces will now break down time spent in the coroutine executor according to the _NamedScope blocks defined for the coroutines you are running. This makes it easier to diagnose cases where coroutine execution might be taking too long.
If you activate the Gameplay Debugger (gdt.Toggle console command), you'll see there is a Coroutines category that displays the currently running coroutines.
If you're using the system heavily you may run out of vertical space to display all of the coroutines currently running. In this case you can either scroll using the Shift+F and Shift+R keys, or you can use the gdt.Coroutine.SetFilter console command to reduce those shown to those whose root name contains one of the filter strings.
By default, the debugger displays in "compact mode", which only shows the root of the coroutine and "leaf nodes", which are those with no children, e.g. lambdas, and waits.
In addition it displays any _NamedScope blocks. These have two functions:
- they can replace the name of the root of a coroutine in the debugger, or
- they can be nested so you can more easily track the lifecycle of a complex coroutine in the debugger.
LIMITATION: a nested
_NamedScopewill not show up in the debugger in compact mode if the coroutine's root is not also a_NamedScope
Compact mode can be toggled with the shortcut displayed on screen (Shift+RightBracket).
The colors of the strips shown in the debugger represent their state.
- 🟩 Green means it completed successfully
- 🟥 Red means it returned a failure (Note that failures can be used to interrupt
_Loop,_Seqand other blocks, so they don't necessarily imply a failure in the global sense) - 🟨 Yellow means it's suspended, i.e. tracked by the system but not actively being evaluated.
- 🟦 Blue means it's being polled continuously every frame
Be sure to check out other plugins that approach this problem differently. There are a couple of cool ones that make use of C++ 20 coroutine support:
Keep in mind that plugins that use C++ 20 coroutines don't run out of the box on UE4 on all platforms without source modifications. Also at least SquidTasks currently has issues with crashing after using a Live Coding recompile. ACE Team Coroutines has neither of these issues.

