Created
July 22, 2025 18:42
-
-
Save koster/a02e05202cf23e6f39f739b2ed063746 to your computer and use it in GitHub Desktop.
TaskManager.cs - control your coroutines
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections; | |
| using System.Collections.Generic; | |
| using System.Reflection; | |
| using DG.Tweening; | |
| using UnityEngine; | |
| public class TaskManager : MonoBehaviour | |
| { | |
| public float targetFrameTime = 1f / 60f; // Default to 60fps equivalent | |
| private List<TaskInfo> activeTasks = new List<TaskInfo>(); | |
| private static FieldInfo waitForSecondsField; | |
| private float accumulatedTime = 0f; | |
| // Cache the reflection field for performance | |
| static TaskManager() | |
| { | |
| waitForSecondsField = typeof(WaitForSeconds).GetField("m_Seconds", | |
| BindingFlags.NonPublic | BindingFlags.Instance); | |
| } | |
| internal class TaskInfo | |
| { | |
| public IEnumerator enumerator; | |
| public string name; | |
| public bool isPaused; | |
| public TaskInfo(IEnumerator enumerator, string name = null) | |
| { | |
| this.enumerator = enumerator; | |
| this.name = name ?? "Unnamed Task"; | |
| this.isPaused = false; | |
| } | |
| } | |
| // Set the target frame rate for time accumulation | |
| public void SetTargetFrameRate(float fps) | |
| { | |
| targetFrameTime = 1f / fps; | |
| } | |
| // Start a new task | |
| public TaskHandle StartTask(IEnumerator task, string taskName = null) | |
| { | |
| var taskInfo = new TaskInfo(task, taskName); | |
| activeTasks.Add(taskInfo); | |
| return new TaskHandle(this, taskInfo); | |
| } | |
| // Start a task from a method that returns IEnumerable | |
| public TaskHandle StartTask(Func<IEnumerable> taskMethod, string taskName = null) | |
| { | |
| return StartTask(taskMethod().GetEnumerator(), taskName); | |
| } | |
| // Manually tick all tasks with delta time - call this when you want tasks to update | |
| public void Tick(float deltaTime) | |
| { | |
| accumulatedTime += deltaTime; | |
| // Process multiple frames if we've accumulated enough time | |
| while (accumulatedTime >= targetFrameTime) | |
| { | |
| ProcessTasks(targetFrameTime); | |
| accumulatedTime -= targetFrameTime; | |
| } | |
| } | |
| // Overload for backwards compatibility - uses Time.deltaTime | |
| public void Tick() | |
| { | |
| Tick(Time.deltaTime); | |
| } | |
| // Process all tasks for one frame | |
| private void ProcessTasks(float frameTime) | |
| { | |
| for (int i = activeTasks.Count - 1; i >= 0; i--) | |
| { | |
| var task = activeTasks[i]; | |
| if (task.isPaused) | |
| continue; | |
| try | |
| { | |
| bool hasNext = task.enumerator.MoveNext(); | |
| if (!hasNext) | |
| { | |
| // Task completed | |
| activeTasks.RemoveAt(i); | |
| Debug.Log($"Task '{task.name}' completed"); | |
| } | |
| else | |
| { | |
| // Handle special yield values | |
| var current = task.enumerator.Current; | |
| if (current is WaitForSeconds wait) | |
| { | |
| // Convert WaitForSeconds to a custom wait | |
| activeTasks[i] = new TaskInfo(WaitForSecondsCoroutine(GetWaitForSecondsTime(wait), task.enumerator), task.name) | |
| { | |
| isPaused = task.isPaused | |
| }; | |
| } | |
| else if (current is WaitForTicks waitTicks) | |
| { | |
| // Handle custom WaitForTicks | |
| activeTasks[i] = new TaskInfo(WaitForTicksCoroutine(waitTicks.tickCount, task.enumerator), task.name) | |
| { | |
| isPaused = task.isPaused | |
| }; | |
| } | |
| else if (current is WaitForEndOfFrame) | |
| { | |
| // Skip this frame, continue next tick | |
| continue; | |
| } | |
| else if (current is WaitForFixedUpdate) | |
| { | |
| // Skip to next fixed update - in manual system, treat as single frame skip | |
| continue; | |
| } | |
| else if (current is WaitUntil waitUntil) | |
| { | |
| // Handle WaitUntil | |
| activeTasks[i] = new TaskInfo(WaitUntilCoroutine(waitUntil, task.enumerator), task.name) | |
| { | |
| isPaused = task.isPaused | |
| }; | |
| } | |
| else if (current is WaitWhile waitWhile) | |
| { | |
| // Handle WaitWhile | |
| activeTasks[i] = new TaskInfo(WaitWhileCoroutine(waitWhile, task.enumerator), task.name) | |
| { | |
| isPaused = task.isPaused | |
| }; | |
| } | |
| else if (current is WaitForTween waitTween) | |
| { | |
| // Handle DOTween wait | |
| activeTasks[i] = new TaskInfo(WaitForTweenCoroutine(waitTween, task.enumerator), task.name) | |
| { | |
| isPaused = task.isPaused | |
| }; | |
| } | |
| else if (current is ControlledTween controlledTween) | |
| { | |
| // Handle controlled tween | |
| activeTasks[i] = new TaskInfo(ControlledTweenCoroutine(controlledTween, task.enumerator, frameTime), task.name) | |
| { | |
| isPaused = task.isPaused | |
| }; | |
| } | |
| // Add more yield instruction handling as needed | |
| } | |
| } | |
| catch (Exception e) | |
| { | |
| Debug.LogError($"Error in task '{task.name}': {e.Message}\n{e.StackTrace}"); | |
| activeTasks.RemoveAt(i); | |
| } | |
| } | |
| } | |
| // Extract wait time from WaitForSeconds using reflection | |
| private float GetWaitForSecondsTime(WaitForSeconds waitForSeconds) | |
| { | |
| if (waitForSecondsField != null) | |
| { | |
| try | |
| { | |
| return (float)waitForSecondsField.GetValue(waitForSeconds); | |
| } | |
| catch (Exception e) | |
| { | |
| Debug.LogWarning($"Failed to extract WaitForSeconds time via reflection: {e.Message}"); | |
| } | |
| } | |
| // Fallback to 1 second if reflection fails | |
| return 1f; | |
| } | |
| // Helper coroutine for WaitForSeconds - now uses accumulated time | |
| private IEnumerator WaitForSecondsCoroutine(float seconds, IEnumerator originalTask) | |
| { | |
| float elapsed = 0f; | |
| while (elapsed < seconds) | |
| { | |
| elapsed += targetFrameTime; // Use our frame time instead of Time.deltaTime | |
| yield return null; | |
| } | |
| // Resume original task | |
| while (originalTask.MoveNext()) | |
| { | |
| yield return originalTask.Current; | |
| } | |
| } | |
| // Helper coroutine for WaitForTicks | |
| private IEnumerator WaitForTicksCoroutine(int ticks, IEnumerator originalTask) | |
| { | |
| for (int i = 0; i < ticks; i++) | |
| { | |
| yield return null; | |
| } | |
| // Resume original task | |
| while (originalTask.MoveNext()) | |
| { | |
| yield return originalTask.Current; | |
| } | |
| } | |
| // Helper coroutine for WaitUntil | |
| private IEnumerator WaitUntilCoroutine(WaitUntil waitUntil, IEnumerator originalTask) | |
| { | |
| // Use reflection to get the predicate from WaitUntil | |
| var predicateField = typeof(WaitUntil).GetField("m_Predicate", BindingFlags.NonPublic | BindingFlags.Instance); | |
| if (predicateField != null) | |
| { | |
| var predicate = (Func<bool>)predicateField.GetValue(waitUntil); | |
| while (!predicate()) | |
| { | |
| yield return null; | |
| } | |
| } | |
| else | |
| { | |
| Debug.LogWarning("Could not access WaitUntil predicate via reflection"); | |
| yield return null; // Skip one frame as fallback | |
| } | |
| // Resume original task | |
| while (originalTask.MoveNext()) | |
| { | |
| yield return originalTask.Current; | |
| } | |
| } | |
| // Helper coroutine for WaitWhile | |
| private IEnumerator WaitWhileCoroutine(WaitWhile waitWhile, IEnumerator originalTask) | |
| { | |
| // Use reflection to get the predicate from WaitWhile | |
| var predicateField = typeof(WaitWhile).GetField("m_Predicate", BindingFlags.NonPublic | BindingFlags.Instance); | |
| if (predicateField != null) | |
| { | |
| var predicate = (Func<bool>)predicateField.GetValue(waitWhile); | |
| while (predicate()) | |
| { | |
| yield return null; | |
| } | |
| } | |
| else | |
| { | |
| Debug.LogWarning("Could not access WaitWhile predicate via reflection"); | |
| yield return null; // Skip one frame as fallback | |
| } | |
| // Resume original task | |
| while (originalTask.MoveNext()) | |
| { | |
| yield return originalTask.Current; | |
| } | |
| } | |
| // Helper coroutine for DOTween integration with pause support | |
| private IEnumerator WaitForTweenCoroutine(WaitForTween waitTween, IEnumerator originalTask) | |
| { | |
| var tween = waitTween.tween; | |
| // Wait while the tween is active and not complete | |
| while (tween != null && tween.IsActive() && !tween.IsComplete()) | |
| { | |
| yield return null; | |
| } | |
| // Resume original task | |
| while (originalTask.MoveNext()) | |
| { | |
| yield return originalTask.Current; | |
| } | |
| } | |
| // Helper coroutine for controlled DOTween that respects pausing - now with proper deltaTime | |
| private IEnumerator ControlledTweenCoroutine(ControlledTween controlledTween, IEnumerator originalTask, float frameTime) | |
| { | |
| while (!controlledTween.IsComplete) | |
| { | |
| // Update the tween manually with our accumulated frameTime | |
| controlledTween.ManualUpdate(frameTime); | |
| yield return null; | |
| } | |
| // Resume original task | |
| while (originalTask.MoveNext()) | |
| { | |
| yield return originalTask.Current; | |
| } | |
| } | |
| // Get count of active tasks | |
| public int ActiveTaskCount => activeTasks.Count; | |
| // Get the current accumulated time | |
| public float AccumulatedTime => accumulatedTime; | |
| // Get the target frame time | |
| public float dt => targetFrameTime; | |
| // Get names of all active tasks | |
| public string[] GetActiveTaskNames() | |
| { | |
| string[] names = new string[activeTasks.Count]; | |
| for (int i = 0; i < activeTasks.Count; i++) | |
| { | |
| names[i] = activeTasks[i].name; | |
| } | |
| return names; | |
| } | |
| // Stop a specific task | |
| internal void StopTask(TaskInfo taskInfo) | |
| { | |
| activeTasks.Remove(taskInfo); | |
| } | |
| // Pause a specific task and its tweens | |
| internal void PauseTask(TaskInfo taskInfo) | |
| { | |
| taskInfo.isPaused = true; | |
| } | |
| // Resume a specific task and its tweens | |
| internal void ResumeTask(TaskInfo taskInfo) | |
| { | |
| taskInfo.isPaused = false; | |
| } | |
| // Stop all tasks | |
| public void StopAllTasks() | |
| { | |
| activeTasks.Clear(); | |
| Debug.Log("All tasks stopped"); | |
| } | |
| // Pause all tasks | |
| public void PauseAllTasks() | |
| { | |
| foreach (var task in activeTasks) | |
| { | |
| task.isPaused = true; | |
| } | |
| } | |
| // Resume all tasks | |
| public void ResumeAllTasks() | |
| { | |
| foreach (var task in activeTasks) | |
| { | |
| task.isPaused = false; | |
| } | |
| } | |
| // Reset accumulated time (useful for debugging or specific timing needs) | |
| public void ResetAccumulatedTime() | |
| { | |
| accumulatedTime = 0f; | |
| } | |
| } | |
| // Handle for managing individual tasks | |
| public class TaskHandle | |
| { | |
| private TaskManager manager; | |
| private TaskManager.TaskInfo taskInfo; | |
| internal TaskHandle(TaskManager manager, TaskManager.TaskInfo taskInfo) | |
| { | |
| this.manager = manager; | |
| this.taskInfo = taskInfo; | |
| } | |
| public void Stop() | |
| { | |
| manager?.StopTask(taskInfo); | |
| } | |
| public void Pause() | |
| { | |
| manager?.PauseTask(taskInfo); | |
| } | |
| public void Resume() | |
| { | |
| manager?.ResumeTask(taskInfo); | |
| } | |
| public bool IsValid => manager != null && taskInfo != null; | |
| public bool IsPaused => taskInfo?.isPaused ?? false; | |
| public string Name => taskInfo?.name ?? "Invalid Task"; | |
| } | |
| // Custom yield instructions you can create | |
| public class WaitForTicks | |
| { | |
| public int tickCount; | |
| public WaitForTicks(int ticks) | |
| { | |
| tickCount = ticks; | |
| } | |
| } | |
| // Wait for a DOTween tween to complete with pause support | |
| public class WaitForTween | |
| { | |
| public DG.Tweening.Tween tween; | |
| public bool wasPausedByTask = false; | |
| public WaitForTween(DG.Tweening.Tween tween) | |
| { | |
| this.tween = tween; | |
| } | |
| } | |
| // DOTween wrapper that can be controlled by the task manager | |
| public class ControlledTween | |
| { | |
| private DG.Tweening.Tween tween; | |
| private bool isManualUpdate; | |
| public ControlledTween(DG.Tweening.Tween tween, bool manualUpdate = true) | |
| { | |
| this.tween = tween; | |
| this.isManualUpdate = manualUpdate; | |
| if (manualUpdate && tween != null) | |
| { | |
| // Set the tween to manual update so it doesn't auto-update | |
| tween.SetUpdate(UpdateType.Manual); | |
| } | |
| } | |
| public bool IsComplete => tween == null || !tween.IsActive() || tween.IsComplete(); | |
| public void ManualUpdate(float deltaTime) | |
| { | |
| if (isManualUpdate && tween != null && tween.IsActive() && !tween.IsComplete()) | |
| { | |
| // Manually update the tween with our deltaTime | |
| tween.ManualUpdate(deltaTime, deltaTime); | |
| } | |
| } | |
| public DG.Tweening.Tween Tween => tween; | |
| } | |
| public static class DOTWeenTaskmanExt | |
| { | |
| public static ControlledTween ToYield(this Tween tween) | |
| { | |
| return new ControlledTween(tween); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment