tools-unity-unitask

SKILL.md

UniTask Async/Await for Unity

Overview

UniTask provides efficient async/await support for Unity with zero allocation, proper cancellation, and Unity lifecycle integration.

When to Use

  • Async operations in Unity (loading, networking, delays)
  • Replacing coroutines with async/await
  • Managing cancellation in MonoBehaviours
  • Async initialization patterns
  • Parallel async operations

Installation

// manifest.json
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask"

Basic Patterns

Simple Async Method

public async UniTask LoadGameAsync()
{
    await UniTask.Delay(1000); // 1 second delay
    await LoadAssetsAsync();
    await InitializeSystemsAsync();
}

Async with Return Value

public async UniTask<PlayerData> LoadPlayerAsync()
{
    var json = await File.ReadAllTextAsync(path);
    return JsonUtility.FromJson<PlayerData>(json);
}

Fire-and-Forget (Use Carefully!)

// ForgetTask() suppresses warnings but loses error handling
LoadGameAsync().Forget();

// Better: Use UniTaskVoid for true fire-and-forget
public async UniTaskVoid StartBackgroundTask()
{
    try
    {
        await DoWorkAsync();
    }
    catch (Exception ex)
    {
        Debug.LogException(ex);
    }
}

Cancellation Patterns

Basic CancellationToken

public class GameLoader : MonoBehaviour
{
    private CancellationTokenSource _cts;
    
    private void OnEnable()
    {
        _cts = new CancellationTokenSource();
        LoadAsync(_cts.Token).Forget();
    }
    
    private void OnDisable()
    {
        _cts?.Cancel();
        _cts?.Dispose();
        _cts = null;
    }
    
    private async UniTask LoadAsync(CancellationToken ct)
    {
        await UniTask.Delay(1000, cancellationToken: ct);
        // Work here...
    }
}

Destroy CancellationToken (Preferred for MonoBehaviour)

public class Enemy : MonoBehaviour
{
    private async UniTaskVoid Start()
    {
        // Automatically cancelled when GameObject destroyed
        var ct = this.GetCancellationTokenOnDestroy();
        
        while (!ct.IsCancellationRequested)
        {
            await UniTask.Delay(1000, cancellationToken: ct);
            Patrol();
        }
    }
}

Linked Cancellation

public async UniTask DoWorkAsync(CancellationToken externalCt)
{
    // Link external token with destroy token
    var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        externalCt,
        this.GetCancellationTokenOnDestroy()
    );
    
    try
    {
        await LongRunningTask(linkedCts.Token);
    }
    finally
    {
        linkedCts.Dispose();
    }
}

Timeout

// Timeout with exception
await task.Timeout(TimeSpan.FromSeconds(5));

// Timeout without exception
var (hasValue, result) = await task.TimeoutWithoutException(TimeSpan.FromSeconds(5));
if (!hasValue)
{
    Debug.Log("Operation timed out");
}

// Timeout with CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await task.AttachExternalCancellation(cts.Token);

Unity Lifecycle Integration

Waiting for Frames

// Wait one frame
await UniTask.Yield();

// Wait for end of frame
await UniTask.WaitForEndOfFrame();

// Wait for fixed update
await UniTask.WaitForFixedUpdate();

// Wait multiple frames
await UniTask.DelayFrame(10);

// Wait for specific player loop
await UniTask.Yield(PlayerLoopTiming.PreUpdate);

Player Loop Timing Options

// Available timings:
PlayerLoopTiming.Initialization
PlayerLoopTiming.EarlyUpdate
PlayerLoopTiming.FixedUpdate
PlayerLoopTiming.PreUpdate
PlayerLoopTiming.Update
PlayerLoopTiming.PreLateUpdate
PlayerLoopTiming.PostLateUpdate
PlayerLoopTiming.TimeUpdate

Waiting for Conditions

// Wait until condition is true
await UniTask.WaitUntil(() => player.IsReady);

// Wait while condition is true
await UniTask.WaitWhile(() => isLoading);

// Wait until with cancellation
await UniTask.WaitUntil(
    () => player.IsReady,
    cancellationToken: ct
);

// Wait until with timeout
await UniTask.WaitUntil(() => player.IsReady)
    .Timeout(TimeSpan.FromSeconds(10));

Waiting for Unity Events

// Wait for button click
await button.OnClickAsync();

// Wait for trigger enter
var other = await gameObject.OnTriggerEnterAsync();

// Wait for collision
var collision = await gameObject.OnCollisionEnterAsync();

// Wait for animation event
await animator.WaitForAnimationEvent("AttackHit");

Parallel Operations

WhenAll (Wait for All)

// Wait for all tasks to complete
var results = await UniTask.WhenAll(
    LoadTexturesAsync(),
    LoadAudioAsync(),
    LoadConfigAsync()
);

// With different return types
var (textures, audio, config) = await UniTask.WhenAll(
    LoadTexturesAsync(),
    LoadAudioAsync(),
    LoadConfigAsync()
);

WhenAny (Wait for First)

// Wait for first to complete
var (winIndex, result1, result2) = await UniTask.WhenAny(
    TryServerAAsync(),
    TryServerBAsync()
);

// Use winner
if (winIndex == 0)
{
    UseResult(result1);
}

Throttling Parallel Operations

// Process with limited concurrency
var semaphore = new SemaphoreSlim(3); // Max 3 concurrent

await UniTask.WhenAll(items.Select(async item =>
{
    await semaphore.WaitAsync();
    try
    {
        await ProcessItemAsync(item);
    }
    finally
    {
        semaphore.Release();
    }
}));

Coroutine Interop

Convert Coroutine to UniTask

// Wrap existing coroutine
await MyCoroutine().ToUniTask();

// With cancellation
await MyCoroutine().ToUniTask(cancellationToken: ct);

// From IEnumerator
IEnumerator LegacyCoroutine()
{
    yield return new WaitForSeconds(1);
}

await LegacyCoroutine().ToUniTask();

Convert UniTask to Coroutine

// For APIs that require coroutines
StartCoroutine(MyUniTask().ToCoroutine());

Async Trigger Components

// Add async triggers to GameObjects
var trigger = gameObject.GetAsyncTriggerEnterTrigger();

// Wait for trigger
var other = await trigger.OnTriggerEnterAsync();

Resource Loading

Addressables Integration

public async UniTask<T> LoadAssetAsync<T>(string address, CancellationToken ct)
{
    var handle = Addressables.LoadAssetAsync<T>(address);
    
    try
    {
        return await handle.ToUniTask(cancellationToken: ct);
    }
    catch (OperationCanceledException)
    {
        Addressables.Release(handle);
        throw;
    }
}

Scene Loading

public async UniTask LoadSceneAsync(string sceneName, CancellationToken ct)
{
    await SceneManager.LoadSceneAsync(sceneName)
        .ToUniTask(cancellationToken: ct);
}

// With progress
public async UniTask LoadSceneWithProgressAsync(string sceneName, IProgress<float> progress)
{
    await SceneManager.LoadSceneAsync(sceneName)
        .ToUniTask(progress: progress);
}

Asset Bundle Loading

public async UniTask<AssetBundle> LoadBundleAsync(string url, CancellationToken ct)
{
    var request = UnityWebRequestAssetBundle.GetAssetBundle(url);
    
    await request.SendWebRequest().ToUniTask(cancellationToken: ct);
    
    if (request.result != UnityWebRequest.Result.Success)
    {
        throw new Exception(request.error);
    }
    
    return DownloadHandlerAssetBundle.GetContent(request);
}

Error Handling

Try-Catch Pattern

public async UniTask SafeLoadAsync()
{
    try
    {
        await LoadDataAsync();
    }
    catch (OperationCanceledException)
    {
        // Expected when cancelled - don't log as error
        Debug.Log("Load cancelled");
    }
    catch (Exception ex)
    {
        Debug.LogException(ex);
        ShowErrorUI();
    }
}

SuppressCancellationThrow

// Returns (isCancelled, result) instead of throwing
var (cancelled, result) = await LoadAsync()
    .SuppressCancellationThrow();

if (cancelled)
{
    return; // Graceful exit
}

ProcessResult(result);

Exception Handling in WhenAll

// Collect all exceptions
try
{
    await UniTask.WhenAll(tasks);
}
catch (AggregateException ae)
{
    foreach (var ex in ae.InnerExceptions)
    {
        Debug.LogException(ex);
    }
}

Progress Reporting

IProgress

public async UniTask LoadWithProgressAsync(IProgress<float> progress, CancellationToken ct)
{
    var items = await GetItemsAsync();
    
    for (int i = 0; i < items.Count; i++)
    {
        await ProcessItemAsync(items[i], ct);
        progress?.Report((float)(i + 1) / items.Count);
    }
}

// Usage
var progress = new Progress<float>(p => loadingBar.value = p);
await LoadWithProgressAsync(progress, ct);

Custom Progress

public struct LoadProgress
{
    public string CurrentItem;
    public int Loaded;
    public int Total;
    public float Percent => (float)Loaded / Total;
}

public async UniTask LoadAsync(IProgress<LoadProgress> progress)
{
    var items = await GetItemsAsync();
    
    for (int i = 0; i < items.Count; i++)
    {
        progress?.Report(new LoadProgress
        {
            CurrentItem = items[i].Name,
            Loaded = i + 1,
            Total = items.Count
        });
        
        await ProcessItemAsync(items[i]);
    }
}

UniTask vs Task

When to Use UniTask

// Unity main thread operations
await UniTask.Delay(1000);              // Use UniTask
await UniTask.Yield();                   // Use UniTask
await SceneManager.LoadSceneAsync(s);    // Use UniTask

// Fire and forget in Unity
DoWorkAsync().Forget();                  // UniTask only

When to Use Task

// Pure .NET operations (no Unity API)
await File.ReadAllTextAsync(path);       // Task is fine
await Task.Run(() => HeavyComputation()); // Task for thread pool

// Converting
var result = await task.AsUniTask();     // Task to UniTask
var task = unitask.AsTask();             // UniTask to Task

Common Pitfalls

Pitfall 1: Missing CancellationToken

// BAD: No cancellation, leaks when object destroyed
public async UniTaskVoid Start()
{
    await UniTask.Delay(10000);
    DoSomething(); // May crash if destroyed
}

// GOOD: Use destroy token
public async UniTaskVoid Start()
{
    await UniTask.Delay(10000, cancellationToken: destroyCancellationToken);
    DoSomething();
}

Pitfall 2: Forget Without Error Handling

// BAD: Exceptions silently swallowed
DoWorkAsync().Forget();

// GOOD: Handle errors
async UniTaskVoid DoWorkSafe()
{
    try
    {
        await DoWorkAsync();
    }
    catch (Exception ex) when (!(ex is OperationCanceledException))
    {
        Debug.LogException(ex);
    }
}
DoWorkSafe().Forget();

Pitfall 3: Blocking on Main Thread

// BAD: Blocks main thread
var result = LoadAsync().GetAwaiter().GetResult();

// GOOD: Await properly
var result = await LoadAsync();

Pitfall 4: Unnecessary Allocations

// BAD: Allocates closure
await UniTask.Delay(1000).ContinueWith(_ => DoSomething());

// GOOD: Just await
await UniTask.Delay(1000);
DoSomething();

Best Practices

  1. Always use CancellationToken for MonoBehaviour async methods
  2. Use GetCancellationTokenOnDestroy() for automatic cleanup
  3. Handle OperationCanceledException separately from other exceptions
  4. Prefer UniTask over Task for Unity operations
  5. Use WhenAll for parallel operations
  6. Report progress for long-running operations
  7. Use SuppressCancellationThrow for optional cancellation handling
  8. Avoid Forget() without proper error handling

Performance Tips

  • UniTask is struct-based (zero allocation when awaited directly)
  • Avoid unnecessary .AsTask() conversions
  • Use UniTask.Yield() instead of await UniTask.Delay(0)
  • Pool CancellationTokenSource for high-frequency operations
  • Use UniTaskCompletionSource for custom async patterns
Weekly Installs
1
GitHub Stars
1
First Seen
2 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1