Skip to content

Instantly share code, notes, and snippets.

@instance-id
Created June 1, 2025 02:58
Show Gist options
  • Save instance-id/dd75c2c44bcdd0ddb0593255471b050b to your computer and use it in GitHub Desktop.
Save instance-id/dd75c2c44bcdd0ddb0593255471b050b to your computer and use it in GitHub Desktop.
Unity replacement for EditorApplication.delayCall using UniTask that can run in the background without needing Editor window focus
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace instance.id
{
// --|-------------------------------------------------
// --| Requires UniTask Package -----------------------
// --|-------------------------------------------------
/// <summary>
/// Provides background task scheduling that continues to execute even when the Unity Editor loses focus.
/// A replacement for EditorApplication.delayCall that uses UniTask for better background execution.
/// </summary>
/// <remarks>
/// This class requires the UniTask package to be installed.
/// </remarks>
public static class BackgroundTaskScheduler
{
private static int taskIdCounter;
private static bool isInitialized;
private static readonly Dictionary<int, CancellationTokenSource> taskCancellationSources
= new Dictionary<int, CancellationTokenSource>();
/// <summary> Initialize the BackgroundTaskScheduler. Called automatically when needed. </summary>
public static void Initialize()
{
if (isInitialized) return;
isInitialized = true;
#if UNITY_EDITOR
EditorApplication.quitting += CancelAllTasks;
EditorApplication.update += EditorApplication.QueuePlayerLoopUpdate;
#else
Application.quitting += CancelAllTasks;
#endif
}
/// <summary>
/// Schedule a task to execute after a single frame delay.
/// Replacement for EditorApplication.delayCall that works when editor loses focus.
/// </summary>
/// <param name="action">The action to execute</param>
/// <returns>Task ID that can be used to cancel the task</returns>
/// <example>
/// <code>
/// BackgroundTaskScheduler.Schedule(() => {
/// // Your delayed code
/// });
/// </code>
/// </example>
public static int Schedule(Action action) => ScheduleDelayed(action, 1);
/// <summary> Schedule a task to execute after the specified number of frames. </summary>
/// <param name="action">The action to execute</param>
/// <param name="frameDelay">Number of frames to delay execution</param>
/// <returns>Task ID that can be used to cancel the task</returns>
public static int ScheduleDelayed(Action action, int frameDelay = 1)
{
if (action == null) throw new ArgumentNullException(nameof(action));
Initialize();
int taskId = taskIdCounter++;
var cts = new CancellationTokenSource();
taskCancellationSources[taskId] = cts;
ExecuteDelayedInternal(action, frameDelay, taskId, cts.Token).Forget();
return taskId;
}
/// <summary> Schedule a task to execute after the specified time delay. </summary>
/// <param name="action">The action to execute</param>
/// <param name="delayInMilliseconds">Delay in milliseconds</param>
/// <returns>Task ID that can be used to cancel the task</returns>
public static int ScheduleDelayed(Action action, float delayInMilliseconds)
{
if (action == null) throw new ArgumentNullException(nameof(action));
Initialize();
int taskId = taskIdCounter++;
var cts = new CancellationTokenSource();
taskCancellationSources[taskId] = cts;
ExecuteDelayedTimeInternal(action, delayInMilliseconds, taskId, cts.Token).Forget();
return taskId;
}
/// <summary> Schedule a task to execute periodically. </summary>
/// <param name="action">The action to execute</param>
/// <param name="intervalInMilliseconds">Interval between executions in milliseconds</param>
/// <param name="initialDelayInMilliseconds">Initial delay before first execution in milliseconds</param>
/// <returns>Task ID that can be used to cancel the task</returns>
/// <example><code>
/// // Schedule a task to execute every 5 seconds
/// int taskId = BackgroundTaskScheduler.SchedulePeriodic(() => {
/// Debug.Log("This will execute every 5 seconds");
/// }, 5000, 1000);
///
/// // Cancel the task
/// BackgroundTaskScheduler.CancelTask(taskId);
/// </code></example>
public static int SchedulePeriodic(Action action, float intervalInMilliseconds, float initialDelayInMilliseconds = 0)
{
if (action == null) throw new ArgumentNullException(nameof(action));
Initialize();
int taskId = taskIdCounter++;
var cts = new CancellationTokenSource();
taskCancellationSources[taskId] = cts;
ExecutePeriodicInternal(action, intervalInMilliseconds, initialDelayInMilliseconds, taskId, cts.Token).Forget();
return taskId;
}
/// <summary> Cancel a scheduled task by its ID. </summary>
/// <param name="taskId">The task ID to cancel</param>
/// <returns>True if the task was found and canceled, false otherwise</returns>
public static bool CancelTask(int taskId)
{
if (taskCancellationSources.TryGetValue(taskId, out var cts))
{
cts.Cancel();
cts.Dispose();
taskCancellationSources.Remove(taskId);
return true;
}
return false;
}
/// <summary> Cancel all scheduled tasks. </summary>
public static void CancelAllTasks()
{
foreach (var cts in taskCancellationSources.Values)
{
cts.Cancel();
cts.Dispose();
}
taskCancellationSources.Clear();
}
private static async UniTaskVoid ExecuteDelayedInternal(Action action, int frameDelay, int taskId, CancellationToken cancellationToken)
{
try
{
await UniTask.DelayFrame(frameDelay, PlayerLoopTiming.Update, cancellationToken); //@formatter:off
if (!cancellationToken.IsCancellationRequested) {
try { action(); }
catch (Exception ex) { Debug.LogException(ex); }
}
}
catch (OperationCanceledException) { /* Task was canceled, do nothing */ }
catch (Exception ex) { Debug.LogException(ex); }
finally { CleanupTask(taskId); }
} //@formatter:on
private static async UniTaskVoid ExecuteDelayedTimeInternal(Action action, float delayInMilliseconds, int taskId, CancellationToken cancellationToken)
{
try
{
await UniTask.Delay((int)delayInMilliseconds, false, PlayerLoopTiming.Update, cancellationToken);
if (!cancellationToken.IsCancellationRequested) //@formatter:off
{
try { action(); }
catch (Exception ex) { Debug.LogException(ex); }
}
}
catch (OperationCanceledException) { /* Task was canceled, do nothing */ }
catch (Exception ex) { Debug.LogException(ex); }
finally { CleanupTask(taskId); }
} //@formatter:on
private static async UniTaskVoid ExecutePeriodicInternal(Action action, float intervalInMilliseconds, float initialDelayInMilliseconds, int taskId, CancellationToken cancellationToken)
{
try
{
// Initial delay
if (initialDelayInMilliseconds > 0) //@formatter:off
await UniTask.Delay((int)initialDelayInMilliseconds, false, PlayerLoopTiming.Update, cancellationToken);
// Continue until canceled
while (!cancellationToken.IsCancellationRequested)
{
try { action(); }
catch (Exception ex) { Debug.LogException(ex); }
await UniTask.Delay((int)intervalInMilliseconds, false, PlayerLoopTiming.Update, cancellationToken);
}
}
catch (OperationCanceledException) { /* Task was canceled, do nothing */ }
catch (Exception ex) { Debug.LogException(ex); }
finally { CleanupTask(taskId); }
} //@formatter:on
private static void CleanupTask(int taskId)
{
if (!taskCancellationSources.TryGetValue(taskId, out var cts))
return;
cts.Dispose();
taskCancellationSources.Remove(taskId);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment