Skip to content

Instantly share code, notes, and snippets.

@GhatSmith
Created September 26, 2018 23:10
Show Gist options
  • Save GhatSmith/d37eb3adbc379ab0e82bf80b7a640b70 to your computer and use it in GitHub Desktop.
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.
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