-
Notifications
You must be signed in to change notification settings - Fork 52
4) Unity Tasks
A utility library inspired by the Task Parallelism Library, but made especially for Unity3d. Supports running and waiting on actions running in background threads. Supports running and waiting on coroutines. Supports coroutines with return results and exception messages !
- Tasks support running on the main thread, background thread or as coroutines.
- Tasks support return results.
- Wait for your tasks to complete in a coroutine or as a thread blocking call
- Graceful exception handling (IsFaulted ?)
- Works on Windows, Mac, Ios, Android
- May have issues on WebGL as it does not support threading (but coroutine should work fine.)
Desktop, Webplayer, iOS, Android, Windows Store
- The "Foundation Tasks" folder contains a very basic example
- The "Foundation.Tasks" folder contains the plugin source code
Drop the Foundation.Tasks.dll into your plug in folder
####Run Tasks have a static factory "Run" method which will create a new task in that started state. This method has overrides for most every situation. You may also construct a task yourself and start it yourself.
####Strategy Tasks have a number of strategies you can choose from.
- Run in Background thread
- Run in a coroutine via the task manager
- Run in the current thread
- Run on the main thread
- Run a custom strategy. This is useful if you want to manually set the task's state, result and exception.
####ContinueWith ContinueWith is a extension method which allows you to execute a piece of code after the task is complete. This is useful with the coroutine strategy as a way to populate the Result property. You may chain multiple continue with's
####Wait
- Wait will halt the thread until the task is complete. Only call this from a background thread. DO NO CALL THIS IN THE MAIN THREAD.
- WaitRoutine is a Coroutine that you may start. This routine will continue until the task is complete. Use this in the main thread.
####TaskManager The task manager is a monobehaviours which interfaces the task's with unity. It is responsible for executing on the main thread and running coroutines. You don't need to add this object to your scene, it is added automatically.
####Exceptions To set the task to the faulted state in an action simply throw an exception. The exception will be saved in the Task.Exception property. For coroutines you will need to set the task state and exception manually (as exception handling in coroutines is limited.
//Pass in an action, function, method or coroutine
var task = UnityTask.Run(() =>
{
throw new Exception("I am failure");
});
I have a static flag to disable background threads. This will cause Unity to act funny (pausing the main thread), but, you will get a complete stack trace.
/// <summary>
/// Forces use of a single thread for debugging
/// </summary>
public static bool DisableMultiThread = false;
/// <summary>
/// Logs Exceptions
/// </summary>
public static bool LogErrors = false;
Tasks are long running processes, so you should use tasks from a coroutine somewhere in your code. Just like the WWW class.
For example lets take a login task
public class AccountMenu : Monobehaviour {
// run on instance startup
IEnumerator Start(){
// returns a UnityTask<bool>
var task = AccountService.Login();
// wait for the task to finish
yield return StartCoroutine(task.WaitRoutine());
if(task.IsFaulted)
// handle fault
else
// handle success
}
}
In the above example the AccountService would be returning a Task of some sort. Internally, it could be a action running in a background thread or a coroutine on the unity thread.
public class AccountService {
public UnityTask<bool> Login(){
// run in background thread
return UnityTask.Run(LoginInternal);
// or run in unity thread as a coroutine
return UnityTask<.RunCoroutine<bool>(LoginInternal2);
}
bool LoginInternal(){
// do work
}
IEnumerator LoginInternal2(UnityTask<bool> task){
// do work
// manually set result / state
}
}
The Custom strategy is used when you want to return a task without wrapping a action or coroutine. Here are two examples
For instance if the method fails a sanity check I will return a custom task in the faulted state and set the exception message manually. I figure this is less overhead than spinning up a background thread and throwing it.
void Login(string username){
return new UnityTask(TaskStrategy.Custom) {
Status = TaskStatus.Faulted,
Exception = new Exception("Invalid Username")
};
//or extension method
return Task.FailedTask("Invalid Username");
}
I also use the custom strategy when I need to return a task but the internal method uses an arbitrary callback - such as in the case of UnityNetworking.
UnityTask<bool> ConnectTask;
void Awake(){
ConnectTask = new UnityTask<bool>(TaskStrategy.Custom);
}
UnityTask<bool> ConnectToServer(HostData username){
ConnectTask.State = TaskState.Running;
// Do Unity Connect Logic here
// Consumer will 'wait' untill this server fails/successes the task
return ConnectTask;
}
void OnConnectedToServer(){
ConnectTask.Result = true;
ConnectTask.State = TaskState.Success;
}
// todo fail and timeout for the connect task
// Assume running from a coroutine
// Action
Debug.Log("Action...");
var mtask = UnityTask.Run(() => { Counter++; });
yield return mtask;
Assert(() => Counter == 1, "Action");
yield return new WaitForSeconds(2);
// Action with
Debug.Log("Action w/ Continue...");
yield return new WaitForSeconds(1);
yield return UnityTask.Run(() =>
{
Counter++;
}).ContinueWith(task =>
{
Counter++;
});
Assert(() => Counter == 3, "Action w/ Continue");
// Coroutine
Debug.Log("Continue...");
yield return new WaitForSeconds(1);
yield return UnityTask.RunCoroutine(DemoCoroutine);
Assert(() => Counter == 4, "Coroutine");
// Coroutine with Continue
Debug.Log("Coroutine w/ Continue...");
yield return new WaitForSeconds(1);
yield return UnityTask.RunCoroutine(DemoCoroutine).ContinueWith(task => { Counter++; });
Assert(() => Counter == 6, "Coroutine w/ Continue");
// Coroutine with result
Debug.Log("Coroutine w/ result...");
yield return new WaitForSeconds(1);
var ctask = UnityTask.RunCoroutine<string>(DemoCoroutineWithResult);
yield return ctask;
Assert(() => Counter == 7, "Coroutine w/ result (A)");
Assert(() => ctask.Result == "Hello", "Coroutine w/ result (B)");
// Coroutine with result and Continue
Debug.Log("CoroutineCoroutine w/ result and continue...");
yield return new WaitForSeconds(1);
var ctask2 = UnityTask.RunCoroutine<string>(DemoCoroutineWithResult).ContinueWith(t =>
{
Assert(() => t.Result == "Hello", "Coroutine w/ result and continue (A)");
t.Result = "Goodbye";
Counter++;
});
yield return ctask2;
Assert(() => Counter == 9, "Coroutine w/ result and continue (B)");
Assert(() => ctask2.Result == "Goodbye", "Coroutine w/ result and continue (C)");
// Function
Debug.Log("Function...");
yield return new WaitForSeconds(1);
var ftask = UnityTask.Run(() =>
{
Counter++;
return "Hello";
});
yield return ftask;
Assert(() => Counter == 10, "Function");
Assert(() => ftask.Result == "Hello", "Function");
//Exception
Debug.Log("Exception...");
yield return new WaitForSeconds(1);
var etask = UnityTask.Run(() => { Counter++; throw new Exception("Hello"); });
yield return etask;
Assert(() => Counter == 11, "Exception");
Assert(() => etask.IsFaulted && etask.Exception.Message == "Hello", "Exception");
//Main Thread
log = string.Empty;
Debug.Log("Basic Tests Passed");
Debug.Log("Threading Tests...");
Debug.Log("Background....");
yield return new WaitForSeconds(1);
yield return UnityTask.Run(() =>
{
UnityTask.Delay(50);
Debug.Log("Sleeping...");
UnityTask.Delay(2000);
Debug.Log("Slept");
});
yield return 1;
Debug.Log("BackgroundToMain");
yield return new WaitForSeconds(1);
yield return UnityTask.Run(() =>
{
UnityTask.Delay(100);
var task = UnityTask.RunOnMain(() =>
{
Debug.Log("Hello From Main");
});
while (task.IsRunning)
{
log += ".";
UnityTask.Delay(100);
}
});
yield return 1;
Debug.Log("BackgroundToRotine");
yield return new WaitForSeconds(1);
yield return UnityTask.Run(() =>
{
var task = UnityTask.RunCoroutine(LongCoroutine());
while (task.IsRunning)
{
log += ".";
UnityTask.Delay(500);
}
});
yield return 1;
Debug.Log("BackgroundToBackground");
yield return new WaitForSeconds(1);
yield return UnityTask.Run(() =>
{
Debug.Log("1 Sleeping...");
UnityTask.Run(() =>
{
Debug.Log("2 Sleeping...");
UnityTask.Delay(2000);
Debug.Log("2 Slept");
});
UnityTask.Delay(2000);
Debug.Log("1 Slept");
});
yield return 1;
Debug.Log("Success All");