// Copyright (c) Alexander Bogarsukov. // Licensed under the MIT license. See the LICENSE.md file in the project root for more information. using System; using System.Collections; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; namespace UnityFx.Tasks { /// /// Utility methods. /// public static class TaskUtility { #region data private class TaskUtilityData { public SynchronizationContext MainThreadContext; public TaskUtilityBehaviour RootBehaviour; public SendOrPostCallback StartCoroutineCallback; public SendOrPostCallback StopCoroutineCallback; public SendOrPostCallback StopAllCoroutinesCallback; } private static TaskUtilityData _data = new TaskUtilityData(); #endregion #region interface /// /// Gets a value indicating whether the current thread is Unity main thread. /// public static bool IsUnityThread { get { return SynchronizationContext.Current == _data.MainThreadContext; } } /// /// Awaits Unity thread. /// public static CompilerServices.UnityThreadAwaitable YieldToUnityThread() { return new CompilerServices.UnityThreadAwaitable(_data.MainThreadContext); } /// /// Asynchronously loads an asset with the specified name from . /// /// Path the asset in the folder. /// Thrown if is . /// A that can be used to track the operation state. /// public static Task LoadAssetAsync(string assetPath) { return LoadAssetAsync(assetPath, CancellationToken.None); } /// /// Asynchronously loads an asset with the specified name from . /// /// Path the asset in the folder. /// A token that can be used to cancel the operation. /// Thrown if is . /// A that can be used to track the operation state. /// public static Task LoadAssetAsync(string assetPath, CancellationToken cancellationToken) { if (assetPath == null) { throw new ArgumentNullException(nameof(assetPath)); } var op = Resources.LoadAsync(assetPath); if (op == null) { return Task.FromException(new InvalidOperationException($"Cannot load asset '{assetPath}' from resources.")); } return op.ToTask(cancellationToken); } /// /// Asynchronously loads an asset with the specified name and type from . /// /// Path the asset in the folder. /// Thrown if is . /// A that can be used to track the operation state. /// public static Task LoadAssetAsync(string assetPath) where T : UnityEngine.Object { return LoadAssetAsync(assetPath, CancellationToken.None); } /// /// Asynchronously loads an asset with the specified name and type from . /// /// Path the asset in the folder. /// A token that can be used to cancel the operation. /// Thrown if is . /// A that can be used to track the operation state. /// public static Task LoadAssetAsync(string assetPath, CancellationToken cancellationToken) where T : UnityEngine.Object { if (assetPath == null) { throw new ArgumentNullException(nameof(assetPath)); } var op = Resources.LoadAsync(assetPath, typeof(T)); if (op == null) { return Task.FromException(new InvalidOperationException($"Cannot load asset '{assetPath}' from resources.")); } return op.ToTask(cancellationToken); } /// /// Asynchronously loads a scene with the specified name. /// /// Name of the scene to load. /// The scene load mode. /// Thrown if is . /// Thrown if the scene hasn't been added to the build settings or the source AssetBundle hasn't been loaded. /// A that can be used to track the operation state. /// public static Task LoadSceneAsync(string sceneName, LoadSceneMode loadMode = LoadSceneMode.Single) { return LoadSceneAsync(sceneName, loadMode, CancellationToken.None); } /// /// Asynchronously loads a scene with the specified name. /// /// Name of the scene to load. /// The scene load mode. /// A token that can be used to cancel the operation. /// Thrown if is . /// Thrown if the scene hasn't been added to the build settings or the source AssetBundle hasn't been loaded. /// A that can be used to track the operation state. /// public static Task LoadSceneAsync(string sceneName, LoadSceneMode loadMode, CancellationToken cancellationToken) { if (sceneName == null) { throw new ArgumentNullException(nameof(sceneName)); } if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var op = SceneManager.LoadSceneAsync(sceneName, loadMode); // NOTE: If the scene cannot be loaded, result of the LoadSceneAsync is null. if (op != null) { var result = new TaskCompletionSource(op); if (cancellationToken.CanBeCanceled) { cancellationToken.Register(() => result.TrySetCanceled(cancellationToken)); } op.completed += o => { var scene = default(Scene); // NOTE: Grab the last scene with the specified name from the list of loaded scenes. for (var i = SceneManager.sceneCount - 1; i >= 0; --i) { var s = SceneManager.GetSceneAt(i); if (s.name == sceneName) { scene = s; break; } } if (scene.isLoaded) { // NOTE: TrySetResult() failure probably means that the operation was cancelled, thus the scene should be unloaded. if (!result.TrySetResult(scene)) { SceneManager.UnloadSceneAsync(scene); } } else { result.TrySetException(new UnityAssetLoadException(sceneName, typeof(Scene))); } }; return result.Task; } else { return Task.FromException(new InvalidOperationException($"Cannot load scene '{sceneName}'. Please make sure it has been added to the build settings or the {nameof(AssetBundle)} has been loaded successfully.")); } } /// /// Starts a coroutine and wraps it with . /// /// Type of the coroutine result value. /// The coroutine delegate. /// Thrown if is . /// Thrown if the scene hasn't been added to the build settings or the asset bundle hasn't been loaded. /// Returns a instance that can be used to track the coroutine state. /// public static Task FromCoroutine(Func, IEnumerator> coroutineFunc) { return FromCoroutine(coroutineFunc, CancellationToken.None); } /// /// Starts a coroutine and wraps it with . /// /// Type of the coroutine result value. /// The coroutine delegate. /// A token that can be used to cancel the operation. /// Thrown if is . /// Thrown if the scene hasn't been added to the build settings or the asset bundle hasn't been loaded. /// Returns a instance that can be used to track the coroutine state. /// public static Task FromCoroutine(Func, IEnumerator> coroutineFunc, CancellationToken cancellationToken) { if (coroutineFunc == null) { throw new ArgumentNullException(nameof(coroutineFunc)); } if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var result = new TaskCompletionSource(); var enumOp = CoroutineWrapperEnum(coroutineFunc(result), result); if (cancellationToken.CanBeCanceled) { cancellationToken.Register(() => { if (result.TrySetCanceled(cancellationToken)) { _data.RootBehaviour.StopCoroutine(enumOp); } }, true); } _data.RootBehaviour.StartCoroutine(enumOp); return result.Task; } /// /// Starts a coroutine. Can be called from non-Unity thread. /// /// The coroutine to run. /// Thrown if is . /// /// /// public static void StartCoroutine(IEnumerator enumerator) { if (enumerator == null) { throw new ArgumentNullException(nameof(enumerator)); } if (SynchronizationContext.Current == _data.MainThreadContext) { if (_data.RootBehaviour) { _data.RootBehaviour.StartCoroutine(enumerator); } } else { if (_data.StartCoroutineCallback == null) { _data.StartCoroutineCallback = state => { if (_data.RootBehaviour) { _data.RootBehaviour.StartCoroutine(state as IEnumerator); } }; } _data.MainThreadContext.Post(_data.StartCoroutineCallback, enumerator); } } /// /// Stops the specified coroutine. Can be called from non-Unity thread. /// /// The coroutine to stop. /// /// /// public static void StopCoroutine(IEnumerator enumerator) { if (enumerator != null && _data.RootBehaviour) { if (SynchronizationContext.Current == _data.MainThreadContext) { _data.RootBehaviour.StopCoroutine(enumerator); } else { if (_data.StopCoroutineCallback == null) { _data.StopCoroutineCallback = state => _data.RootBehaviour.StopCoroutine(state as IEnumerator); } _data.MainThreadContext.Post(_data.StopCoroutineCallback, enumerator); } } } /// /// Stops all coroutines. Can be called from non-Unity thread. /// /// /// /// public static void StopAllCoroutines() { if (_data.RootBehaviour) { if (SynchronizationContext.Current == _data.MainThreadContext) { _data.RootBehaviour.StopAllCoroutines(); } else { if (_data.StopAllCoroutinesCallback == null) { _data.StopAllCoroutinesCallback = state => _data.RootBehaviour.StopAllCoroutines(); } _data.MainThreadContext.Post(_data.StopAllCoroutinesCallback, null); } } } internal static void Initialize(GameObject go, SynchronizationContext mainThreadContext) { // NOTE: Should only be called once. if (_data.RootBehaviour) { throw new InvalidOperationException(); } _data.MainThreadContext = mainThreadContext; _data.RootBehaviour = go.AddComponent(); } internal static void AddCompletionCallback(UnityWebRequest request, Action action) { _data.RootBehaviour.AddCompletionCallback(request, action); } #endregion #region implementation private sealed class TaskUtilityBehaviour : MonoBehaviour { private Dictionary _ops; private List> _opsToRemove; public void AddCompletionCallback(UnityWebRequest op, Action cb) { if (_ops == null) { _ops = new Dictionary(); _opsToRemove = new List>(); } _ops.Add(op, cb); } private void Update() { if (_ops != null && _ops.Count > 0) { foreach (var item in _ops) { if (item.Key.isDone) { _opsToRemove.Add(item); } } foreach (var item in _opsToRemove) { _ops.Remove(item.Key); try { item.Value(); } catch (Exception e) { Debug.LogException(e, this); } } _opsToRemove.Clear(); } } } private static IEnumerator CoroutineWrapperEnum(IEnumerator workerEnum, TaskCompletionSource tcs) { yield return workerEnum; tcs.TrySetResult(default(T)); } #endregion } }