Last active
February 16, 2022 10:32
-
-
Save thebeardphantom/260bbc95287629d3d157d166433ed861 to your computer and use it in GitHub Desktop.
Simple, efficient task scheduler for Unity using SortedSet<T>.
This file contains 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 UnityEngine; | |
public readonly struct TaskData | |
{ | |
#region Fields | |
public readonly double ExecTime; | |
public readonly double DelayTime; | |
public readonly bool Loop; | |
public readonly TaskHandle Handle; | |
#endregion | |
#region Constructors | |
public TaskData(double execTime, bool loop) : this(TaskHandle.Create(), execTime, loop) { } | |
public TaskData(TaskHandle existingHandle, double execTime, bool loop) | |
{ | |
Handle = existingHandle; | |
ExecTime = execTime; | |
Loop = loop; | |
DelayTime = ExecTime - Time.timeAsDouble; | |
} | |
#endregion | |
} |
This file contains 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; | |
public readonly struct TaskHandle : IEquatable<TaskHandle> | |
{ | |
#region Fields | |
private readonly Guid _guid; | |
#endregion | |
#region Constructors | |
private TaskHandle(Guid guid) | |
{ | |
_guid = guid; | |
} | |
#endregion | |
#region Methods | |
public static TaskHandle Create() | |
{ | |
return new TaskHandle(Guid.NewGuid()); | |
} | |
/// <inheritdoc /> | |
public bool Equals(TaskHandle other) | |
{ | |
return _guid.Equals(other._guid); | |
} | |
/// <inheritdoc /> | |
public override bool Equals(object obj) | |
{ | |
return obj is TaskHandle other && Equals(other); | |
} | |
/// <inheritdoc /> | |
public override int GetHashCode() | |
{ | |
return _guid.GetHashCode(); | |
} | |
#endregion | |
} |
This file contains 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.Generic; | |
using System.Diagnostics; | |
using UnityEngine; | |
using UnityEngine.Assertions; | |
using UnityEngine.Pool; | |
public class TaskSchedulerService : MonoBehaviour | |
{ | |
#region Types | |
public delegate void TaskCallback(in TaskData task); | |
private class ScheduledTask : IComparable<ScheduledTask> | |
{ | |
#region Fields | |
private bool _canceled; | |
#endregion | |
#region Properties | |
public TaskCallback Callback { get; private set; } | |
public TaskData Data { get; private set; } | |
public bool IsValid => | |
!_canceled && Callback.IsNotNull() && (Callback.Method.IsStatic || Callback.Target.IsNotNull()); | |
#endregion | |
#region Methods | |
public void Init(TaskCallback callback, TaskData data) | |
{ | |
Callback = callback; | |
Data = data; | |
} | |
public void Clear() | |
{ | |
Callback = default; | |
Data = default; | |
_canceled = default; | |
} | |
public void Cancel() | |
{ | |
_canceled = true; | |
} | |
/// <inheritdoc /> | |
public int CompareTo(ScheduledTask other) | |
{ | |
return Data.ExecTime.CompareTo(other.Data.ExecTime); | |
} | |
#endregion | |
} | |
#endregion | |
#region Fields | |
private readonly Dictionary<TaskHandle, ScheduledTask> _tasksByHandle = new Dictionary<TaskHandle, ScheduledTask>(); | |
private readonly SortedSet<ScheduledTask> _tasks = new SortedSet<ScheduledTask>(); | |
private readonly Stopwatch _stopwatch = new Stopwatch(); | |
private readonly ObjectPool<ScheduledTask> _taskPool = new ObjectPool<ScheduledTask>( | |
() => new ScheduledTask(), | |
default, | |
task => task.Clear(), | |
default, | |
default, | |
128); | |
#endregion | |
#region Properties | |
[field: SerializeField] | |
private float MaxFrameTimeMs { get; set; } | |
#endregion | |
#region Methods | |
public TaskHandle ScheduleDelayed(Action callback, double delay, bool loop = false) | |
{ | |
return ScheduleDelayed((in TaskData _) => callback(), delay, loop); | |
} | |
public TaskHandle ScheduleAtTime(Action callback, double execTime, bool loop = false) | |
{ | |
return ScheduleAtTime((in TaskData _) => callback(), execTime, loop); | |
} | |
public TaskHandle ScheduleDelayed(TaskCallback callback, double delay, bool loop = false) | |
{ | |
return ScheduleAtTime(callback, Time.timeAsDouble + delay, loop); | |
} | |
public TaskHandle ScheduleAtTime(TaskCallback callback, double execTime, bool loop = false) | |
{ | |
var currentTime = Time.timeAsDouble; | |
Assert.IsTrue(execTime > currentTime, "time > Time.timeAsDouble"); | |
var task = _taskPool.Get(); | |
task.Init(callback, new TaskData(execTime, loop)); | |
_tasks.Add(task); | |
_tasksByHandle.Add(task.Data.Handle, task); | |
return task.Data.Handle; | |
} | |
public void CancelTask(in TaskHandle handle) | |
{ | |
if (_tasksByHandle.TryGetValue(handle, out var task)) | |
{ | |
task.Cancel(); | |
} | |
} | |
private void Update() | |
{ | |
_stopwatch.Restart(); | |
var time = Time.timeAsDouble; | |
while (_tasks.Count > 0 && _stopwatch.Elapsed.TotalMilliseconds < MaxFrameTimeMs) | |
{ | |
var task = _tasks.Min; | |
void RemoveAndRelease() | |
{ | |
_tasks.Remove(task); | |
_taskPool.Release(task); | |
} | |
// Remove/skip if canceled or callback/target is gone | |
if (!task.IsValid) | |
{ | |
RemoveAndRelease(); | |
continue; | |
} | |
// Bail early if task isn't ready yet or spent too much processing time this frame | |
var timeSpentThisFrame = _stopwatch.Elapsed.TotalMilliseconds; | |
if (task.Data.ExecTime > time || timeSpentThisFrame > MaxFrameTimeMs) | |
{ | |
break; | |
} | |
// Cache some necessary data, dequeue/release, invoke callback | |
var taskData = task.Data; | |
var callback = task.Callback; | |
if (task.Data.Loop) | |
{ | |
_tasks.Remove(task); | |
task.Init(callback, new TaskData(task.Data.Handle, time + task.Data.DelayTime, true)); | |
_tasks.Add(task); | |
} | |
else | |
{ | |
RemoveAndRelease(); | |
} | |
callback.Invoke(in taskData); | |
} | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment