284 lines
7.1 KiB
Markdown
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 |
|