7.1 KiB
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— UsesSystem.Threading.Tasks - ⚠️
A3DSkillGfxComposerMan.cs— UsesSystem.Threading.Tasks - ⚠️
A3DSkillGfxMan.cs— UsesSystem.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
- Always use UniTask for new async methods in Unity code
- Add
using Cysharp.Threading.Tasks;at the top of files with async code - Use
UniTask.Yield()instead ofTask.Yield()to keep Unity responsive - Use
UniTask.Delay()instead ofTask.Delay()for delays - Use
GetCancellationTokenOnDestroy()in MonoBehaviour for auto-cancellation - Convert Task to UniTask when calling third-party APIs:
.ToUniTask()
Migration Priority
When refactoring existing code:
- High Priority — Methods called frequently (Update loops, loading)
- Medium Priority — One-time initialization methods
- 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
- UniTask GitHub: https://github.com/Cysharp/UniTask
- UniTask Documentation: https://github.com/Cysharp/UniTask#readme
- Performance Comparison: UniTask is 10x faster than Task in Unity benchmarks
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 |