Created
September 26, 2018 23:10
-
-
Save GhatSmith/d37eb3adbc379ab0e82bf80b7a640b70 to your computer and use it in GitHub Desktop.
Script allowing to execute code on the main thread. Can be useful if you need to delay code execution or call Unity API without controlling code flow. Like when implementing Unity ISerializationCallbackReceiver interface.
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.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Assertions; | |
using System.Threading; | |
#if UNITY_EDITOR | |
using UnityEditor; | |
#endif | |
/// <summary> | |
/// A queue of commands to execute on the main thread. | |
/// Working in Editor and in Play mode | |
/// Implementation based on http://JacksonDunstan.com/articles/3930 | |
/// </summary> | |
public class MainThreadQueue : MonoBehaviour | |
{ | |
public class DelayedAction | |
{ | |
public System.Action action; | |
public float delay; | |
public float creationTime; | |
public DelayedAction(System.Action action, float delay) | |
{ | |
this.action = action; | |
this.delay = delay; | |
creationTime = Time.realtimeSinceStartup; | |
} | |
} | |
private static MainThreadQueue instance = null; | |
// Queue of commands to execute | |
private static Queue<System.Action> commandQueue; | |
private static List<DelayedAction> delayedCommandQueue; | |
private static List<IMainThreadListener> mainThreadListeners = new List<IMainThreadListener>(); | |
// Stopwatch for limiting the time spent by Execute | |
private static System.Diagnostics.Stopwatch executeLimitStopwatch; | |
private static Thread mainThread = null; | |
public static bool IsMainThread => mainThread != null && Thread.CurrentThread == mainThread; | |
// Cached iterators | |
private static IMainThreadListener listener; | |
private static System.Action action; | |
private static DelayedAction delayedAction; | |
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] | |
static void Init() | |
{ | |
if (instance == null) | |
{ | |
instance = new GameObject(typeof(MainThreadQueue).Name).AddComponent<MainThreadQueue>(); | |
DontDestroyOnLoad(instance.gameObject); | |
} | |
} | |
#if UNITY_EDITOR | |
[InitializeOnLoadMethod()] | |
static void EditorInit() | |
{ | |
EditorApplication.update -= EditorUpdate; | |
EditorApplication.update += EditorUpdate; | |
} | |
#endif | |
private void Awake() | |
{ | |
mainThread = Thread.CurrentThread; | |
} | |
private void Update() | |
{ | |
Execute(5); | |
ExecuteDelayedCommands(5); | |
ExecuteListeners(5); | |
} | |
private static void EditorUpdate() | |
{ | |
Execute(5); | |
ExecuteDelayedCommands(5); | |
ExecuteListeners(5); | |
} | |
/// <summary> Create the queue. It initially has no commands. /// </summary> | |
static MainThreadQueue() | |
{ | |
commandQueue = new Queue<System.Action>(); | |
delayedCommandQueue = new List<DelayedAction>(); | |
executeLimitStopwatch = new System.Diagnostics.Stopwatch(); | |
} | |
/// <summary> Queue a command. This function is thread-safe. </summary> | |
public static void QueueCommand(System.Action action, bool forceWaitingOneFrame = false) | |
{ | |
if (!forceWaitingOneFrame && IsMainThread) action.Invoke(); | |
else | |
{ | |
lock (commandQueue) | |
{ | |
commandQueue.Enqueue(action); | |
} | |
} | |
} | |
/// <summary> Queue a delayed command. This function is thread-safe. </summary> | |
public static void QueueDelayedCommand(DelayedAction delayedCommand) | |
{ | |
lock (delayedCommandQueue) | |
{ | |
delayedCommandQueue.Add(delayedCommand); | |
} | |
} | |
/// <summary> Execute commands until there are none left or a maximum time is used </summary> | |
/// <param name="maxMilliseconds"> Maximum number of milliseconds to execute for. Must be positive. </param> | |
private static void Execute(int maxMilliseconds = int.MaxValue) | |
{ | |
Assert.IsTrue(maxMilliseconds > 0); | |
// Process commands until we run out of time | |
executeLimitStopwatch.Reset(); | |
executeLimitStopwatch.Start(); | |
while (executeLimitStopwatch.ElapsedMilliseconds < maxMilliseconds) | |
{ | |
// Get the next queued action, but stop if the queue is empty | |
lock (commandQueue) | |
{ | |
if (commandQueue.Count == 0) break; | |
action = commandQueue.Dequeue(); | |
} | |
action.Invoke(); | |
} | |
} | |
/// <summary> Execute delayed commands until there are none left or a maximum time is used </summary> | |
/// <param name="maxMilliseconds"> Maximum number of milliseconds to execute for. Must be positive. </param> | |
private static void ExecuteDelayedCommands(int maxMilliseconds = int.MaxValue) | |
{ | |
Assert.IsTrue(maxMilliseconds > 0); | |
// Process delayed commands until we run out of time | |
executeLimitStopwatch.Reset(); | |
executeLimitStopwatch.Start(); | |
int delayedCommandIterator = 0; | |
while (executeLimitStopwatch.ElapsedMilliseconds < maxMilliseconds && delayedCommandIterator < delayedCommandQueue.Count) | |
{ | |
// Get the next queued action, but stop if the queue is empty | |
lock (delayedCommandQueue) | |
{ | |
if (delayedCommandQueue.Count == 0) break; | |
if (Time.realtimeSinceStartup - delayedCommandQueue[delayedCommandIterator].creationTime < delayedCommandQueue[delayedCommandIterator].delay) | |
{ | |
delayedAction = null; | |
delayedCommandIterator++; | |
} | |
else | |
{ | |
delayedAction = delayedCommandQueue[delayedCommandIterator]; | |
delayedCommandQueue.RemoveAt(delayedCommandIterator); | |
} | |
} | |
if (delayedAction != null) delayedAction.action.Invoke(); | |
} | |
} | |
/// <summary> Execute commands until there are none left or a maximum time is used </summary> | |
/// <param name="maxMilliseconds"> Maximum number of milliseconds to execute for. Must be positive. </param> | |
private static void ExecuteListeners(int maxMilliseconds = int.MaxValue) | |
{ | |
Assert.IsTrue(maxMilliseconds > 0); | |
// Process commands until we run out of time | |
executeLimitStopwatch.Reset(); | |
executeLimitStopwatch.Start(); | |
while (executeLimitStopwatch.ElapsedMilliseconds < maxMilliseconds) | |
{ | |
// Get the next queued action, but stop if the queue is empty | |
lock (mainThreadListeners) | |
{ | |
if (mainThreadListeners.Count == 0) | |
{ | |
break; | |
} | |
listener = mainThreadListeners[0]; | |
mainThreadListeners.RemoveAt(0); | |
} | |
listener.OnMainThread(); | |
} | |
} | |
public static void AddListener(IMainThreadListener listener, bool forceWaitingOneFrame = false) | |
{ | |
if (mainThreadListeners.Contains(listener)) return; | |
if (!forceWaitingOneFrame && IsMainThread) listener.OnMainThread(); | |
else | |
{ | |
lock (mainThreadListeners) | |
{ | |
mainThreadListeners.Add(listener); | |
} | |
} | |
} | |
} | |
public interface IMainThreadListener | |
{ | |
void OnMainThread(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment