Files
test/agent-skills/12-prefer-unitask-over-task.md
2026-02-24 18:45:24 +07:00

7.1 KiB

Prefer UniTask Over Task for Unity Async Operations

Why UniTask?

UniTask (Cysharp.Threading.Tasks) is a high-performance async/await library designed specifically for Unity. It provides significant advantages over standard .NET Task:

Performance Benefits

  • Zero allocation — No GC allocations for async operations (Task allocates ~200 bytes per await)
  • Unity-optimized — Built on Unity's PlayerLoop, integrates with Unity lifecycle
  • Better performance — Up to 10x faster than Task in Unity

Unity Integration

  • Cancellation tokens — Automatic cancellation when GameObject is destroyed
  • PlayerLoop integration — Runs on Unity's main thread by default
  • Coroutine-like behavior — Can await Unity operations (WaitForSeconds, etc.)
  • Better error handling — More Unity-friendly exception handling

Codebase Status

The project currently has mixed usage:

  • LitModelHolder.cs — Already uses UniTask
  • ⚠️ CECAttacksMan.cs — Uses System.Threading.Tasks
  • ⚠️ A3DSkillGfxComposerMan.cs — Uses System.Threading.Tasks
  • ⚠️ A3DSkillGfxMan.cs — Uses System.Threading.Tasks

Goal: Migrate all async code to UniTask over time.


When to Use UniTask vs Task

Use UniTask For:

  • All Unity async operations (loading assets, waiting, coroutines)
  • MonoBehaviour async methods (Start, Update, etc.)
  • Addressable loading (async asset loading)
  • Any code that runs in Unity's main thread

⚠️ Use Task Only For:

  • Pure .NET library code (no Unity dependencies)
  • Third-party APIs that return Task (wrap with ToUniTask())
  • Thread pool operations (use UniTask.RunOnThreadPool() instead)

Migration Patterns

Pattern 1: Method Signatures

Old (Task):

using System.Threading.Tasks;

public async Task<bool> Load(string path)
{
    await Task.Delay(100);
    return true;
}

New (UniTask):

using Cysharp.Threading.Tasks;

public async UniTask<bool> Load(string path)
{
    await UniTask.Delay(100);
    return true;
}

Pattern 2: Yield to Keep Unity Responsive

Old (Task):

// CECAttacksMan.cs line 117
await System.Threading.Tasks.Task.Yield();

New (UniTask):

await UniTask.Yield();  // Yields to Unity's main thread

Pattern 3: Addressable Loading

Old (Task):

public async Task<GameObject> LoadPrefabAsync(string path)
{
    var handle = Addressables.LoadAssetAsync<GameObject>(path);
    await handle.Task;  // Blocks, can cause freezing
    return handle.Result;
}

New (UniTask):

public async UniTask<GameObject> LoadPrefabAsync(string path)
{
    var handle = Addressables.LoadAssetAsync<GameObject>(path);
    await handle.ToUniTask();  // Non-blocking, Unity-friendly
    return handle.Result;
}

Pattern 4: Cancellation Tokens

Old (Task):

public async Task LoadAsync(CancellationToken ct)
{
    await Task.Delay(1000, ct);
}

New (UniTask):

public async UniTask LoadAsync(CancellationToken ct = default)
{
    // Auto-cancels when GameObject is destroyed if using GetCancellationTokenOnDestroy()
    await UniTask.Delay(1000, cancellationToken: ct);
}

// In MonoBehaviour:
private async void Start()
{
    var ct = this.GetCancellationTokenOnDestroy();  // Auto-cancels on destroy
    await LoadAsync(ct);
}

Pattern 5: Converting Task to UniTask

When calling third-party APIs that return Task:

// If you must call a Task-returning method:
Task<bool> result = SomeThirdPartyLibrary.DoSomethingAsync();
bool value = await result.ToUniTask();  // Convert to UniTask

Codebase-Specific Examples

Example 1: CECAttacksMan.cs

Current (line 74-121):

public async void LoadAllSkillGfxAsync()
{
    // ...
    await System.Threading.Tasks.Task.Yield();  // ❌ Should be UniTask.Yield()
}

Should be:

using Cysharp.Threading.Tasks;  // Add at top

public async void LoadAllSkillGfxAsync()
{
    // ...
    await UniTask.Yield();  // ✅ Better performance, Unity-integrated
}

Example 2: A3DSkillGfxComposerMan.cs

Current (line 40):

public async Task<bool> LoadOneComposerAsync(...)
{
    if (!await composer.Load(...))  // Task<bool>
        return false;
}

Should be:

using Cysharp.Threading.Tasks;  // Add at top

public async UniTask<bool> LoadOneComposerAsync(...)
{
    if (!await composer.Load(...))  // UniTask<bool>
        return false;
}

Example 3: A3DSkillGfxComposer.Load()

Current (line 502):

public async Task<bool> Load(SkillStub skillStub, ...)
{
    flyGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName);
}

Should be:

using Cysharp.Threading.Tasks;  // Add at top

public async UniTask<bool> Load(SkillStub skillStub, ...)
{
    flyGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName);
    // If LoadPrefabAsync returns Task, convert:
    // flyGFX = await AddressableManager.Instance.LoadPrefabAsync(...).ToUniTask();
}

Rules for New Code

  1. Always use UniTask for new async methods in Unity code
  2. Add using Cysharp.Threading.Tasks; at the top of files with async code
  3. Use UniTask.Yield() instead of Task.Yield() to keep Unity responsive
  4. Use UniTask.Delay() instead of Task.Delay() for delays
  5. Use GetCancellationTokenOnDestroy() in MonoBehaviour for auto-cancellation
  6. Convert Task to UniTask when calling third-party APIs: .ToUniTask()

Migration Priority

When refactoring existing code:

  1. High Priority — Methods called frequently (Update loops, loading)
  2. Medium Priority — One-time initialization methods
  3. Low Priority — Rarely-called utility methods

Don't break working code — migrate incrementally, test after each change.


Common Mistakes to Avoid

Don't Mix Task and UniTask

// BAD: Mixing Task and UniTask
public async Task<bool> Load()
{
    await UniTask.Delay(100);  // ❌ Task method using UniTask
    return true;
}

Don't Use Task.Yield() in Unity

// BAD: Task.Yield() doesn't integrate with Unity's main thread
await Task.Yield();  // ❌ May not yield to Unity properly

Do Use UniTask Consistently

// GOOD: Consistent UniTask usage
public async UniTask<bool> Load()
{
    await UniTask.Delay(100);  // ✅ Proper Unity integration
    return true;
}

References


Quick Reference

Task API UniTask Equivalent
Task UniTask
Task<T> UniTask<T>
Task.Yield() UniTask.Yield()
Task.Delay(ms) UniTask.Delay(ms)
Task.Run(action) UniTask.RunOnThreadPool(action)
CancellationToken CancellationToken (same, but use GetCancellationTokenOnDestroy())
task.ToUniTask() Convert Task → UniTask