C++14 coroutine-based task library for games

SquidTasks

Squid::Tasks is a header-only C++14 coroutine-based task library for games.

Full project and source code available at https://github.com/westquote/SquidTasks/.

Full API documentation can be found at https://westquote.github.io/SquidTasks/.

License

Squid::Tasks is developed by Tim Ambrogi Saxon and Elliott Mahler at Giant Squid Studios, and uses the MIT License.

Overview of Squid::Tasks

Squid::Tasks consists of several top-level headers within the include directory.

  • Task.h - Task-handles and standard awaiters [REQUIRED]
  • TaskManager.h - Manager that runs and resumes a collection of tasks
  • TokenList.h - Data structure for tracking decentralized state across multiple tasks
  • FunctionGuard.h - Scope guard that calls a function as it leaves scope
  • TaskFSM.h - Finite state machine that implements states using task factories

Sample projects can be found under the @c /samples directory.

Integrating Squid::Tasks

The steps for integrating Squid::Tasks into your game depends on how your game is built:

Configuring Squid::Tasks with TasksConfig.h

The Squid::Tasks library can be configured in a variety of important ways. This is done by enabling and disabling preprocessor values within the include/TasksConfig.h file:

  • SQUID_ENABLE_TASK_DEBUG: Enables Task debug callstack tracking and debug names via Task::GetDebugStack() and Task::GetDebugName()
  • SQUID_ENABLE_DOUBLE_PRECISION_TIME: Switches time representation from 32-bit single-precision floats to 64-bit double-precision floats
  • SQUID_ENABLE_NAMESPACE: Enables a Squid:: namespace around all classes in the Squid::Tasks library
  • SQUID_USE_EXCEPTIONS: Enables experimental (largely-untested) exception-handling, and replaces all asserts with runtime_error exceptions
  • SQUID_ENABLE_GLOBAL_TIME: Enables global time support (alleviating the need to specify a time stream for time-sensitive awaiters) [see Appendix A for more details]

An Example First Task

To get started using Squid::Tasks, the first step is to write and execute your first task from within your project. Many modern C++ game engines feature some sort of "actor" class - a game entity that exists within the scene and is updated each frame. Our example code assume this class exists, but the same principles will apply for projects that are written under a different paradigm.

The first step is to identify an actor class that would benefit from coroutine support, such as an enemy actor. Here is an example Enemy class from a hypothetical 2D game:

class Enemy : public Actor
{
public:
	void SetRotation(float in_degrees); // Set the rotation of the enemy
	float GetRotation() const; // Get the rotation of the enemy
	void SetPosition(Vec2f in_pos); // Set the position of the enemy
	Vec2f GetPosition() const; // Get the position of th enemy
	void MoveToward(Vec2f in_pos, float in_speed, float in_dt) const; // Move toward a target position at a given speed
	void FireProjectileAt(Vec2f in_pos); // Fire a simple projectile to a target position
	std::shared_ptr<Player> GetPlayer() const; // Get the location of the player actor
	float GameTime() const; // Get the current game time (in seconds)
	float DeltaTime() const; // Get the current frame's delta-time (in seconds)

	virtual void OnInitialize() override // Automatically called when this enemy enters the scene
	{
		Actor::OnInitialize(); // Call the base Actor function
	}
	virtual void Tick(float in_dt) override // Automatically called every frame
	{
		Actor::Tick(in_dt); // Call the base Actor function
	}
	virtual void OnDestroy() override // Automatically called when this enemy leaves the scene
	{
		Actor::OnDestroy(); // Call the base Actor function
	}
};

We want to try writing a simple enemy AI using Squid::Tasks. Conventionally, the Tick() function would be responsible for performing all AI logic calculations, so we will use that as the entry-point into our first task coroutine. First, we will create a TaskManager as a private member m_taskMgr. Then, we call m_taskMgr.Update() from within Tick(). Lastly, we need to make sure all of tasks stop running as soon as the enemy leaves the scene, so we call m_taskMgr.KillAllTasks() from within OnDestroy().

class Enemy : public Actor
{
public:
	// ...

	virtual void Tick(float in_dt) override // Automatically called every frame
	{
		Actor::Tick(in_dt); // Call the base Actor function
		m_taskMgr.Update(); // Resume all active tasks once per tick
	}
	virtual void OnDestroy() override // Automatically called when this enemy leaves the scene
	{
		m_taskMgr.KillAllTasks(); // Kill all active tasks when we leave the scene
		Actor::OnDestroy(); // Call the base Actor function
	}

protected:
	TaskManage m_taskMgr;
};

Now that we have the task manager hooked up, we can write and run our first task. Let's make our first task very simple, and just have it print out a string and then terminate. To create a task, we simply write a member function with returns type Task<>, and make sure to use at least one co_await or co_return keyword within the function body. This tells the compiler to compile the function as a coroutine with Task<> as the handle type for the coroutine.

class Enemy : public Actor
{
public:
	// ...
	
	virtual void OnInitialize() override // Automatically called when this enemy enters the scene
	{
		Actor::OnInitialize(); // Call the base Actor function
		m_taskMgr.RunManaged(ManageEnemyAI()); // Run our task as a fire-and-forget "managed task"
	}

	// ...

	Task<> ManageEnemyAI()
	{
		TASK_NAME(__FUNCTION__); // Gives the task a name for debugging purposes

		printf("Hello, enemy AI!\n");
		co_return; // Return from this task
	}
};

With these changes, any enemy instance that enters the scene will print "Hello, enemy AI!". Note that we actually run the task from within OnInitialize(). This line is what actually instantiates the task and tells the task manager to update it every frame. Now that we have the complete scaffolding in, we can try to write an actual enemy behavior. Let's try writing a simple chase AI that chases the player if they get too close to the enemy.

class Enemy : public Actor
{
public:
	// ...
	
	Task<> ManageEnemyAI()
	{
		TASK_NAME(__FUNCTION__); // Gives the task a name for debugging purposes

		while(true) // This "infinite loop" means this task should run for the enemy's lifetime
		{
			// Wait until player gets within a 100-pixel radius
			co_await WaitUntil([&] {
				return Distance(GetPlayer()->GetPosition(), GetPosition()) < 100.0f;
			});

			// Move toward the player as long as they are within a 100-pixel radius
			while(Distance(GetPlayer()->GetPosition(), GetPosition()) < 100.0f)
			{
				MoveToward(GetPlayer()->GetPosition(), 100.0f, DeltaTime());
				co_await Suspend();
			}

			// Cool-down for 2 seconds before following again
			co_await WaitSeconds(2.0f, GameTime());
		}
	}
};

Our chase enemy AI is complete! One advantage of coroutines is that they tend to be fairly straightforward to read, so hopefully you can guess at what some of the above logic means. Regardless, let's break down how this works. The first thing we do is create a while(true) loop around our logic. This is a common coroutine pattern, but it can be confusing the first time you see it. In a normal function, an infinite loop would result in the thread soft-locking. However, in coroutines this pattern essentially means "this coroutine will run for the lifetime of the object running it", which is the desired behavior for our enemy AI task.

The next thing we see is the new co_await keyword. The co_await <awaiter> expression, when evaluated, will suspend the current task until the awaiter is ready to be resumed again. In this example we use 3 of the most versatile and powerful awaiters in Squid::Tasks:

  • Suspend() -> Waits until the next time the task is resumed (usually a single frame)
  • WaitSeconds() -> Waits until N seconds have passed in a given time-stream
  • WaitUntil() -> Waits until a given function returns true

With these 3 awaiters, it is possible to implement enormously complex state machines with relatively straightforward code. (To learn about the other awaiters that come with Squid::Tasks, refer to the \ref Awaiters documentation.)

Next Steps

Hopefully, this brief tutorial has given you an outline of the steps required to integrate coroutines into your own projects. From here, we recommend exploring the "GeneriQuest" sample project under samples/Sample_TextGame. It demonstrates both simple and complex applications of coroutines in a simple text-based game example.

This is the end of the tutorial documentation (for now)! If you made it this far, feel free to write to [tim at giantsquidstudios.com] to let us know any ways in which our documentation could have been more useful for you in learning to use Squid::Tasks!

Appendices

APPENDIX A: Enabling Global Time Support

Every game project has its own method of updating and measuring game time. Most games feature multiple different "time-streams", such as "game time", "real time", "editor time", "paused time", "audio time", etc... Because of this, the Squid::Tasks library requires each time-sensitive awaiter (e.g. WaitSeconds(), Timeout(), etc) to be presented with a time-stream function that returns the current time in the desired time-stream. By convention, these time-streams are passed as functions into the final argument of time-sensitive awaiters.

A final (optional) step of integrating Squid::Tasks is to enable global time support and implement a global Squid::GetTime() function.

For less-complex projects it can be desirable to default to a "global time-stream" that removes the requirement to explicitly pass a time-stream function into time-sensitive awaiters. To enable this functionality, the user must set SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h and implement a special function called Squid::GetTime(). Failure to define this function will result in a linker error.

The Squid::GetTime() function should return a floating-point value representing the number of seconds since the program started running. Here is an example Squid::GetTime() function implementation from within the main.cpp file of a sample project:

NAMESPACE_SQUID_BEGIN
tTaskTime GetTime()
{
	return (tTaskTime)TimeSystem::GetTime();
}
NAMESPACE_SQUID_END

It is recommended to save off the current time value at the start of each game frame, returning that saved value from within Squid::GetTime(). The reason for this is that, within a single frame, you likely want all of the tasks to behave as if they are updating at the same time. By providing the same exact time value to all Tasks that are resumed within a given update, the software is more likely to behave in a stable and predictable manner.

Owner
Tim Ambrogi Saxon
Codemaker. Creator of games. Design Director + Programmer @ Giant Squid Studios.
Tim Ambrogi Saxon
Comments
  • Does it support exceptions for UnrealEngine?

    Does it support exceptions for UnrealEngine?

    Hi Tim! Is it supports throwing exceptions in resumable functions in UnrealEngine? I tried to use coroutines in my UE project but got UB (access violation/froze) using coroutines and exceptions together.

  • Initial cmake build support.

    Initial cmake build support.

    Just a quick CMake project, I'm using it included as a header only sub-project.

    It doesn't include the private headers currently, but that's mostly for project support and not sure if they should or should not be included.

  • Crash after module using SquidTasks is reloaded with Live Coding

    Crash after module using SquidTasks is reloaded with Live Coding

    Hi! I get the following crash after trying to execute a SquidTasks coroutine following a Live Coding recompile of the module containing the coroutine in a UE5 project. Apparently the m_taskInternal shared ptr inside the promise is null.

    image

  • PS4/PS5 Documentation

    PS4/PS5 Documentation

    We're testing the integration here for PS4/PS5 with UE4 and are hitting some compiler errors. Can you clarify the documentation for consoles or add some example integration documentation?

    TasksCommonPrivate.h(178,10): fatal error: 'experimental/coroutine' file not found #include <experimental/coroutine>

    TasksCommonPrivate.h(59,28): error : expected class name struct static_false : std::false_type

    Your GDC Talk was great!

  • After following the UE5 steps to enable, we need to compile everything whenever we generate project

    After following the UE5 steps to enable, we need to compile everything whenever we generate project

    After right-clicking on the .uproject file and selecting "Generate Visual Studio project files" we need to recompile everything. This makes it very slow to work with since you might generate projects multiple times per day.

    1. Compile DebugGameEditor|Win64 and launch (to ensure that everything is compiled)
    2. Close Visual Studio
    3. Open VS again and launch. Notice that everything is compiled and it launches instantly
    4. Close Visual Studio
    5. Right-click .uproject file and select "Generate Visual Studio project files"
    6. Open visual studio and launch. Notice that it will now compile everything

    I noticed that in step 5 all the "Definition.*.h" files would be generated with "#define WITH_CPP_COROUTINES 0", but after we compiled through Visual Studio it would be set to "#define WITH_CPP_COROUTINES 1" (since we want Coroutines). Our fix for this was to add "bEnableCppCoroutinesForEvaluation = true;" into UnrealEditor.Target.cs and now they generate with 1 instead of 0. It would be good to update the setup guide with this information and if you know a better way of doing it to include that instead.

  • error C3861: '_WaitUntil': identifier not found when SQUID_ENABLE_NAMESPACE=1

    error C3861: '_WaitUntil': identifier not found when SQUID_ENABLE_NAMESPACE=1

    Since WaitUntil and WaitWhile are macros, they are not captured inside namespaces.

    Workaround is to disable namespaces by using: #define SQUID_ENABLE_NAMESPACE 0

  • Factoring out task forward declerations.

    Factoring out task forward declerations.

    Factor out task forward declarations to pull in fewer deps in headers that just need to declare a Task<> function.

    Includes https://github.com/westquote/SquidTasks/pull/5 due to the CMakeLists include, could be trivially rebased out.

  • Initial CMake build support

    Initial CMake build support

    Just a quick CMake project, I'm using it included as a header only sub-project.

    It doesn't include the private headers currently, but that's mostly for project support and not sure if they should or should not be included.

    Updated PR from separate branch.

Coroutine - C++11 single .h asymmetric coroutine implementation via ucontext / fiber

C++11 single .h asymmetric coroutine implementation API in namespace coroutine: routine_t create(std::function<void()> f); void destroy(routine_t id);

Dec 4, 2022
A C++20 coroutine library based off asyncio
A C++20 coroutine library based off asyncio

kuro A C++20 coroutine library, somewhat modelled on Python's asyncio Requirements Kuro requires a C++20 compliant compiler and a Linux OS. Tested on

Nov 9, 2022
C++20 Coroutine-Based Synchronous Parser Combinator Library

This library contains a monadic parser type and associated combinators that can be composed to create parsers using C++20 Coroutines.

Oct 13, 2022
Elle - The Elle coroutine-based asynchronous C++ development framework.
Elle - The Elle coroutine-based asynchronous C++ development framework.

Elle, the coroutine-based asynchronous C++ development framework Elle is a collection of libraries, written in modern C++ (C++14). It contains a rich

Nov 19, 2022
Arcana.cpp - Arcana.cpp is a collection of helpers and utility code for low overhead, cross platform C++ implementation of task-based asynchrony.

Arcana.cpp Arcana is a collection of general purpose C++ utilities with no code that is specific to a particular project or specialized technology are

Nov 23, 2022
:copyright: Concurrent Programming Library (Coroutine) for C11

libconcurrent tiny asymmetric-coroutine library. Description asymmetric-coroutine bidirectional communication by yield_value/resume_value native conte

Sep 2, 2022
Single header asymmetric stackful cross-platform coroutine library in pure C.
Single header asymmetric stackful cross-platform coroutine library in pure C.

minicoro Minicoro is single-file library for using asymmetric coroutines in C. The API is inspired by Lua coroutines but with C use in mind. The proje

Nov 30, 2022
A golang-style C++ coroutine library and more.

CO is an elegant and efficient C++ base library that supports Linux, Windows and Mac platforms. It pursues minimalism and efficiency, and does not rely on third-party library such as boost.

Dec 4, 2022
Cppcoro - A library of C++ coroutine abstractions for the coroutines TS

CppCoro - A coroutine library for C++ The 'cppcoro' library provides a large set of general-purpose primitives for making use of the coroutines TS pro

Nov 29, 2022
A go-style coroutine library in C++11 and more.
A go-style coroutine library in C++11 and more.

cocoyaxi English | 简体中文 A go-style coroutine library in C++11 and more. 0. Introduction cocoyaxi (co for short), is an elegant and efficient cross-pla

Dec 2, 2022
A header-only C++ library for task concurrency
A header-only C++ library for task concurrency

transwarp Doxygen documentation transwarp is a header-only C++ library for task concurrency. It allows you to easily create a graph of tasks where eve

Nov 23, 2022
OOX: Out-of-Order Executor library. Yet another approach to efficient and scalable tasking API and task scheduling.

OOX Out-of-Order Executor library. Yet another approach to efficient and scalable tasking API and task scheduling. Try it Requirements: Install cmake,

Oct 25, 2022
Cpp-taskflow - Modern C++ Parallel Task Programming Library
Cpp-taskflow - Modern C++ Parallel Task Programming Library

Cpp-Taskflow A fast C++ header-only library to help you quickly write parallel programs with complex task dependencies Why Cpp-Taskflow? Cpp-Taskflow

Mar 30, 2021
Powerful multi-threaded coroutine dispatcher and parallel execution engine

Quantum Library : A scalable C++ coroutine framework Quantum is a full-featured and powerful C++ framework build on top of the Boost coroutine library

Nov 28, 2022
Async GRPC with C++20 coroutine support

agrpc Build an elegant GRPC async interface with C++20 coroutine and libunifex (target for C++23 executor). Get started mkdir build && cd build conan

Nov 15, 2022
Mx - C++ coroutine await, yield, channels, i/o events (single header + link to boost)

mx C++11 coroutine await, yield, channels, i/o events (single header + link to boost). This was originally part of my c++ util library kit, but I'm se

Sep 21, 2019
A General-purpose Parallel and Heterogeneous Task Programming System
A General-purpose Parallel and Heterogeneous Task Programming System

Taskflow Taskflow helps you quickly write parallel and heterogeneous tasks programs in modern C++ Why Taskflow? Taskflow is faster, more expressive, a

Dec 8, 2022
A task scheduling framework designed for the needs of game developers.

Intel Games Task Scheduler (GTS) To the documentation. Introduction GTS is a C++ task scheduling framework for multi-processor platforms. It is design

Nov 26, 2022
A hybrid thread / fiber task scheduler written in C++ 11

Marl Marl is a hybrid thread / fiber task scheduler written in C++ 11. About Marl is a C++ 11 library that provides a fluent interface for running tas

Nov 29, 2022