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

284 lines
7.1 KiB
Markdown

# 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):**
```csharp
using System.Threading.Tasks;
public async Task<bool> Load(string path)
{
await Task.Delay(100);
return true;
}
```
**✅ New (UniTask):**
```csharp
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):**
```csharp
// CECAttacksMan.cs line 117
await System.Threading.Tasks.Task.Yield();
```
**✅ New (UniTask):**
```csharp
await UniTask.Yield(); // Yields to Unity's main thread
```
### Pattern 3: Addressable Loading
**❌ Old (Task):**
```csharp
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):**
```csharp
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):**
```csharp
public async Task LoadAsync(CancellationToken ct)
{
await Task.Delay(1000, ct);
}
```
**✅ New (UniTask):**
```csharp
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:**
```csharp
// 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):**
```csharp
public async void LoadAllSkillGfxAsync()
{
// ...
await System.Threading.Tasks.Task.Yield(); // ❌ Should be UniTask.Yield()
}
```
**Should be:**
```csharp
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):**
```csharp
public async Task<bool> LoadOneComposerAsync(...)
{
if (!await composer.Load(...)) // Task<bool>
return false;
}
```
**Should be:**
```csharp
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):**
```csharp
public async Task<bool> Load(SkillStub skillStub, ...)
{
flyGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName);
}
```
**Should be:**
```csharp
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
```csharp
// 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
```csharp
// BAD: Task.Yield() doesn't integrate with Unity's main thread
await Task.Yield(); // ❌ May not yield to Unity properly
```
### ✅ Do Use UniTask Consistently
```csharp
// 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 |