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
 | 
