220 lines
8.4 KiB
C++
220 lines
8.4 KiB
C++
#pragma once
|
|
|
|
/// @defgroup TaskManager Task Manager
|
|
/// @brief Manager that runs and resumes a collection of tasks.
|
|
/// @{
|
|
///
|
|
/// A TaskManager is a simple manager class that holds an ordered list of tasks and resumes them whenever it is updated.
|
|
///
|
|
/// Running Tasks
|
|
/// -------------
|
|
/// There are two primary ways to run tasks on a task manager.
|
|
///
|
|
/// The first method (running an "unmanaged task") is to pass a task into @ref TaskManager::Run(). This will move the task
|
|
/// into the task manager and return a @ref TaskHandle that can be used to observe and manage the lifetime of the task (as well
|
|
/// as potentially take a return value after the task finishes). With unmanaged tasks, the task manager only holds a weak
|
|
/// reference to the task, meaning that the @ref TaskHandle returned by @ref TaskManager::Run() is the only remaining strong
|
|
/// reference to the task. Because of this, the caller is entirely responsible for managing the lifetime of the task.
|
|
///
|
|
/// The second method (running a "managed task") is to pass a task into @ref TaskManager::RunManaged(). Like
|
|
/// @ref TaskManager::Run(), this will move the task into the task manager and return a @ref WeakTaskHandle that can be used to
|
|
/// observe the lifetime of the task (as well as manually kill it, if desired). Unlike unmanaged tasks, the task manager
|
|
/// stores a strong reference to the task. Because of this, that caller is not responsible for managing the lifetime of
|
|
/// the task. This difference in task ownership means that (unlike an unmanaged task) a managed task can be thought of as
|
|
/// a "fire-and-forget" task that will run until either it finishes or until something else explicitly kills it.
|
|
///
|
|
/// Order of Execution
|
|
/// ------------------
|
|
/// The ordering of task updates within a call to @ref TaskManager::Update() is stable, meaning that the first task that
|
|
/// is run on a task manager will remain the first to resume, no matter how many other tasks are run on the task manager
|
|
/// (or terminate) in the meantime.
|
|
///
|
|
/// Integration into Actor Classes
|
|
/// ------------------------------
|
|
/// Consider the following example of a TaskManager that has been integrated into a TaskActor base class:
|
|
///
|
|
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp}
|
|
///
|
|
/// class TaskActor : 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(ManageActor()); // Run main actor task as a fire-and-forget "managed task"
|
|
/// }
|
|
///
|
|
/// 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:
|
|
/// virtual Task<> ManageActor() // Overridden (in its entirety) by child classes
|
|
/// {
|
|
/// co_await WaitForever(); // Waits forever (doing nothing)
|
|
/// }
|
|
///
|
|
/// TaskManager m_taskMgr;
|
|
/// };
|
|
///
|
|
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
///
|
|
/// In the above example, TaskManager is instantiated once per high-level actor. It is updated once per frame within
|
|
/// the Tick() method, and all its tasks are killed when it leaves the scene in OnDestroy(). Lastly, a single entry-point
|
|
/// coroutine is run as a managed task when the actor enters the scene. (The above is the conventional method of integration
|
|
/// into this style of game engine.)
|
|
///
|
|
/// Note that it is sometimes necessary to have multiple TaskManagers within a single actor. For example, if there are
|
|
/// multiple tick functions (such as one for pre-physics updates and one for post-physics updates), then instantiating
|
|
/// a second "post-physics" task manager may be desirable.
|
|
|
|
#include <vector>
|
|
|
|
#include "Task.h"
|
|
|
|
NAMESPACE_SQUID_BEGIN
|
|
|
|
//--- TaskManager ---//
|
|
/// Manager that runs and resumes a collection of tasks.
|
|
class TaskManager
|
|
{
|
|
public:
|
|
~TaskManager() {} /// Destructor (disables copy/move construction + assignment)
|
|
|
|
/// @brief Run an unmanaged task
|
|
/// @details Run() return a @ref TaskHandle<> that holds a strong reference to the task. If there are ever no
|
|
/// strong references remaining to an unmanaged task, it will immediately be killed and removed from the manager.
|
|
template <typename tRet>
|
|
SQUID_NODISCARD TaskHandle<tRet> Run(Task<tRet>&& in_task)
|
|
{
|
|
// Run unmanaged task
|
|
TaskHandle<tRet> taskHandle = in_task;
|
|
WeakTask weakTask = std::move(in_task);
|
|
RunWeakTask(std::move(weakTask));
|
|
return taskHandle;
|
|
}
|
|
template <typename tRet>
|
|
SQUID_NODISCARD TaskHandle<tRet> Run(const Task<tRet>& in_task) /// @private Illegal copy implementation
|
|
{
|
|
static_assert(static_false<tRet>::value, "Cannot run an unmanaged task by copy (try Run(std::move(task)))");
|
|
return {};
|
|
}
|
|
|
|
/// @brief Run a managed task
|
|
/// @details RunManaged() return a @ref WeakTaskHandle, meaning it can be used to run a "fire-and-forget" background
|
|
/// task in situations where it is not necessary to observe or control task lifetime.
|
|
template <typename tRet>
|
|
WeakTaskHandle RunManaged(Task<tRet>&& in_task)
|
|
{
|
|
// Run managed task
|
|
WeakTaskHandle weakTaskHandle = in_task;
|
|
m_strongRefs.push_back(Run(std::move(in_task)));
|
|
return weakTaskHandle;
|
|
}
|
|
template <typename tRet>
|
|
WeakTaskHandle RunManaged(const Task<tRet>& in_task) /// @private Illegal copy implementation
|
|
{
|
|
static_assert(static_false<tRet>::value, "Cannot run a managed task by copy (try RunManaged(std::move(task)))");
|
|
return {};
|
|
}
|
|
|
|
/// @brief Run a weak task
|
|
/// @details RunWeakTask() runs a WeakTask. The caller is assumed to have already created a strong TaskHandle<> that
|
|
/// references the WeakTask, thus keeping it from being killed. When the last strong reference to the WeakTask is
|
|
/// destroyed, the task will immediately be killed and removed from the manager.
|
|
void RunWeakTask(WeakTask&& in_task)
|
|
{
|
|
// Run unmanaged task
|
|
m_tasks.push_back(std::move(in_task));
|
|
}
|
|
|
|
/// Call Task::Kill() on all tasks (managed + unmanaged)
|
|
void KillAllTasks()
|
|
{
|
|
m_tasks.clear(); // Destroying all the weak tasks implicitly destroys all internal tasks
|
|
|
|
// No need to call Kill() on each TaskHandle in m_strongRefs
|
|
m_strongRefs.clear(); // Handles in the strong refs array only ever point to tasks in the now-cleared m_tasks array
|
|
}
|
|
|
|
/// @brief Issue a stop request using @ref Task::RequestStop() on all active tasks (managed and unmanaged)
|
|
/// @details Returns a new awaiter task that will wait until all those tasks have all terminated.
|
|
Task<> StopAllTasks()
|
|
{
|
|
// Request stop on all tasks
|
|
std::vector<WeakTaskHandle> weakHandles;
|
|
for(auto& task : m_tasks)
|
|
{
|
|
task.RequestStop();
|
|
weakHandles.push_back(task);
|
|
}
|
|
|
|
// Return a fence task that waits until all stopped tasks are complete
|
|
return [](std::vector<WeakTaskHandle> in_weakHandles) -> Task<> {
|
|
TASK_NAME("StopAllTasks() Fence Task");
|
|
for(const auto& weakHandle : in_weakHandles)
|
|
{
|
|
co_await weakHandle; // Wait until task is complete
|
|
}
|
|
}(std::move(weakHandles));
|
|
}
|
|
|
|
/// Call @ref Task::Resume() on all active tasks exactly once (managed + unmanaged)
|
|
void Update()
|
|
{
|
|
// Resume all tasks
|
|
size_t writeIdx = 0;
|
|
for(size_t readIdx = 0; readIdx < m_tasks.size(); ++readIdx)
|
|
{
|
|
if(m_tasks[readIdx].Resume() != eTaskStatus::Done)
|
|
{
|
|
if(writeIdx != readIdx)
|
|
{
|
|
m_tasks[writeIdx] = std::move(m_tasks[readIdx]);
|
|
}
|
|
++writeIdx;
|
|
}
|
|
}
|
|
m_tasks.resize(writeIdx);
|
|
|
|
// Prune strong tasks that are done
|
|
auto removeIt = m_strongRefs.erase(std::remove_if(m_strongRefs.begin(), m_strongRefs.end(), [](const auto& in_taskHandle) {
|
|
return in_taskHandle.IsDone();
|
|
}), m_strongRefs.end());
|
|
}
|
|
|
|
/// Get a debug string containing a list of all active tasks
|
|
std::string GetDebugString(std::optional<TaskDebugStackFormatter> in_formatter = {}) const
|
|
{
|
|
std::string debugStr;
|
|
for(const auto& task : m_tasks)
|
|
{
|
|
if(!task.IsDone())
|
|
{
|
|
if(debugStr.size())
|
|
{
|
|
debugStr += '\n';
|
|
}
|
|
debugStr += task.GetDebugStack(in_formatter);
|
|
}
|
|
}
|
|
return debugStr;
|
|
}
|
|
|
|
private:
|
|
std::vector<WeakTask> m_tasks;
|
|
std::vector<TaskHandle<>> m_strongRefs;
|
|
};
|
|
|
|
NAMESPACE_SQUID_END
|
|
|
|
///@} end of TaskManager group
|