// 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
}
}