Skip to content

Instantly share code, notes, and snippets.

@koster
Created July 22, 2025 18:42
Show Gist options
  • Select an option

  • Save koster/a02e05202cf23e6f39f739b2ed063746 to your computer and use it in GitHub Desktop.

Select an option

Save koster/a02e05202cf23e6f39f739b2ed063746 to your computer and use it in GitHub Desktop.
TaskManager.cs - control your coroutines
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