| name | unitask |
| description | UniTask async/await for Unity — zero-alloc async, cancellation tokens, PlayerLoop integration, async LINQ. Use instead of coroutines for cancellation support and cleaner async code. |
| globs | ["**/UniTask*","**/*Async*.cs","**/Cysharp*"] |
UniTask — Zero-Allocation Async/Await for Unity
UniTask (Cysharp) provides async/await that integrates natively with Unity's PlayerLoop, produces zero GC allocations, and supports proper cancellation. Prefer UniTask over coroutines and System.Threading.Tasks.Task in all Unity projects.
UniTask vs Coroutines vs System.Threading.Tasks.Task
| Feature | Coroutine | Task | UniTask |
|---|
| GC allocation | Enumerator + box | Task object + state machine | Zero (struct-based) |
| Cancellation | Manual flag | CancellationToken | CancellationToken |
| Return values | No | Yes | Yes |
| Exception handling | Swallowed silently | try/catch | try/catch |
| Runs on thread pool | No | Yes (dangerous in Unity) | No (PlayerLoop) |
| Awaitable | No | Yes | Yes |
Basic Usage
Method Signatures
using Cysharp.Threading.Tasks;
public async UniTask LoadLevelAsync(CancellationToken ct)
{
await UniTask.Delay(1000, cancellationToken: ct);
}
public async UniTask<int> CalculateScoreAsync(CancellationToken ct)
{
await UniTask.Yield(ct);
return 100;
}
public async UniTaskVoid OnButtonClickedAsync()
{
await DoSomethingAsync(this.GetCancellationTokenOnDestroy());
}
CRITICAL: Never Use async void
public async void DoSomething() { ... }
public async UniTask DoSomethingAsync(CancellationToken ct) { ... }
public async UniTaskVoid DoSomethingFireAndForget() { ... }
Waiting and Delays
await UniTask.Delay(1000, cancellationToken: ct);
await UniTask.Delay(TimeSpan.FromSeconds(1.5f), cancellationToken: ct);
await UniTask.Yield();
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
await UniTask.NextFrame(ct);
await UniTask.DelayFrame(5, cancellationToken: ct);
await UniTask.WaitUntil(() => m_IsReady, cancellationToken: ct);
await UniTask.WaitWhile(() => m_IsLoading, cancellationToken: ct);
await UniTask.WaitUntilValueChanged(transform, t => t.position, cancellationToken: ct);
await SceneManager.LoadSceneAsync("GameScene").ToUniTask(cancellationToken: ct);
await Resources.LoadAsync<Texture2D>("myTexture").ToUniTask(cancellationToken: ct);
await UnityWebRequest.Get(url).SendWebRequest().ToUniTask(cancellationToken: ct);
Cancellation Tokens
CRITICAL: Always Pass Cancellation Tokens
Async operations that outlive their owning object cause MissingReferenceException and undefined behavior. Every async method must accept and respect a CancellationToken.
Pattern 1: GetCancellationTokenOnDestroy (Simple)
public class SimpleAsync : MonoBehaviour
{
private async UniTaskVoid Start()
{
CancellationToken ct = this.GetCancellationTokenOnDestroy();
await UniTask.Delay(2000, cancellationToken: ct);
Debug.Log("This won't run if object was destroyed");
}
}
Pattern 2: Manual CancellationTokenSource (Enable/Disable)
public class ManagedAsync : MonoBehaviour
{
private CancellationTokenSource m_Cts;
private void OnEnable()
{
m_Cts = new CancellationTokenSource();
RunLoopAsync(m_Cts.Token).Forget();
}
private void OnDisable()
{
m_Cts?.Cancel();
m_Cts?.Dispose();
m_Cts = null;
}
private async UniTask RunLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await UniTask.Delay(1000, cancellationToken: ct);
DoPeriodicWork();
}
}
}
Pattern 3: Linked Tokens (Combine Destroy + Manual Cancel)
public class LinkedTokenExample : MonoBehaviour
{
private CancellationTokenSource m_ActionCts;
public async UniTask PerformActionAsync()
{
m_ActionCts?.Cancel();
m_ActionCts?.Dispose();
m_ActionCts = new CancellationTokenSource();
CancellationToken destroyCt = this.GetCancellationTokenOnDestroy();
CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(
m_ActionCts.Token, destroyCt);
try
{
await DoWorkAsync(linked.Token);
}
catch (OperationCanceledException)
{
}
finally
{
linked.Dispose();
}
}
}
Handling OperationCanceledException
public async UniTask LoadDataAsync(CancellationToken ct)
{
try
{
await SomeAsyncOperation(ct);
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
PlayerLoop Integration
UniTask hooks into Unity's PlayerLoop for precise timing control.
await UniTask.Yield(PlayerLoopTiming.Initialization);
await UniTask.Yield(PlayerLoopTiming.EarlyUpdate);
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
await UniTask.Yield(PlayerLoopTiming.PreUpdate);
await UniTask.Yield(PlayerLoopTiming.Update);
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate);
await UniTask.WaitForFixedUpdate(ct);
await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate, ct);
WhenAll / WhenAny — Parallel Execution
(int score, string name) = await UniTask.WhenAll(
LoadScoreAsync(ct),
LoadNameAsync(ct)
);
int winnerIndex = await UniTask.WhenAny(
WaitForInputAsync(ct),
WaitForTimeoutAsync(5f, ct)
);
(bool hasResult, int result) = await UniTask.WhenAny(
FetchFromCacheAsync(ct),
FetchFromNetworkAsync(ct)
);
var textures = await UniTask.WhenAll(
paths.Select(p => LoadTextureAsync(p, ct))
);
Forget and Fire-and-Forget
DoSomethingAsync(ct).Forget();
DoSomethingAsync(ct).SuppressCancellationThrow().Forget();
UniTaskCompletionSource — Manual Completion
For wrapping callback-based APIs or creating custom awaitable operations.
public class DialogSystem : MonoBehaviour
{
private UniTaskCompletionSource<DialogResult> m_DialogTcs;
public async UniTask<DialogResult> ShowDialogAsync(string message, CancellationToken ct)
{
m_DialogTcs = new UniTaskCompletionSource<DialogResult>();
ct.Register(() => m_DialogTcs.TrySetCanceled());
ShowDialogUI(message);
return await m_DialogTcs.Task;
}
public void OnConfirmClicked() => m_DialogTcs.TrySetResult(DialogResult.Confirm);
public void OnCancelClicked() => m_DialogTcs.TrySetResult(DialogResult.Cancel);
}
Async LINQ
UniTask provides async LINQ operators for event streams.
using Cysharp.Threading.Tasks.Linq;
button.OnClickAsAsyncEnumerable()
.ForEachAsync(_ =>
{
Debug.Log("Clicked");
}, ct);
button.OnClickAsAsyncEnumerable()
.ThrottleFirst(TimeSpan.FromSeconds(1))
.ForEachAsync(_ => ProcessClick(), ct);
var channel = Channel.CreateSingleConsumerUnbounded<int>();
channel.Writer.TryWrite(42);
await channel.Reader.ReadAllAsync(ct).ForEachAsync(item => Process(item));
Integration with DOTween
Await DOTween animations using the DOTween-UniTask bridge.
await transform.DOMove(targetPos, 1f)
.SetEase(Ease.OutQuad)
.ToUniTask(cancellationToken: ct);
Sequence seq = DOTween.Sequence();
seq.Append(transform.DOScale(1.2f, 0.2f));
seq.Append(transform.DOScale(1f, 0.2f));
await seq.ToUniTask(cancellationToken: ct);
await transform.DOMove(pointA, 0.5f).ToUniTask(cancellationToken: ct);
await transform.DOMove(pointB, 0.5f).ToUniTask(cancellationToken: ct);
await transform.DOMove(pointC, 0.5f).ToUniTask(cancellationToken: ct);
Integration with Addressables
GameObject prefab = await Addressables.LoadAssetAsync<GameObject>("EnemyPrefab")
.ToUniTask(cancellationToken: ct);
GameObject instance = await Addressables.InstantiateAsync("EnemyPrefab", position, rotation)
.ToUniTask(cancellationToken: ct);
await Addressables.LoadSceneAsync("GameScene", LoadSceneMode.Additive)
.ToUniTask(cancellationToken: ct);
Common Patterns
Async Initialization Chain
public class GameBootstrap : MonoBehaviour
{
private async UniTaskVoid Start()
{
CancellationToken ct = this.GetCancellationTokenOnDestroy();
try
{
await InitializeServicesAsync(ct);
await LoadPlayerDataAsync(ct);
await PreloadAssetsAsync(ct);
await LoadGameSceneAsync(ct);
}
catch (OperationCanceledException)
{
Debug.Log("Bootstrap cancelled");
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
}
Async State Machine
public class EnemyAI : MonoBehaviour
{
private CancellationTokenSource m_Cts;
private void OnEnable()
{
m_Cts = new CancellationTokenSource();
RunAIAsync(m_Cts.Token).Forget();
}
private void OnDisable()
{
m_Cts?.Cancel();
m_Cts?.Dispose();
}
private async UniTask RunAIAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await PatrolAsync(ct);
await ChaseAsync(ct);
await AttackAsync(ct);
await UniTask.Yield(ct);
}
}
private async UniTask PatrolAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested && !CanSeePlayer())
{
MoveToNextWaypoint();
await UniTask.Delay(100, cancellationToken: ct);
}
}
}
Timeout Wrapper
public static async UniTask<T> WithTimeout<T>(
UniTask<T> task,
float timeoutSeconds,
CancellationToken ct)
{
int winnerIndex = await UniTask.WhenAny(
task,
UniTask.Delay(TimeSpan.FromSeconds(timeoutSeconds), cancellationToken: ct)
.ContinueWith(() => default(T))
);
if (winnerIndex == 1)
throw new TimeoutException($"Operation timed out after {timeoutSeconds}s");
return await task;
}
Debounced Input
private async UniTask ProcessSearchInputAsync(TMP_InputField input, CancellationToken ct)
{
string previousText = string.Empty;
while (!ct.IsCancellationRequested)
{
await UniTask.WaitUntilValueChanged(input, i => i.text, cancellationToken: ct);
await UniTask.Delay(300, cancellationToken: ct);
string currentText = input.text;
if (currentText != previousText)
{
previousText = currentText;
await PerformSearchAsync(currentText, ct);
}
}
}
Anti-Patterns
Do not use async void anywhere
public async void OnButtonClicked() { ... }
public async UniTaskVoid OnButtonClickedAsync()
{
CancellationToken ct = this.GetCancellationTokenOnDestroy();
await HandleClickAsync(ct);
}
Do not forget cancellation tokens
public async UniTask BadMethod()
{
await UniTask.Delay(5000);
transform.position = Vector3.zero;
}
public async UniTask GoodMethod(CancellationToken ct)
{
await UniTask.Delay(5000, cancellationToken: ct);
transform.position = Vector3.zero;
}
Do not use Task.Run or Task.Delay in Unity
await Task.Run(() => transform.position = Vector3.zero);
await Task.Delay(1000);
await UniTask.SwitchToMainThread();
await UniTask.Delay(1000);