Skip to content

Instantly share code, notes, and snippets.

@thebeardphantom
Last active February 16, 2022 10:32
Show Gist options
  • Save thebeardphantom/260bbc95287629d3d157d166433ed861 to your computer and use it in GitHub Desktop.
Save thebeardphantom/260bbc95287629d3d157d166433ed861 to your computer and use it in GitHub Desktop.
Simple, efficient task scheduler for Unity using SortedSet<T>.
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
}
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
}
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