#define UNITY_ASSERTIONS using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Assertions; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedAutoPropertyAccessor.Global namespace Nullframes.Threading { /// /// Class for dispatching the execution of a
/// , or /// from a background thread to the main thread. ///
/// #if UNITY_EDITOR [UnityEditor.InitializeOnLoad] #endif [DisallowMultipleComponent] [AddComponentMenu("Threading/Dispatcher")] public sealed partial class Dispatcher : MonoBehaviour { /* * Configuration */ [Space] [Tooltip("Determine the runtime hide flags for the dispatcher scene object")] [SerializeField] private HideFlags runtimeHideFlags = HideFlags.HideInHierarchy; [Tooltip("Enable / Disable execution of dispatched work during FixedUpdate")] [SerializeField] private bool enableFixedUpdate = false; [Tooltip("Enable / Disable execution of dispatched work during Update")] [SerializeField] private bool enableUpdate = true; [Tooltip("Enable / Disable execution of dispatched work during LateUpdate")] [SerializeField] private bool enableLateUpdate = true; /* * Properties & Getter */ /// /// Returns true if called from the main thread and false if not. /// /// /// public static bool IsMainThread() => Thread.CurrentThread.ManagedThreadId == (mainThread?.ManagedThreadId ?? throw new Exception($"{nameof(Dispatcher)}.{nameof(mainThread)} is not initialized")); /// /// Throws an InvalidOperationException if not called from the main thread. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void GuardAgainstIsNotMainThread(string methodCall) { if (!IsMainThread()) { throw new InvalidOperationException($"{methodCall} is only allowed to bne called from the main thread!"); } } /// /// Ensure that a instance exists. /// /// /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] [MethodImpl(MethodImplOptions.NoOptimization)] public static void Validate() { #if UNITY_EDITOR Assert.IsNotNull(GetOrCreateDispatcherInstance()); #else GetOrCreateDispatcherInstance(); #endif } /// /// Get the definition of the currently executed update cycle. /// /// public static ExecutionCycle CurrentCycle { get; private set; } = ExecutionCycle.Update; /// /// Return a that is valid for the duration of the applications runtime. /// This means until OnApplicationQuit is called in a build /// or until the play state is changed in the editor. /// /// public static CancellationToken RuntimeToken => runtimeCts.Token; /// /// Enable / Disable execution of dispatched work during FixedUpdate /// public static bool EnableFixedUpdate { get => Current.enableFixedUpdate; set => Current.enableFixedUpdate = value; } /// /// Enable / Disable execution of dispatched work during Update /// public static bool EnableUpdate { get => Current.enableUpdate; set => Current.enableUpdate = value; } /// /// Enable / Disable execution of dispatched work during LateUpdate /// public static bool EnableLateUpdate { get => Current.enableLateUpdate; set => Current.enableLateUpdate = value; } #if UNITY_EDITOR /// /// Enable / Disable execution of dispatched work during editor update /// This property is editor only! /// public static bool EnableEditorUpdate { get; set; } #endif //-------------------------------------------------------------------------------------------------------------- #region --- Private Fields --- private static readonly Thread mainThread = Thread.CurrentThread; private static CancellationTokenSource runtimeCts = new CancellationTokenSource(); private static readonly Queue defaultExecutionQueue = new Queue(10); private static volatile bool queuedDefault = false; private static readonly Queue fixedUpdateExecutionQueue = new Queue(10); private static volatile bool queuedFixed = false; private static readonly Queue updateExecutionQueue = new Queue(10); private static volatile bool queuedUpdate = false; private static readonly Queue lateUpdateExecutionQueue = new Queue(10); private static volatile bool queuedLate = false; #if UNITY_EDITOR private static readonly Queue editorExecutionQueue = new Queue(10); private static volatile bool queuedEditor = false; static Dispatcher() { UnityEditor.EditorApplication.playModeStateChanged += change => { switch (change) { case UnityEditor.PlayModeStateChange.ExitingEditMode: case UnityEditor.PlayModeStateChange.ExitingPlayMode: CancelAndResetRuntimeToken(); break; } }; UnityEditor.EditorApplication.update += EditorUpdate; } private static void EditorUpdate() { CurrentCycle = ExecutionCycle.EditorUpdate; if (queuedEditor) { lock (editorExecutionQueue) { while (editorExecutionQueue.Count > 0) { editorExecutionQueue.Dequeue().Invoke(); } queuedEditor = false; } } ReleaseDefaultQueue(); } #endif //UNITY_EDITOR [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] private static void DispatchAfterAssembliesLoaded() => ReleaseDefaultQueue(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void DispatchBeforeSceneLoad() => ReleaseDefaultQueue(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void DispatchAfterSceneLoad() => ReleaseDefaultQueue(); private void OnApplicationQuit() { CancelAndResetRuntimeToken(); } private static void CancelAndResetRuntimeToken() { runtimeCts.Cancel(); runtimeCts.Dispose(); runtimeCts = new CancellationTokenSource(); } #endregion //-------------------------------------------------------------------------------------------------------------- #region --- Update: Behaviour --- private void Update() { if (!enableUpdate) { return; } CurrentCycle = ExecutionCycle.Update; if (queuedUpdate) { lock (updateExecutionQueue) { while (updateExecutionQueue.Count > 0) { updateExecutionQueue.Dequeue().Invoke(); } queuedUpdate = false; } } ReleaseDefaultQueue(); } private void LateUpdate() { if (!enableLateUpdate) { return; } CurrentCycle = ExecutionCycle.LateUpdate; if (queuedLate) { lock (lateUpdateExecutionQueue) { while (lateUpdateExecutionQueue.Count > 0) { lateUpdateExecutionQueue.Dequeue().Invoke(); } queuedLate = false; } } ReleaseDefaultQueue(); } private void FixedUpdate() { if (!enableFixedUpdate) { return; } CurrentCycle = ExecutionCycle.FixedUpdate; if (queuedFixed) { lock (fixedUpdateExecutionQueue) { while (fixedUpdateExecutionQueue.Count > 0) { fixedUpdateExecutionQueue.Dequeue().Invoke(); } queuedFixed = false; } } ReleaseDefaultQueue(); } #endregion #region --- Event Queue Release --- [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ReleaseDefaultQueue() { if (!queuedDefault) { return; } lock (defaultExecutionQueue) { while (defaultExecutionQueue.Count > 0) { defaultExecutionQueue.Dequeue()?.Invoke(); } queuedDefault = false; } } #endregion } }