Unity异步编程实战:用UniTask管理复杂工作流,告别回调地狱
2026/8/6 2:21:17 网站建设 项目流程

1. 项目概述:为什么Unity开发者需要UniTask来管理复杂异步工作流?

如果你在Unity里做过稍微复杂一点的逻辑,比如一个角色从A点跑到B点,途中需要等待一个对话框弹出、玩家做出选择、然后播放一段过场动画、最后再加载下一个场景的资源,你大概率已经体会过“回调地狱”或者“协程嵌套”的酸爽了。传统的Coroutine(协程)配合yield return,代码写起来一层套一层,可读性差,错误处理更是噩梦。而原生的C#Task在Unity的MonoBehaviour生命周期和单线程主循环里又有点水土不服,用起来束手束脚。

这就是UniTask出现的背景。它不是一个简单的语法糖,而是一套为Unity量身定制的、高性能的异步/等待(async/await)解决方案。它深度整合了Unity的PlayerLoop,让你能用写同步代码一样清晰的逻辑,去处理那些 inherently asynchronous(天生异步)的游戏操作。今天要聊的“复杂异步工作流的顺序执行”,正是UniTask最能大显身手的场景。这不仅仅是“一个接一个”执行那么简单,它关乎资源加载、UI交互、动画播放、网络请求等多个离散异步操作如何优雅、可靠、高效地串联成一个完整的业务流程。

想象一下新手引导、关卡序列、剧情对话树或者一个复杂的合成系统,这些都需要严格的顺序控制。用UniTask,你可以把这些工作流写得像一本清晰的剧本,而不是一堆散落的纸条。

2. 核心思路:从“回调地狱”到“声明式工作流”

在深入代码之前,我们先理清思路。处理异步顺序执行,传统上有几种方式:

2.1 传统方式的困境

  • 回调函数(Callback Hell):一个操作完成时调用下一个操作的回调。当链条变长时,代码向右无限延伸,难以阅读和维护,错误传播更是灾难。
  • 协程嵌套(Coroutine Nesting):通过StartCoroutine在一个协程里启动另一个协程。这改善了结构,但依然存在yield return等待的局限性,且取消操作和异常处理非常笨拙。
  • 状态机(State Machine):手动维护一个枚举状态,在Update里切换。这是最底层的方式,复杂且容易出错。

2.2 UniTask带来的范式转变UniTask引入了基于async/await的声明式编程模型。你不再关心“如何等待”,而是声明“等待什么”。顺序执行的核心思路变得极其直观:

  1. 将每个异步操作封装成一个返回UniTask的方法。例如,UniTask LoadSceneAsync(string name),UniTask PlayCutsceneAsync(int id)
  2. 使用await关键字顺序调用它们await会挂起当前方法的执行,直到等待的UniTask完成,然后继续执行下一行。从代码上看,它就是顺序执行的。
  3. 利用UniTask提供的丰富组合器(Combinators)来处理更复杂的流程,比如UniTask.WhenAll(并行)、UniTask.WhenAny(竞速)、以及我们今天重点的UniTask.Sequence或直接await链式调用。

这种方式的巨大优势在于:

  • 可读性:代码顺序就是执行顺序,一目了然。
  • 可维护性:每个步骤都是独立的方法,易于复用和测试。
  • 强大的错误处理:可以使用熟悉的try-catch-finally来捕获整个工作流中任何步骤的异常。
  • 便捷的取消操作:通过CancellationToken可以轻松取消整个异步链。
  • 零或低GC开销UniTask是值类型(struct),设计上就避免了托管堆分配,对于性能敏感的Unity应用至关重要。

3. 基础构建:将一切封装为UniTask

在构建工作流之前,我们必须把Unity中各种异步操作“翻译”成UniTaskUniTask已经为大多数常见操作提供了开箱即用的扩展方法。

3.1 资源加载Unity原生的ResourceRequestAddressables/AssetBundle的异步加载,都可以轻松转换。

using Cysharp.Threading.Tasks; using UnityEngine; public class ResourceLoader { // 1. 使用 Resources.LoadAsync (传统Resources系统) public static async UniTask<GameObject> LoadPrefabAsync(string path) { // ToUniTask() 将 AsyncOperation 转换为 UniTask var request = Resources.LoadAsync<GameObject>(path); await request.ToUniTask(); return (GameObject)request.asset; } // 2. 使用 Addressables (推荐) public static async UniTask<GameObject> LoadAddressableAsync(string key) { // Addressables.LoadAssetAsync 本身返回 AsyncOperationHandle, // UniTask 有专门的扩展方法 WaitAsync() 或 ToUniTask() var handle = Addressables.LoadAssetAsync<GameObject>(key); // 使用 WithCancellation 支持取消 var obj = await handle.ToUniTask(); // 注意:Addressables需要管理释放,这里简化了。实际应考虑使用UniTask的返回值或封装生命周期。 return obj; } }

注意AddressablesAsyncOperationHandle转换为UniTask后,await完成并不代表资源可以立即释放。你需要管理handle的生命周期,通常在依赖该资源的对象销毁时调用Addressables.Release(handle)。一个常见的模式是将handle与某个MonoBehaviour的生命周期绑定。

3.2 延时与帧控制替代yield return new WaitForSeconds(2)yield return null

public async UniTask MyTimedRoutine() { Debug.Log("开始等待"); // 等待2秒 await UniTask.Delay(TimeSpan.FromSeconds(2f)); // 或 UniTask.Delay(2000) 毫秒 Debug.Log("2秒后"); // 等待下一帧 await UniTask.NextFrame(); Debug.Log("下一帧"); // 等待直到某个条件满足 (替代 yield return new WaitUntil) await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space)); Debug.Log("空格键被按下"); // 在 FixedUpdate 中等待 await UniTask.WaitForFixedUpdate(); }

UniTask.Delay是基于时间的可靠等待,不受Time.timeScale影响(除非使用ignoreTimeScale: false参数)。UniTask.NextFrameyield return null更轻量。

3.3 动画与Tween等待一个Animation播放完毕或一个DOTween动画完成。

using DG.Tweening; // 假设使用DOTween public async UniTask PlayAnimationsAsync(Animator animator, Transform target) { // 等待Animator状态播放完毕(简化示例,实际需根据状态名或哈希) animator.Play("Run"); // 使用 UniTask.WaitUntil 检查动画是否播放完毕。更严谨的做法是订阅动画事件或使用AnimatorStateInfo。 await UniTask.WaitUntil(() => animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f); // 等待DOTween动画完成 await target.DOMove(new Vector3(5, 0, 0), 1f).AsyncWaitForCompletion(); // DOTween的 AsyncWaitForCompletion() 返回的是一个可等待的 YieldInstruction 封装, // UniTask 可以很好地 await 它。也可以使用 ToUniTask() 扩展。 }

3.4 UI交互(等待按钮点击)这是UniTask非常强大的一个场景,可以优雅地等待用户输入。

using UnityEngine.UI; public class DialogueUI : MonoBehaviour { public Button confirmButton; public Button cancelButton; // 返回一个UniTask,当按钮被点击时完成,并返回点击的是哪个按钮 public async UniTask<bool> WaitForPlayerChoiceAsync() { // 使用 UniTaskCompletionSource 手动控制一个 UniTask 的完成 var utcs = new UniTaskCompletionSource<bool>(); // 临时注册监听器 void OnConfirm() => utcs.TrySetResult(true); void OnCancel() => utcs.TrySetResult(false); confirmButton.onClick.AddListener(OnConfirm); cancelButton.onClick.AddListener(OnCancel); try { // 等待直到其中一个按钮被点击,结果即为true(确认)或false(取消) bool result = await utcs.Task; return result; } finally { // 无论成功还是异常,都要移除监听,避免内存泄漏 confirmButton.onClick.RemoveListener(OnConfirm); cancelButton.onClick.RemoveListener(OnCancel); } } }

这个模式是处理一次性异步事件(如点击、碰撞、消息)的通用模板。UniTaskCompletionSource是你手动创建和控制UniTask生命周期的关键工具。

3.5 网络请求使用UnityWebRequest或其它HTTP客户端。

using UnityEngine.Networking; public async UniTask<string> FetchDataFromServerAsync(string url) { using (var request = UnityWebRequest.Get(url)) { // SendWebRequest 返回一个 AsyncOperation, 用 ToUniTask 等待 await request.SendWebRequest().ToUniTask(); if (request.result == UnityWebRequest.Result.Success) { return request.downloadHandler.text; } else { // 直接抛出异常,可以在上层统一捕获 throw new System.Exception($"网络请求失败: {request.error}"); } } }

4. 核心实践:构建顺序执行的工作流

有了这些基础的UniTask构建块,我们就可以像搭积木一样组装复杂的工作流了。核心就是简单的await链。

4.1 基础顺序链最直观的方式,一行一个await

public async UniTask ExecuteMainStorylineAsync() { Debug.Log("=== 主线剧情开始 ==="); // 1. 加载场景和必要资源 await SceneManager.LoadSceneAsync("Village").ToUniTask(); var hero = await ResourceLoader.LoadAddressableAsync("HeroPrefab"); Instantiate(hero, spawnPoint.position, Quaternion.identity); // 2. 播放开场动画 await PlayCutsceneAsync(101); // 3. 等待NPC对话并显示选择支 var dialogueResult = await dialogueUI.ShowDialogueAsync("OldMan", "Welcome to the village..."); if (dialogueResult == DialogueResult.AcceptQuest) { // 4. 如果接受任务,播放特定动画并更新任务状态 await PlayAnimationAsync("QuestAccepted"); questSystem.AcceptQuest(1); } // 5. 无论是否接受,都延时几秒后进入自由探索 await UniTask.Delay(3000); Debug.Log("=== 进入自由探索模式 ==="); // 工作流结束 }

这段代码清晰地描述了一个剧情流程。任何一步出错(比如资源加载失败),异常会向上抛出,你可以用try-catch包裹整个ExecuteMainStorylineAsync来统一处理。

4.2 使用UniTask.Void处理“发后即忘”有些步骤你可能不想等待,但又需要它开始执行。可以用UniTask.Void或直接忽略await(不推荐,因为异常会被吞掉)。

public async UniTask StartGameWithEffectsAsync() { // 播放背景音乐(不等待其结束) _ = audioManager.PlayBGMAsync("MainTheme").Forget(); // Forget() 是 UniTask.Void 的别名 // 同时播放一个开场Logo特效,但也不等待 _ = PlayLogoEffectAsync(); // 等待核心资源加载(这是我们必须等待的) await LoadCoreAssetsAsync(); // 核心资源加载完后,继续主逻辑... }

实操心得:对于“发后即忘”的任务,务必使用.Forget()或将其赋值给一个_(丢弃变量)。这有两个好处:一是明确表达了开发者“不关心其结果”的意图;二是如果该异步方法内部发生异常,UniTask会将其发布到UniTaskScheduler.UnobservedTaskException,你可以在全局设置一个处理器来记录这些未被观察的异常,避免静默失败。直接调用而不await或不处理返回值,异常会被吞没,极难调试。

4.3 错误处理与取消这是生产级代码必须考虑的。

public async UniTask<bool> TryExecuteTutorialAsync(CancellationToken cancellationToken = default) { try { // 将CancellationToken传递给每一个支持取消的内部异步操作 await ShowWelcomeMessageAsync(cancellationToken); await UniTask.Delay(1000, cancellationToken: cancellationToken); // Delay也支持取消 await GuidePlayerMovementAsync(cancellationToken); // 如果cancellationToken在等待过程中被取消,会抛出OperationCanceledException return true; } catch (OperationCanceledException) { Debug.Log("教程被用户取消。"); // 执行一些清理工作,比如关闭打开的UI tutorialUI.CloseAll(); return false; } catch (System.Exception e) { Debug.LogError($"教程执行出错: {e.Message}"); // 可能弹出一个错误提示框 errorPopup.Show("Tutorial failed."); return false; } finally { // 无论成功、失败还是取消,finally块都会执行,适合做通用清理 Resources.UnloadUnusedAssets(); } } // 在某个MonoBehaviour中调用 private CancellationTokenSource _tutorialCts; private void StartTutorial() { _tutorialCts?.Cancel(); // 取消之前的(如果存在) _tutorialCts = new CancellationTokenSource(); _ = TryExecuteTutorialAsync(_tutorialCts.Token); // 启动新的 } private void OnPlayerPressedSkipButton() { _tutorialCts?.Cancel(); // 用户点击跳过,触发取消 }

关键点

  1. 传递CancellationToken:尽可能让你封装的异步方法接受一个CancellationToken参数,并传递给内部所有可取消的UniTask操作(如Delay,WaitUntil, 网络请求等)。这是实现协同取消的基础。
  2. 区分取消和错误OperationCanceledException是正常的取消流程,不应视为错误。其他Exception才是真正的异常。
  3. 管理CancellationTokenSource生命周期:谁创建,谁负责在适当的时候(如OnDestroy)调用Dispose()。通常将CTSMonoBehaviour的生命周期绑定。

4.4 组合使用:顺序中的并行一个复杂工作流中,某些步骤可以并行以提升性能。

public async UniTask LoadLevelAsync(int levelId) { // 1. 顺序:先显示加载界面 loadingScreen.Show(); // 2. 并行:同时加载场景、UI资源和背景音乐 var loadSceneTask = SceneManager.LoadSceneAsync($"Level_{levelId}").ToUniTask(); var loadUITask = LoadUIResourcesAsync(); var loadBGMTask = audioManager.LoadAudioClipAsync($"BGM_Level{levelId}"); // 使用 UniTask.WhenAll 等待所有并行任务完成 await UniTask.WhenAll(loadSceneTask, loadUITask, loadBGMTask); // 3. 顺序:全部加载完后,隐藏加载界面并播放一个入场效果 loadingScreen.Hide(); await PlayLevelEntranceEffectAsync(); Debug.Log("关卡加载完成!"); }

UniTask.WhenAll返回一个新的UniTask,当所有传入的任务都完成时,它才完成。如果其中任何一个任务失败(抛出异常),WhenAll也会立即失败,并抛出那个异常(或AggregateException)。这保证了错误能及时传递。

5. 高级模式与架构设计

当工作流变得非常复杂,比如一个包含多个阶段、每个阶段又有分支和循环的RPG任务系统时,简单的await链会变得难以维护。我们需要更结构化的设计。

5.1 状态机与UniTask结合将工作流的每个步骤抽象为一个状态(State),每个状态是一个返回UniTask的方法。由一个状态机控制器来驱动状态的转换。

public interface IGameState { UniTask OnEnterState(); UniTask OnExecuteState(); UniTask OnExitState(); } public class DialogueState : IGameState { private DialogueUI _ui; public async UniTask OnEnterState() { /* 显示对话框UI */ } public async UniTask OnExecuteState() { var choice = await _ui.WaitForPlayerChoiceAsync(); // 根据choice决定下一个状态... } public async UniTask OnExitState() { /* 隐藏对话框UI */ } } public class GameStateMachine { private IGameState _currentState; public async UniTask ChangeStateAsync(IGameState newState) { if (_currentState != null) { await _currentState.OnExitState(); } _currentState = newState; if (_currentState != null) { await _currentState.OnEnterState(); await _currentState.OnExecuteState(); // 执行状态逻辑,内部可能触发下一次状态变更 } } }

这种模式非常适合游戏流程管理,如战斗状态、菜单导航等。

5.2 使用UniTask.Lazy实现延迟执行与依赖管理有时,工作流中的某些步骤是昂贵的,但可能因为条件分支而永远不会被执行。我们可以用UniTask.Lazy来延迟创建UniTask

public class SmartAssetLoader { private Dictionary<string, UniTask<GameObject>> _assetCache = new(); public UniTask<GameObject> LoadAssetLazyAsync(string key) { if (!_assetCache.TryGetValue(key, out var task)) { // UniTask.Lazy 直到这个task被await时,内部的工厂方法才会被执行。 task = UniTask.Lazy(async () => { Debug.Log($"开始真正加载资源: {key}"); var obj = await Addressables.LoadAssetAsync<GameObject>(key).Task; return obj; }); _assetCache[key] = task; } return task; } } // 使用 public async UniTask MaybeLoadAssetAsync() { if (player.HasSpecialItem()) { // 只有满足条件时,才会触发实际的加载操作 var specialAsset = await loader.LoadAssetLazyAsync("SpecialEffect"); UseAsset(specialAsset); } // 否则,“SpecialEffect”资源永远不会被加载 }

这对于优化内存和加载时间非常有用,实现了按需加载。

5.3 自定义Awaiter实现特定等待条件虽然UniTask提供了丰富的等待方法,但有时你需要等待一些非常特定的事件。你可以通过实现INotifyCompletionICriticalNotifyCompletion接口来创建自定义的Awaiter。不过,更简单的方式是利用现有的UniTaskCompletionSource

例如,等待游戏内某个自定义事件:

public class GameEventAwaiter { private static Dictionary<string, UniTaskCompletionSource<bool>> _eventTasks = new(); public static UniTask WaitForEventAsync(string eventName) { if (!_eventTasks.ContainsKey(eventName)) { _eventTasks[eventName] = new UniTaskCompletionSource<bool>(); } return _eventTasks[eventName].Task; } public static void TriggerEvent(string eventName) { if (_eventTasks.TryGetValue(eventName, out var utcs)) { utcs.TrySetResult(true); // 可选:移除或创建新的CompletionSource以供下次等待 _eventTasks.Remove(eventName); } } } // 在工作流中等待 await GameEventAwaiter.WaitForEventAsync("EnemyBossDefeated"); Debug.Log("Boss被打败了,继续剧情!"); // 在Boss死亡的地方触发 void OnBossDeath() { GameEventAwaiter.TriggerEvent("EnemyBossDefeated"); }

6. 性能优化与陷阱规避

UniTask性能很好,但滥用或误用仍会导致问题。

6.1 避免在Update循环中频繁创建UniTaskUniTask是值类型,本身分配在栈上,GC压力小。但如果你在Update中频繁调用一个返回new UniTask的方法,或者频繁使用UniTask.Run(在线程池上调度),仍然会有开销。

// 不佳:每帧都创建一个新的UniTask void Update() { if (Input.GetKeyDown(KeyCode.Space)) { _ = DoSomethingAsync(); // 这会在每帧按键时都产生一个任务 } } // 更佳:使用状态标志或节流 private bool _isProcessing = false; async void Update() { if (Input.GetKeyDown(KeyCode.Space) && !_isProcessing) { _isProcessing = true; await DoSomethingAsync(); _isProcessing = false; } }

6.2 注意Unity对象生命周期与异步这是Unity异步编程中最常见的坑。在async方法中,你可能会在await之后尝试访问一个已经被销毁的Unity对象(如GameObjectMonoBehaviour)。

public class DangerousBehaviour : MonoBehaviour { public async UniTaskVoid DoSomethingAndDestroyAsync() { await UniTask.Delay(3000); // 等待3秒 // 危险!3秒内这个GameObject可能已经被Destroy了。 this.transform.position = Vector3.one; // 可能抛出 MissingReferenceException } private void OnDestroy() { // 如果异步操作还在进行,需要取消它 } }

解决方案

  1. 使用CancellationToken:将MonoBehaviourDestroyCancellationToken传递给所有异步方法。
    public async UniTaskVoid SafeCoroutineAsync() { // 使用this.GetCancellationTokenOnDestroy()获取与GameObject生命周期绑定的Token var ct = this.GetCancellationTokenOnDestroy(); try { await UniTask.Delay(3000, cancellationToken: ct); // 如果对象在等待期间被销毁,await会抛出OperationCanceledException,不会执行到这里。 this.transform.position = Vector3.one; } catch (OperationCanceledException) { // 正常取消,安静退出 } }
  2. 在await后检查对象是否存活
    await UniTask.Delay(3000); if (this == null) return; // 或者 if (!this.gameObject.activeInHierarchy) this.transform.position = Vector3.one;
    第一种方法(使用CancellationToken)更优雅、更可靠,是UniTask在Unity中的最佳实践之一。

6.3 处理异步方法中的异常async方法中抛出的异常,会在该方法返回的Task/UniTaskawait时重新抛出。如果这个UniTask没有被await(比如用Forget()了),异常默认会被吞没。

  • 全局异常处理:可以订阅UniTaskScheduler.UnobservedTaskException来记录所有未被观察的异常。
    UniTaskScheduler.UnobservedTaskException += (sender, e) => { Debug.LogError($"未观察到的异常: {e.Exception}"); };
  • 局部处理:对于重要的后台任务,即使不await,也尽量用ContinueWithSuppress来处理异常。
    DoRiskyOperationAsync().Forget(); // 异常可能丢失 DoRiskyOperationAsync().ContinueWith((task) => { if (task.Status == UniTaskStatus.Faulted) { Debug.LogError($"操作失败: {task.GetAwaiter().GetResult()}"); } }).Forget(); // 这样能捕获到异常

7. 实战:一个完整的关卡加载与初始化工作流示例

让我们将所有知识点串联起来,实现一个真实的关卡加载流程。

using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.SceneManagement; using System.Collections.Generic; using System.Threading; public class LevelManager : MonoBehaviour { [SerializeField] private LoadingScreen _loadingScreen; [SerializeField] private string _levelAddressableKey; [SerializeField] private string[] _preloadAssetKeys; private CancellationTokenSource _loadingCts; private List<AsyncOperationHandle> _assetHandles = new List<AsyncOperationHandle>(); public async UniTask<bool> LoadLevelAsync(int levelIndex) { // 0. 取消可能正在进行的上一次加载 _loadingCts?.Cancel(); _loadingCts = new CancellationTokenSource(); var ct = _loadingCts.Token; try { // 1. 显示加载界面 _loadingScreen.ShowProgress(0f); await UniTask.NextFrame(ct); // 确保UI更新一帧 // 2. 并行预加载常用资产(不阻塞主流程) var preloadTask = PreloadCommonAssetsAsync(ct); // 3. 加载关卡场景(主耗时操作) _loadingScreen.UpdateStatus("正在加载场景..."); var sceneLoadOp = Addressables.LoadSceneAsync($"Levels/Level_{levelIndex}", LoadSceneMode.Single, false); // 不自动激活 _assetHandles.Add(sceneLoadOp); // 将加载进度反馈给UI while (!sceneLoadOp.IsDone && !ct.IsCancellationRequested) { await UniTask.Yield(ct); _loadingScreen.UpdateProgress(sceneLoadOp.PercentComplete * 0.7f); // 场景加载占70%进度 } ct.ThrowIfCancellationRequested(); // 如果取消,抛出异常 // 4. 等待预加载完成 _loadingScreen.UpdateStatus("正在加载资源..."); await preloadTask; _loadingScreen.UpdateProgress(0.85f); // 5. 加载关卡专属资源 var levelSpecificTask = LoadLevelSpecificAssetsAsync(ct); await levelSpecificTask; _loadingScreen.UpdateProgress(0.95f); // 6. 激活场景(瞬间完成) var sceneInstance = sceneLoadOp.Result; await sceneInstance.ActivateAsync().ToUniTask(cancellationToken: ct); _loadingScreen.UpdateProgress(1.0f); await UniTask.Delay(300, cancellationToken: ct); // 短暂停留,让玩家看到100% // 7. 隐藏加载界面,播放入场效果 _loadingScreen.Hide(); await PlayLevelEntranceEffectAsync(ct); Debug.Log($"关卡 {levelIndex} 加载完成!"); return true; } catch (OperationCanceledException) { Debug.Log("关卡加载被取消。"); Cleanup(); return false; } catch (System.Exception e) { Debug.LogError($"关卡加载失败: {e}"); _loadingScreen.ShowError("加载失败,请重试。"); Cleanup(); return false; } } private async UniTask PreloadCommonAssetsAsync(CancellationToken ct) { var tasks = new List<UniTask>(); foreach (var key in _preloadAssetKeys) { // 使用Lazy或直接加载,这里假设直接加载 var handle = Addressables.LoadAssetAsync<GameObject>(key); _assetHandles.Add(handle); tasks.Add(handle.ToUniTask().AttachExternalCancellation(ct)); } // 并行加载所有通用资源 await UniTask.WhenAll(tasks); } private async UniTask LoadLevelSpecificAssetsAsync(CancellationToken ct) { // 加载关卡配置、敌人数据等 var configTask = Addressables.LoadAssetAsync<LevelConfig>($"{_levelAddressableKey}_Config").ToUniTask().AttachExternalCancellation(ct); // ... 其他加载 await configTask; // 应用配置... } private async UniTask PlayLevelEntranceEffectAsync(CancellationToken ct) { // 播放一个简单的淡入或动画 await _loadingScreen.FadeOutAsync(0.5f, ct); } private void Cleanup() { // 释放所有通过Addressables加载的资源的句柄 foreach (var handle in _assetHandles) { if (handle.IsValid()) { Addressables.Release(handle); } } _assetHandles.Clear(); _loadingCts?.Dispose(); _loadingCts = null; } private void OnDestroy() { // 组件销毁时,取消加载并清理 _loadingCts?.Cancel(); Cleanup(); } }

这个示例涵盖了顺序与并行混合、进度反馈、错误处理、资源生命周期管理和取消操作,是一个接近生产环境的复杂工作流模板。

最后一点个人体会:从协程切换到UniTask最大的障碍不是语法,而是思维方式的转变。一旦你习惯了用async/await来思考异步流程,你就会发现代码的掌控力大大增强。开始时,可以从替换简单的WaitForSecondsWWW/UnityWebRequest入手,慢慢尝试处理UI事件,最后再架构复杂的游戏流程。记得始终把CancellationToken和对象生命周期放在心上,这两点是写出稳健异步代码的护城河。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询