Created
June 1, 2025 02:58
-
-
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
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.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