Async Operations Tutorial
Working with UniTask and Awaitable
Learn how to use NOPE-PRO with asynchronous operations using UniTask or Unity's built-in Awaitable system.
Prerequisites
Before starting, ensure you have:
- Enabled either
NOPE_UNITASK
orNOPE_AWAITABLE
inNOPE-PRO Settings Panel
inWindow
->NOPE-PRO
->Settings
or Project Settings - For UniTask: Installed the UniTask package
- For Awaitable: Using Unity 6 or newer
Choosing Your Async System
UniTask (Recommended for Unity 2021-2023)
1// In Project Settings → Player → Scripting Define Symbols
2NOPE_UNITASK
3
4// Your code
5using Cysharp.Threading.Tasks;
6
7public async UniTask<Result<Data, string>> LoadDataAsync() { }
Awaitable (Unity 6+)
1// In Project Settings → Player → Scripting Define Symbols
2NOPE_AWAITABLE
3
4// Your code
5using UnityEngine;
6
7public async Awaitable<Result<Data, string>> LoadDataAsync() { }
Basic Async Usage
Converting Sync to Async
1// Synchronous version
2public Result<GameData, string> LoadGameData(string filename)
3{
4 return Result.Of(
5 () => File.ReadAllText(filename),
6 ex => $"Failed to read file: {ex.Message}"
7 )
8 .Bind(json => ParseJson<GameData>(json));
9}
10
11// Async version with UniTask
12#if NOPE_UNITASK
13public async UniTask<Result<GameData, string>> LoadGameDataAsync(string filename)
14{
15 return await Result.Of(
16 async () => await ReadAllTextAsync(filename),
17 ex => $"Failed to read file: {ex.Message}"
18 )
19 .Bind(json => ParseJsonAsync<GameData>(json));
20}
21
22private async UniTask<string> ReadAllTextAsync(string filename)
23{
24 return await File.ReadAllTextAsync(filename);
25}
26#endif
27
28// Async version with Awaitable
29#if NOPE_AWAITABLE
30public async Awaitable<Result<GameData, string>> LoadGameDataAsync(string filename)
31{
32 return await Result.Of(
33 async () => await ReadAllTextAsync(filename),
34 ex => $"Failed to read file: {ex.Message}"
35 )
36 .Bind(json => ParseJson<GameData>(json));
37}
38
39private async Awaitable<string> ReadAllTextAsync(string filename)
40{
41 return await File.ReadAllTextAsync(filename);
42}
43#endif
Async Result Creation
1#if NOPE_UNITASK
2// From async operation
3public async UniTask<Result<User, string>> GetUserAsync(string userId)
4{
5 try
6 {
7 var response = await UnityWebRequest.Get($"api/users/{userId}")
8 .SendWebRequest()
9 .ToUniTask();
10
11 if (response.result != UnityWebRequest.Result.Success)
12 return $"Network error: {response.error}";
13
14 var user = JsonUtility.FromJson<User>(response.downloadHandler.text);
15 return user;
16 }
17 catch (Exception ex)
18 {
19 return $"Unexpected error: {ex.Message}";
20 }
21}
22
23// Using Result.Of with async
24public async UniTask<Result<User, string>> GetUserSafeAsync(string userId)
25{
26 return await Result.Of(
27 async () => {
28 var response = await UnityWebRequest.Get($"api/users/{userId}")
29 .SendWebRequest()
30 .ToUniTask();
31
32 if (response.result != UnityWebRequest.Result.Success)
33 throw new Exception(response.error);
34
35 return JsonUtility.FromJson<User>(response.downloadHandler.text);
36 },
37 ex => $"Failed to get user: {ex.Message}"
38 );
39}
40#endif
Async Chaining Operations
Map and Bind with Async
1public class GameService
2{
3 // Mix sync and async operations
4 public async UniTask<Result<GameSession, string>> StartGameSession(string playerId)
5 {
6 return await ValidatePlayerId(playerId) // Sync: Result<string, string>
7 .Bind(id => LoadPlayerAsync(id)) // Async: UniTask<Result<Player, string>>
8 .Map(player => CreateSessionAsync(player)) // Async: UniTask<GameSession>
9 .Ensure(session => ValidateSessionAsync(session),
10 "Session validation failed") // Async validation
11 .Tap(session => LogSessionStartAsync(session)); // Async side effect
12 }
13
14 // Sync validation
15 private Result<string, string> ValidatePlayerId(string playerId)
16 {
17 if (string.IsNullOrEmpty(playerId))
18 return Result<string, string>.Failure("Invalid player ID");
19 return Result<string, string>.Success(playerId);
20 }
21
22 // Async player loading
23 private async UniTask<Result<Player, string>> LoadPlayerAsync(string playerId)
24 {
25 await UniTask.Delay(100); // Simulate network delay
26
27 // Load from database/API
28 return new Player { Id = playerId, Name = "Player1" };
29 }
30
31 // Async session creation
32 private async UniTask<GameSession> CreateSessionAsync(Player player)
33 {
34 await UniTask.Delay(50);
35
36 return new GameSession
37 {
38 SessionId = Guid.NewGuid().ToString(),
39 Player = player,
40 StartTime = DateTime.Now
41 };
42 }
43
44 // Async validation
45 private async UniTask<bool> ValidateSessionAsync(GameSession session)
46 {
47 await UniTask.Delay(10);
48 return session.Player != null && !string.IsNullOrEmpty(session.SessionId);
49 }
50
51 // Async logging
52 private async UniTask LogSessionStartAsync(GameSession session)
53 {
54 await UniTask.Delay(5);
55 Debug.Log($"Session {session.SessionId} started for {session.Player.Name}");
56 }
57}
58#endif
Async Combination Patterns
1#if NOPE_UNITASK
2public class DataLoader
3{
4 // Parallel loading with error handling
5 public async UniTask<Result<GameData, string>> LoadAllGameDataAsync()
6 {
7 // Start all tasks in parallel
8 var configTask = LoadConfigAsync();
9 var assetsTask = LoadAssetsAsync();
10 var saveDataTask = LoadSaveDataAsync();
11
12 // Wait for all to complete
13 var results = await UniTask.WhenAll(configTask, assetsTask, saveDataTask);
14
15 // Combine results
16 return Result.CombineValues(results.Item1, results.Item2, results.Item3)
17 .Map(tuple => new GameData
18 {
19 Config = tuple.Item1,
20 Assets = tuple.Item2,
21 SaveData = tuple.Item3
22 });
23 }
24
25 // Sequential loading with dependencies
26 public async UniTask<Result<GameState, string>> InitializeGameAsync()
27 {
28 return await LoadConfigAsync()
29 .Bind(config => LoadAssetsForConfig(config)
30 .Map(assets => (config, assets)))
31 .Bind(tuple => LoadSaveDataWithAssets(tuple.assets)
32 .Map(saveData => new GameState
33 {
34 Config = tuple.config,
35 Assets = tuple.assets,
36 SaveData = saveData
37 }))
38 .EnableDebug("GameInitialization");
39 }
40}
41#endif
Error Handling in Async Operations
Cancellation Support
1#if NOPE_UNITASK
2public class CancellableOperations
3{
4 private CancellationTokenSource cts;
5
6 public async UniTask<Result<LevelData, string>> LoadLevelAsync(string levelId)
7 {
8 cts?.Cancel();
9 cts = new CancellationTokenSource();
10
11 try
12 {
13 return await LoadLevelInternalAsync(levelId, cts.Token)
14 .EnableDebug($"LoadLevel_{levelId}");
15 }
16 catch (OperationCanceledException)
17 {
18 return "Level loading was cancelled";
19 }
20 }
21
22 private async UniTask<Result<LevelData, string>> LoadLevelInternalAsync(
23 string levelId,
24 CancellationToken cancellationToken)
25 {
26 // Check cancellation before each step
27 cancellationToken.ThrowIfCancellationRequested();
28
29 var manifest = await LoadManifestAsync(levelId);
30 if (manifest.IsFailure)
31 return manifest.Error;
32
33 cancellationToken.ThrowIfCancellationRequested();
34
35 var assets = await LoadAssetsAsync(manifest.Value, cancellationToken);
36 if (assets.IsFailure)
37 return assets.Error;
38
39 cancellationToken.ThrowIfCancellationRequested();
40
41 return new LevelData
42 {
43 Id = levelId,
44 Manifest = manifest.Value,
45 Assets = assets.Value
46 };
47 }
48
49 public void CancelLoading()
50 {
51 cts?.Cancel();
52 }
53}
54#endif
Retry with Async
1#if NOPE_UNITASK
2public static class AsyncRetryExtensions
3{
4 public static async UniTask<Result<T, E>> RetryAsync<T, E>(
5 this Func<UniTask<Result<T, E>>> operation,
6 int maxAttempts,
7 float delaySeconds,
8 Func<E, bool> shouldRetry = null)
9 {
10 Result<T, E> result = default;
11
12 for (int attempt = 1; attempt <= maxAttempts; attempt++)
13 {
14 result = await operation();
15
16 if (result.IsSuccess)
17 return result;
18
19 if (shouldRetry != null && !shouldRetry(result.Error))
20 return result;
21
22 if (attempt < maxAttempts)
23 {
24 Debug.Log($"Attempt {attempt} failed, retrying in {delaySeconds}s...");
25 await UniTask.Delay(TimeSpan.FromSeconds(delaySeconds));
26 }
27 }
28
29 return result;
30 }
31}
32
33// Usage
34public async UniTask<Result<ServerData, string>> FetchDataWithRetry()
35{
36 return await AsyncRetryExtensions.RetryAsync(
37 operation: FetchFromServerAsync,
38 maxAttempts: 3,
39 delaySeconds: 1.0f,
40 shouldRetry: error => !error.Contains("401") // Don't retry auth errors
41 );
42}
43#endif
Real-World Examples
Example 1: Save System with Cloud Sync
1#if NOPE_UNITASK
2public class CloudSaveManager : MonoBehaviour
3{
4 private bool isOnline = true;
5
6 public async UniTask SaveGameAsync(SaveData data)
7 {
8 await SerializeSaveData(data)
9 .EnableDebug("SaveGame")
10 .Bind(json => SaveLocallyAsync(json))
11 .Bind(json => isOnline
12 ? SyncToCloudAsync(json)
13 : UniTask.FromResult(Result<string, string>.Success(json)))
14 .Match(
15 onSuccess: _ => { ShowSaveSuccessUI(); return Unit.Value; },
16 onFailure: error => { ShowSaveErrorUI(error); return Unit.Value; }
17 );
18 }
19
20 private Result<string, string> SerializeSaveData(SaveData data)
21 {
22 return Result.Of(
23 () => JsonUtility.ToJson(data),
24 ex => $"Serialization failed: {ex.Message}"
25 );
26 }
27
28 private async UniTask<Result<string, string>> SaveLocallyAsync(string json)
29 {
30 try
31 {
32 var path = Path.Combine(Application.persistentDataPath, "save.json");
33 await File.WriteAllTextAsync(path, json);
34 return Result<string, string>.Success(json);
35 }
36 catch (Exception ex)
37 {
38 return Result<string, string>.Failure($"Local save failed: {ex.Message}");
39 }
40 }
41
42 private async UniTask<Result<string, string>> SyncToCloudAsync(string json)
43 {
44 return await AsyncRetryExtensions.RetryAsync(
45 operation: () => UploadToCloudAsync(json),
46 maxAttempts: 3,
47 delaySeconds: 2.0f
48 );
49 }
50
51 private void ShowSaveSuccessUI() => Debug.Log("Save successful!");
52 private void ShowSaveErrorUI(string error) => Debug.LogError($"Save failed: {error}");
53 private UniTask<Result<string, string>> UploadToCloudAsync(string json)
54 {
55 Debug.Log("Uploading to cloud...");
56 return UniTask.FromResult(Result<string, string>.Success(json));
57 }
58}
59#endif
Example 2: Resource Loading Pipeline
1#if NOPE_UNITASK
2public class ResourceManager : MonoBehaviour
3{
4 private readonly Dictionary<string, object> cache = new();
5
6 public async UniTask<Result<T, string>> LoadResourceAsync<T>(string path)
7 where T : UnityEngine.Object
8 {
9 return await CheckCache<T>(path)
10 .OrElse(() => LoadFromStreamingAssets<T>(path))
11 .OrElse(() => LoadFromResources<T>(path))
12 .OrElse(() => DownloadFromServer<T>(path))
13 .Tap(resource => CacheResource(path, resource))
14 .EnableDebug($"LoadResource_{path}");
15 }
16
17 private Result<T, string> CheckCache<T>(string path) where T : UnityEngine.Object
18 {
19 if (cache.TryGetValue(path, out var cached) && cached is T typedCache)
20 {
21 Debug.Log($"Loaded {path} from cache");
22 return typedCache;
23 }
24 return "Not in cache";
25 }
26
27 private async UniTask<Result<T, string>> LoadFromStreamingAssets<T>(string path)
28 where T : UnityEngine.Object
29 {
30 var fullPath = Path.Combine(Application.streamingAssetsPath, path);
31
32 if (!File.Exists(fullPath))
33 return "Not in streaming assets";
34
35 try
36 {
37 var assetBundle = await AssetBundle.LoadFromFileAsync(fullPath);
38 if (assetBundle == null)
39 {
40 return "Failed to load AssetBundle.";
41 }
42
43 var assetRequest = assetBundle.LoadAssetAsync<T>(Path.GetFileNameWithoutExtension(path));
44 await assetRequest;
45 var loadedAsset = assetRequest.asset;
46
47 assetBundle.Unload(false);
48
49 if (loadedAsset == null)
50 {
51 return "Asset not found in bundle";
52 }
53
54 return Result<T, string>.Success((T)loadedAsset);
55 }
56 catch (Exception ex)
57 {
58 return $"Streaming assets load failed: {ex.Message}";
59 }
60 }
61
62 private async UniTask<Result<T, string>> LoadFromResources<T>(string path)
63 where T : UnityEngine.Object
64 {
65 var request = Resources.LoadAsync<T>(path);
66 await request.ToUniTask();
67
68 return request.asset != null
69 ? Result<T, string>.Success((T)request.asset)
70 : "Not in Resources";
71 }
72
73 private async UniTask<Result<T, string>> DownloadFromServer<T>(string path)
74 where T : UnityEngine.Object
75 {
76 // Implementation depends on your server setup
77 await UniTask.Delay(1000);
78 return "Server download not implemented";
79 }
80
81 private async UniTask CacheResource<T>(string path, T resource)
82 {
83 cache[path] = resource;
84 await UniTask.Yield(); // Ensure we don't block
85 }
86}
87#endif
Example 3: Complex Game Flow
1#if NOPE_UNITASK
2public class GameFlowController : MonoBehaviour
3{
4 public async UniTask StartNewGameAsync(string playerName)
5 {
6 var flow = await CreateNewPlayer(playerName)
7 .EnableDebug("NewGameFlow")
8 .Bind(player => SelectStartingClass(player))
9 .Bind(player => AllocateStartingResources(player))
10 .Bind(player => GenerateStartingWorld(player))
11 .Tap(player => SavePlayerProgressAsync(player))
12 .Bind(player => LoadFirstLevel(player))
13 .Finally(async result => {
14 if (result.IsFailure)
15 {
16 await ShowErrorAndReturnToMenuAsync(result.Error);
17 }
18 return result;
19 });
20 }
21
22 private Result<Player, string> CreateNewPlayer(string name)
23 {
24 if (string.IsNullOrWhiteSpace(name))
25 return "Player name cannot be empty";
26
27 if (name.Length < 3 || name.Length > 20)
28 return "Player name must be 3-20 characters";
29
30 return new Player
31 {
32 Id = Guid.NewGuid().ToString(),
33 Name = name,
34 Level = 1,
35 Experience = 0
36 };
37 }
38
39 private async UniTask<Result<Player, string>> SelectStartingClass(Player player)
40 {
41 var classSelection = await ShowClassSelectionUIAsync();
42
43 if (classSelection.IsCancelled)
44 return "Player cancelled class selection";
45
46 player.Class = classSelection.SelectedClass;
47 player.Stats = GetStartingStatsForClass(classSelection.SelectedClass);
48
49 return player;
50 }
51
52 private async UniTask<Result<Player, string>> AllocateStartingResources(Player player)
53 {
54 await UniTask.Delay(500); // Simulate allocation
55
56 player.Inventory = new Inventory
57 {
58 Gold = 100,
59 Items = GetStartingItemsForClass(player.Class)
60 };
61
62 return player;
63 }
64
65 private async UniTask<Result<Player, string>> GenerateStartingWorld(Player player)
66 {
67 try
68 {
69 var worldGen = new WorldGenerator();
70 player.WorldSeed = UnityEngine.Random.Range(0, int.MaxValue);
71
72 await worldGen.GenerateAsync(player.WorldSeed)
73 .WithTimeout(30.0f);
74
75 return player;
76 }
77 catch (TimeoutException)
78 {
79 return "World generation timed out";
80 }
81 catch (Exception ex)
82 {
83 return $"World generation failed: {ex.Message}";
84 }
85 }
86
87 private async UniTask<Result<Player, string>> LoadFirstLevel(Player player)
88 {
89 var levelId = GetStartingLevelForClass(player.Class);
90
91 return await LoadLevelAsync(levelId)
92 .Map(_ => {
93 SpawnPlayer(player);
94 ShowTutorial(player.Class);
95 return player;
96 });
97 }
98}
99#endif
Performance Considerations
Avoid Unnecessary Allocations
1#if NOPE_UNITASK
2// Bad - creates new UniTask for sync operation
3public async UniTask<Result<int, string>> CalculateAsync(int x, int y)
4{
5 return await UniTask.FromResult(Result<int, string>.Success(x + y));
6}
7
8// Good - return sync result directly
9public Result<int, string> Calculate(int x, int y)
10{
11 return x + y;
12}
13
14// Good - only use async when needed
15public async UniTask<Result<Data, string>> ProcessAsync(string input)
16{
17 var parsed = ParseInput(input); // Sync operation
18 if (parsed.IsFailure)
19 return parsed.Error;
20
21 return await FetchDataAsync(parsed.Value); // Actual async operation
22}
23#endif
Batch Async Operations
1#if NOPE_UNITASK
2public class BatchProcessor
3{
4 // Process items in parallel batches
5 public async UniTask<Result<List<ProcessedItem>, string>> ProcessItemsAsync(
6 List<Item> items,
7 int batchSize = 10)
8 {
9 var results = new List<ProcessedItem>();
10 var errors = new List<string>();
11
12 for (int i = 0; i < items.Count; i += batchSize)
13 {
14 var batch = items.Skip(i).Take(batchSize);
15 var batchTasks = batch.Select(item => ProcessItemAsync(item));
16
17 var batchResults = await UniTask.WhenAll(batchTasks);
18
19 foreach (var result in batchResults)
20 {
21 result.Match(
22 onSuccess: item => { results.Add(item); return Unit.Value; },
23 onFailure: error => { errors.Add(error); return Unit.Value; }
24 );
25 }
26 }
27
28 return errors.Any()
29 ? $"Failed to process {errors.Count} items: {string.Join(", ", errors.Take(3))}"
30 : results;
31 }
32
33 private async UniTask<Result<ProcessedItem, string>> ProcessItemAsync(Item item)
34 {
35 try
36 {
37 // Simulate processing
38 await UniTask.Delay(100);
39 return new ProcessedItem { Name = item.Name, Value = item.Value };
40 }
41 catch (Exception ex)
42 {
43 return $"Error processing item {item.Name}: {ex.Message}";
44 }
45 }
46}
47#endif
Testing Async Code
1#if NOPE_UNITASK && UNITY_INCLUDE_TESTS
2using NUnit.Framework;
3using Cysharp.Threading.Tasks;
4
5public class AsyncResultTests
6{
7 [Test]
8 public async UniTask TestAsyncChaining()
9 {
10 var result = await LoadDataAsync()
11 .Bind(data => ProcessDataAsync(data))
12 .Map(processed => processed.ToString());
13
14 Assert.IsTrue(result.IsSuccess);
15 Assert.AreEqual("Expected output", result.Value);
16 }
17
18 [Test]
19 public async UniTask TestAsyncErrorHandling()
20 {
21 var result = await FailingOperationAsync()
22 .MapError(error => $"Handled: {error}")
23 .OrElse(() => FallbackOperationAsync());
24
25 Assert.IsTrue(result.IsSuccess);
26 }
27
28 [Test]
29 public async UniTask TestAsyncTimeout()
30 {
31 var result = await LongRunningOperationAsync()
32 .WithTimeout(0.1f);
33
34 Assert.IsTrue(result.IsFailure);
35 Assert.IsTrue(result.Error.Contains("timed out"));
36 }
37}
38#endif
Summary
You've learned:
- ✅ How to enable and use async support in NOPE-PRO
- ✅ Converting between sync and async operations
- ✅ Chaining async operations with proper error handling
- ✅ Implementing timeout and cancellation
- ✅ Building real-world async systems
- ✅ Performance best practices