Examples
These examples can also be found in the .example
directory.
Basic example
using System.Threading.Tasks;
using UnityEngine;
class MyClass : MonoBehaviour
{
void Start()
{
this.StartTask(RunAsync);
}
async Task RunAsync()
{
while (true)
{
Debug.Log("Running...");
await Task.Yield();
}
}
}
This example will print Running...
every frame when the component is enabled and will stop when
the component gets destroyed.
Awaiting other methods
using System;
using System.Threading.Tasks;
using UnityEngine;
class MyClass : MonoBehaviour
{
void Start()
{
this.StartTask(RunAsync);
}
async Task RunAsync()
{
while (true)
{
var val = await GetValueAsync();
Debug.Log($"Got value: '{val}'");
}
}
async Task<int> GetValueAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
return Time.frameCount;
}
}
When you await
other methods they automatically belong to the same scope as the task that starts
them. So in this example GetValueAsync
also runs as part of the MyClass
scope and stops when
the component is destroyed.
Exposing a method that produces a value
But what if you want to expose a api that produces a value, what happens to the task once your component gets destroyed.
using System;
using System.Threading.Tasks;
using UnityEngine;
class Producer : MonoBehaviour
{
public Task<int> GetValueAsync() => this.StartTask(ProduceValueAsync);
async Task<int> ProduceValueAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
return Time.frameCount;
}
}
The Task
(or Task<T>
) returned out of This.StartTask(...)
properly goes into a 'cancelled'
state when the component is destroyed. Which means that when you await that task you will get a
TaskCanceledException
that you can handle.
On the receiving side:
using System.Threading.Tasks;
using UnityEngine;
class Consumer : MonoBehaviour
{
[SerializeField] private Producer producer;
void Start()
{
this.StartTask(RunAsync);
}
async Task RunAsync()
{
try
{
var val = await producer.GetValueAsync();
Debug.Log($"Got value: '{val}'");
}
catch (TaskCanceledException)
{
Debug.Log("The producer was destroyed before producing the result");
}
}
}
If you don't catch the exception then the exception is logged to the unity log and your task will go to a faulted state. But the nice thing is that if both components have the same lifetime (destroyed at the same time) then there is no problem (and you won't get any exceptions).
Avoiding closures
To avoid having to capture closures you can pass an argument into you task using this.StartTask(...)
.
using System;
using System.Threading.Tasks;
using UnityEngine;
class MyClass : MonoBehaviour
{
void Start()
{
var delay = 1;
this.StartTask(WaitAndDoSomethingAsync, delay);
}
async Task WaitAndDoSomethingAsync(int secondsDelay)
{
await Task.Delay(TimeSpan.FromSeconds(secondsDelay));
Debug.Log("Doing something");
}
}
Only one argument is supported but with the 'new' tuples in c# 7 there is a nice (and efficient) workaround:
using System;
using System.Threading.Tasks;
using UnityEngine;
class MyClassWithValueTuple : MonoBehaviour
{
void Start()
{
this.StartTask(WaitAndLog, (secondsDelay: 1, message: "Hello World"));
}
async Task WaitAndLog((int secondsDelay, string message) input)
{
await Task.Delay(TimeSpan.FromSeconds(input.secondsDelay));
Debug.Log(input.message);
}
}
Cancelling external work
To make it easier to cancel external work when your component is destroyed this.StartTask(...)
optionally gives you a CancellationToken
to give to external api's.
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
class MyClass : MonoBehaviour
{
void Start()
{
var url = "https://github.com/BastianBlokland/componenttask-unity";
this.StartTask(DownloadTextAsync, url);
}
async Task DownloadTextAsync(string url, CancellationToken cancelToken)
{
using (var client = new HttpClient())
{
var response = await client.GetAsync(url, cancelToken);
var responseText = await response.Content.ReadAsStringAsync();
Debug.Log($"Text: '{responseText}'");
}
}
}
Giving the CancellationToken
here will make sure that the web-request is actually aborted when
this component is destroyed.
Running expensive blocking work on a background thread
Something that the Task
based model make very easy is interacting with code that runs on a
different thread. You can for example run your expensive blocking code in a background thread and
await a Task
handle to it.
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
class MyClass : MonoBehaviour
{
void Start()
{
this.StartTask(RunAsync);
}
async Task RunAsync()
{
var result = await Task.Run(VeryExpensiveBlockingCode);
Debug.Log($"Got value: '{result}'");
}
int VeryExpensiveBlockingCode()
{
Thread.Sleep(TimeSpan.FromSeconds(5));
return 42;
}
}
Even though VeryExpensiveBlockingCode
blocks for 5 seconds because we run it on a background-thread
(with Task.Run) the unity-thread stays responsive.
Caching a task-runner.
If you are going to start many tasks you can also create a ITaskRunner on a gameobject and cache a reference to it. That runner will remain valid as long as that gameobject is still alive.
using System.Threading.Tasks;
using UnityEngine;
using ComponentTask;
class MyClass : MonoBehaviour
{
private ITaskRunner runner;
void Start()
{
this.runner = this.gameObject.CreateTaskRunner();
}
void Update()
{
this.runner.StartTask(this.WaitAndLogAsync);
}
async Task WaitAndLogAsync()
{
await Task.Yield();
Debug.Log("Running");
}
}
Custom LocalTaskRunner.
If you dont to scope you tasks to Unity Components but control the update ticks yourself you can manually create a 'LocalTaskRunner' and control its ticks yourself.
using System;
using System.Threading.Tasks;
using UnityEngine;
using ComponentTask;
class MyClass : MonoBehaviour, IExceptionHandler
{
[SerializeField] private bool isPaused;
private LocalTaskRunner runner;
void Start()
{
this.runner = new LocalTaskRunner(exceptionHandler: this);
this.runner.StartTask(this.RunAsync);
}
void Update()
{
if (!this.isPaused)
this.runner.Execute();
}
void OnDestroy()
{
this.runner.Dispose();
}
async Task RunAsync()
{
while (true)
{
Debug.Log("Running");
await Task.Yield();
}
}
void IExceptionHandler.Handle(Exception exception)
{
Debug.Log($"Exception occurred: '{exception.Message}'");
}
}
OnGUI tasks using custom LocalTaskRunner.
Because with a custom LocalTaskRunner you control when tasks are updated you could implement tasks
that run during OnGUI
to draw ui.
using System;
using System.Threading.Tasks;
using UnityEngine;
using ComponentTask;
class MyClass : MonoBehaviour, IExceptionHandler
{
private LocalTaskRunner guiTaskRunner;
void Start()
{
this.guiTaskRunner = new LocalTaskRunner(exceptionHandler: this);
this.guiTaskRunner.StartTask(this.DrawUIAsync);
}
void OnGUI()
{
this.guiTaskRunner.Execute();
}
void OnDestroy()
{
this.guiTaskRunner.Dispose();
}
async Task DrawUIAsync()
{
while (true)
{
await Task.Yield();
GUI.Label(new Rect(0f, 0f, 100f, 100f), "Drawn from a task :)");
}
}
void IExceptionHandler.Handle(Exception exception)
{
Debug.Log($"Exception occurred: '{exception.Message}'");
}
}
Using a similar pattern your can make tasks task run during FixedUpdate
to interact with Unity's
physics world for example.