1. 项目概述:为什么我们需要关注 Unity Editor Coroutines?
如果你在 Unity 编辑器开发上投入过时间,无论是写一个自定义的 Inspector 面板,还是开发一个资产导入后的处理工具,甚至是一个复杂的场景编辑插件,你大概率会遇到一个共同的痛点:如何在编辑器代码里处理那些需要“等待”的操作。比如,你想在导入一批模型后,自动为它们生成 LOD(细节层次),这个生成过程可能需要几秒钟,你肯定不希望在这几秒内整个编辑器界面都卡死,用户点不了任何按钮。又或者,你想实现一个进度条,实时显示资源打包的进度。
这时候,你可能会想到协程(Coroutines)。在 Unity 的游戏运行时(Runtime)中,协程是我们的老朋友,用yield return new WaitForSeconds(1f);来实现延迟执行简直不要太方便。但当你兴冲冲地把同样的代码搬到Editor文件夹下的脚本里,准备在OnInspectorGUI或者一个编辑器窗口的OnGUI方法里使用时,你会发现它根本不起作用。编辑器环境下的 GUI 系统是立即执行的,它没有一个像游戏主循环那样的更新机制来驱动这些yield语句。
这就是Unity Editor Coroutines项目要解决的核心问题。它不是一个官方内置的功能,而是一个由社区推动、高度实用化的解决方案,专门为 Unity 编辑器扩展开发而生。简单说,它让你能在编辑器脚本里,像在游戏脚本里使用StartCoroutine一样,启动和管理协程,从而实现非阻塞的、带延迟的、可等待的编辑器操作。这对于提升工具的用户体验和开发效率至关重要。想象一下,你的工具可以边处理资源边更新进度条,或者在不冻结界面的情况下执行一个耗时的计算任务,这会让你的工具看起来专业得多。
我最初接触这个需求,是在开发一个自动化的灯光烘焙工具时。我需要遍历场景中的所有静态物体,逐个计算并生成光照贴图 UV。这个过程可能长达数分钟,如果不用协程,界面就会完全卡住,用户甚至无法取消操作。自从用上了 Editor Coroutines 这套机制,我不仅能显示一个漂亮的进度条,还能在每一帧检查用户是否点击了“取消”按钮,体验提升了好几个档次。
2. 核心原理与方案选型:不止一种实现方式
在 Unity 的编辑器生态里,实现类似协程的功能,主要有几种思路。理解它们的区别,能帮助我们在不同场景下做出最合适的选择。
2.1 官方“准方案”:EditorApplication.update 委托
这是最原始、也是最基础的方法。Unity 的EditorApplication.update是一个静态事件,在编辑器的每一帧都会被调用。你可以利用它来模拟一个简单的更新循环。
using UnityEditor; using UnityEngine; using System.Collections; public class SimpleEditorCoroutine { private IEnumerator _routine; public SimpleEditorCoroutine(IEnumerator routine) { _routine = routine; EditorApplication.update += Update; } private void Update() { if (_routine == null || !_routine.MoveNext()) { Stop(); } } public void Stop() { EditorApplication.update -= Update; _routine = null; } }使用方式:
// 在某个编辑器方法中启动 new SimpleEditorCoroutine(MyRoutine()); IEnumerator MyRoutine() { Debug.Log("开始"); yield return null; // 等待一帧 Debug.Log("一帧后"); // 模拟一个耗时操作 float startTime = (float)EditorApplication.timeSinceStartup; while ((float)EditorApplication.timeSinceStartup - startTime < 2.0f) { yield return null; // 每帧检查 } Debug.Log("2秒后"); }优点:
- 零依赖:不需要任何第三方库,代码简单直接。
- 概念清晰:易于理解其工作原理。
缺点与坑点:
- 生命周期管理繁琐:每个协程实例都需要手动注册和注销
update事件。如果忘记注销,会导致内存泄漏(该委托一直持有对象引用,阻止GC回收)和逻辑错误(停止的协程仍在每帧执行)。 - 缺乏统一的调度器:多个协程各自为政,难以集中管理、暂停、恢复或全部停止。
- 不支持真正的“等待”对象:像
WaitForSeconds这样的运行时对象在编辑器下无法工作,你需要自己用EditorApplication.timeSinceStartup来实现时间等待,代码不够优雅。 - 与编辑器重绘不同步:
EditorApplication.update的频率很高,但如果你在协程中修改了 GUI 相关的数据,可能需要在下一帧 GUI 绘制时才会体现,有时需要手动调用Repaint()。
实操心得:早期我很多小工具都用这种方式,结果项目里散落着各种
EditorApplication.update += ...和-= ...的代码,后期维护和调试简直是噩梦。特别是工具运行中如果抛出未处理的异常,可能导致注销代码没执行,造成幽灵更新,非常难排查。
2.2 社区主流方案:封装完善的调度器
正是由于原生方案的种种不便,社区中出现了几个封装更完善的项目。它们通常包含一个全局的、单例的调度器(Scheduler),负责管理所有活跃的编辑器协程。这些项目解决了上述大部分痛点:
- 自动生命周期管理:你只需要启动协程,调度器会负责在其完成或出错后自动清理。
- 统一的控制:可以通过调度器暂停、恢复所有协程,或在编辑器退出时自动停止所有任务。
- 仿运行时API:提供类似
StartCoroutine(IEnumerator)的 API,甚至实现自己的EditorWaitForSeconds等类,让代码风格与运行时高度一致。 - 错误处理:能更好地捕获和报告协程中的异常,避免一个协程出错导致整个调度器崩溃。
- 与 EditorGUI 协同:一些高级实现会考虑与
EditorGUIUtility.ExitGUI()以及Repaint的配合,让进度条更新更流畅。
常见的开源项目/代码片段来源:
- Unity Forum / 社区博客:很多资深开发者分享过自己的实现。这些代码通常比较轻量,但功能可能不完整。
- GitHub 开源库:有一些专门维护的仓库,例如一些以
UnityEditorCoroutines或EditorAsync命名的项目。它们结构更清晰,可能有单元测试,是更可靠的选择。 - 大型插件/框架内置:一些成熟的 Unity 编辑器扩展框架(如某些资产管理框架、自动化构建系统)内部已经集成了成熟的编辑器协程模块。
选型考量:
- 如果你的需求很简单:比如只是在一个工具里做一次性的、简单的延迟操作,自己用
EditorApplication.update写一个简单的封装也许就够了。 - 如果你在开发复杂的编辑器插件:强烈建议直接引入一个成熟的社区方案。这能节省大量底层调试时间,让你更专注于业务逻辑。在选择时,可以查看项目的更新频率、Issue 处理情况以及代码复杂度。
2.3 异步编程模型(async/await)的融合
随着 C# 对异步编程支持的深入,async/await模式也成为编辑器扩展中的一个选项。你可以利用Task.Delay来模拟等待,但需要注意,Task.Delay默认使用线程池,可能会在非主线程上恢复,而 Unity 的编辑器 API 绝大多数都不是线程安全的。
一种安全的模式是结合Task和EditorApplication.delayCall:
using System.Threading.Tasks; using UnityEditor; public async void DoEditorTaskAsync() { Debug.Log("步骤1"); // 使用 Task.Run 将耗时计算丢到后台线程 var heavyResult = await Task.Run(() => SomeHeavyCalculation()); // 回到主线程更新UI await Task.Delay(100); // 这里用Task.Delay没问题,但恢复可能在非主线程 // 更安全的方式是使用自定义的切换到主线程的等待 await EditorThreadHelper.MainThreadContext; Debug.Log($"计算完成,结果: {heavyResult}"); // 现在可以安全地操作 EditorGUI 或任何 Unity 对象了 } // 一个简单的切换到主线程的工具(示例) public static class EditorThreadHelper { public static MainThreadAwaitable MainThreadContext => new MainThreadAwaitable(); public struct MainThreadAwaitable { public Awaiter GetAwaiter() => new Awaiter(); public struct Awaiter : System.Runtime.CompilerServices.INotifyCompletion { public bool IsCompleted => UnityEditor.EditorApplication.isPlaying ? UnityEngine.Object.FindObjectOfType<UnityEngine.EventSystems.EventSystem>() != null : true; // 简化判断 public void GetResult() { } public void OnCompleted(System.Action continuation) { UnityEditor.EditorApplication.delayCall += () => continuation(); } } } }优点:
- 代码更现代:对于熟悉 C# 异步编程的开发者来说,逻辑流更清晰。
- 易于处理真正的 I/O 操作:如网络请求、文件读写等。
缺点与注意事项:
- 线程安全是雷区:必须非常小心地确保所有访问 Unity 对象或调用编辑器 API 的代码都在主线程执行。上面的
EditorThreadHelper只是一个极简示例,生产环境需要更健壮的实现。 - 与协程生态融合稍复杂:如果你想在同一个流程里混用
IEnumerator协程和async方法,需要额外的桥接代码。 - Unity 版本兼容性:对 C# 语言版本有要求,需确保项目使用的 .NET 或 Mono 版本支持。
我的建议:对于纯粹的、序列化的编辑器操作流程(A做完做B,中间等一会儿),传统的基于
IEnumerator的编辑器协程方案更直观、更安全,社区支持也更好。如果你的工具涉及大量的文件系统异步操作或网络通信,可以考虑在底层使用async/await,但务必封装好回到主线程的机制。
3. 实战:集成并使用一个成熟的 Editor Coroutines 方案
假设我们选择了一个来自 GitHub 的、相对成熟的UnityEditorCoroutines库。下面我将以一个实际的场景——**“批量重命名场景中的选中物体”**工具为例,展示完整的集成和使用流程。这个工具需要逐帧处理,以避免界面卡顿,并显示进度。
3.1 获取与集成
- 获取代码:通常你会得到一个
.cs文件(如EditorCoroutines.cs)或一个小的 UnityPackage。 - 放入项目:将其放入你的项目的
Assets/Editor文件夹或任何Editor文件夹下。确保其命名空间不会与你项目的其他代码冲突。通常这些库的代码会放在一个像UnityEditor.Coroutines这样的命名空间里。
3.2 工具设计与协程分解
我们的工具功能:
- 在
Window -> My Tools -> Batch Rename打开一个编辑器窗口。 - 用户输入一个基础名称(如 “Enemy_”)和起始索引。
- 点击“应用”后,工具遍历当前选中的所有 GameObject。
- 为每个物体按顺序重命名(如 “Enemy_01”, “Enemy_02”)。
- 在重命名过程中,窗口显示一个进度条,并且每重命名一个物体后短暂延迟一帧,让用户能看到变化。
核心协程设计:
IEnumerator BatchRenameRoutine(string baseName, int startIndex) { GameObject[] selectedObjects = Selection.gameObjects; int totalCount = selectedObjects.Length; for (int i = 0; i < totalCount; i++) { // 更新进度 float progress = (float)i / totalCount; // 这里需要一种方式将进度信息传递回 OnGUI。通常可以通过成员变量。 _currentProgress = progress; _currentProcessingObject = selectedObjects[i].name; // 执行重命名 selectedObjects[i].name = $"{baseName}_{startIndex + i:00}"; // 重要:标记场景已修改,否则撤销操作可能不生效 Undo.RegisterCompleteObjectUndo(selectedObjects[i], "Batch Rename"); // 等待一帧,让UI有机会更新,也让用户能看到逐个变化的过程 yield return null; // 使用编辑器协程库支持的“等待下一帧” // 如果你想有更明显的间隔,可以等待一个短暂时间(例如0.1秒) // yield return new EditorWaitForSeconds(0.1f); // 假设库提供了这个类 } _currentProgress = 1.0f; _currentProcessingObject = “完成!”; // 重命名完成后,强制刷新一下界面,然后清空状态 this.Repaint(); yield return null; _isRunning = false; _currentProgress = 0f; }3.3 编辑器窗口与协程驱动
下面是完整的编辑器窗口代码,集成了协程的启动、停止和状态更新。
using UnityEditor; using UnityEngine; using System.Collections; // 假设引入的编辑器协程库提供了这个静态类 using UnityEditor.Coroutines; // 命名空间可能不同 public class BatchRenameWindow : EditorWindow { private string _baseName = “NewObject_”; private int _startIndex = 1; private bool _isRunning = false; private float _currentProgress = 0f; private string _currentProcessingObject = “”; private object _currentCoroutine; // 用于持有协程句柄,以便停止 [MenuItem(“Window/My Tools/Batch Rename”)] public static void ShowWindow() { GetWindow<BatchRenameWindow>(“Batch Rename”); } void OnGUI() { EditorGUILayout.LabelField(“批量重命名工具”, EditorStyles.boldLabel); EditorGUILayout.Space(); // 如果正在运行,禁用输入控件 EditorGUI.BeginDisabledGroup(_isRunning); _baseName = EditorGUILayout.TextField(“基础名称”, _baseName); _startIndex = EditorGUILayout.IntField(“起始索引”, _startIndex); EditorGUILayout.HelpBox($"当前选中 {Selection.gameObjects.Length} 个对象。”, MessageType.Info); EditorGUI.EndDisabledGroup(); EditorGUILayout.Space(); // 开始/停止按钮 GUI.enabled = Selection.gameObjects.Length > 0; // 有选中对象才可点击 if (_isRunning) { if (GUILayout.Button(“停止处理”, GUILayout.Height(30))) { StopCoroutine(); } } else { if (GUILayout.Button(“开始重命名”, GUILayout.Height(30))) { StartCoroutine(); } } GUI.enabled = true; // 进度显示 if (_isRunning) { EditorGUILayout.Space(); Rect progressRect = EditorGUILayout.GetControlRect(false, 20); EditorGUI.ProgressBar(progressRect, _currentProgress, “处理中…”); EditorGUILayout.LabelField($"正在处理: {_currentProcessingObject}”); } // 实时重绘:如果协程正在运行,我们需要每帧都更新界面以显示进度 if (_isRunning) { Repaint(); } } void StartCoroutine() { if (Selection.gameObjects.Length == 0) { EditorUtility.DisplayDialog(“错误”, “请先在场景中选择至少一个GameObject。”, “确定”); return; } _isRunning = true; _currentProgress = 0f; // 关键:使用编辑器协程库启动协程,并保存返回的句柄 _currentCoroutine = EditorCoroutineUtility.StartCoroutine(BatchRenameRoutine(_baseName, _startIndex), this); } void StopCoroutine() { if (_currentCoroutine != null) { // 关键:使用编辑器协程库停止指定的协程 EditorCoroutineUtility.StopCoroutine(_currentCoroutine); _currentCoroutine = null; } _isRunning = false; _currentProgress = 0f; Repaint(); } // 当窗口关闭时,确保清理协程 void OnDestroy() { StopCoroutine(); } // 协程本体 IEnumerator BatchRenameRoutine(string baseName, int startIndex) { GameObject[] selectedObjects = Selection.gameObjects; int totalCount = selectedObjects.Length; for (int i = 0; i < totalCount; i++) { // 检查是否被外部停止(虽然StopCoroutine会直接终止,但这里加个判断更安全) if (!_isRunning) yield break; _currentProcessingObject = selectedObjects[i].name; _currentProgress = (float)i / totalCount; // 执行核心操作 string newName = $“{baseName}{startIndex + i:00}”; Undo.RegisterCompleteObjectUndo(selectedObjects[i], “Batch Rename”); selectedObjects[i].name = newName; // 让编辑器场景视图也立即刷新显示新名字 EditorApplication.RepaintHierarchyWindow(); // 等待一帧。这是编辑器协程库能正确理解的关键。 yield return null; } // 完成收尾 _currentProgress = 1.0f; _currentProcessingObject = “所有对象重命名完成!”; // 最后再重绘一次,显示完成状态 Repaint(); // 等待一帧再重置状态,让用户能看到“完成”提示 yield return null; _isRunning = false; _currentCoroutine = null; Debug.Log($“批量重命名完成,共处理 {totalCount} 个对象。”); } }关键点解析:
EditorCoroutineUtility.StartCoroutine:这是库提供的核心API。它接受一个IEnumerator和可选的owner对象。指定owner(这里传了this窗口实例)的好处是,当这个owner被销毁(如窗口关闭)时,库可能会自动停止关联的协程,防止内存泄漏。EditorCoroutineUtility.StopCoroutine:通过启动时返回的句柄来停止特定协程。这比粗暴地关闭整个调度器要精细。Repaint()的调用:在协程运行期间(_isRunning为真),我们在OnGUI末尾调用了Repaint()。这确保了即使没有用户输入,窗口也能每秒多次(与编辑器帧率同步)刷新,从而流畅地更新进度条。在协程内部,我们也在关键节点后调用了Repaint()和EditorApplication.RepaintHierarchyWindow()来更新界面和场景视图。yield return null:在编辑器协程中,这表示“等待直到下一次编辑器更新循环”。这是驱动协程向前执行的核心机制。- 撤销操作:使用
Undo.RegisterCompleteObjectUndo注册撤销点非常重要。这样用户就可以按Ctrl+Z撤销批量重命名操作。务必在修改对象属性之前注册。
4. 高级技巧与避坑指南
在实际项目中使用编辑器协程,你会遇到一些标准教程里不会提的细节问题。这里分享我踩过的一些坑和总结的技巧。
4.1 处理异常与协程停止
协程中的异常如果不捕获,可能会导致整个协程调度器静默停止,或者将错误抛到编辑器控制台但协程状态混乱。
建议的异常处理模式:
IEnumerator SafeRoutine() { try { yield return SomeOperationThatMightFail(); yield return AnotherOperation(); } catch (System.Exception e) { Debug.LogError($“编辑器协程执行失败: {e.Message}”); EditorUtility.DisplayDialog(“操作错误”, e.Message, “确定”); // 在这里进行必要的状态清理 yield break; // 确保协程退出 } finally { // 确保无论成功失败,某些清理工作都会执行 _isRunning = false; } }停止协程的最佳实践:
- 提供取消机制:像上面的例子一样,在循环内检查
_isRunning标志。 - 资源清理:如果协程中打开了文件流、网络连接等非托管资源,确保在
finally块或OnDestroy中释放。 - 避免在协程中途修改关键对象状态:如果协程正在遍历一个列表并修改它,外部突然清空了这个列表,会导致异常。考虑在协程开始时获取数据的副本。
4.2 与 EditorGUI 的深度配合
进度条与后台任务:上面的例子展示了基本的进度条。对于更复杂的操作,你可能需要将“计算”和“UI更新”分离。
IEnumerator LongCalculationWithProgress() { int totalSteps = 1000; for (int i = 0; i < totalSteps; i++) { // 1. 执行一部分计算(这里是模拟) PerformHeavyCalculationStep(i); // 2. 更新进度,但不要太频繁地重绘,以免影响性能 if (i % 10 == 0) // 每10步更新一次UI { _currentProgress = (float)i / totalSteps; _currentStepInfo = $“正在计算步骤 {i}…”; // 请求重绘,但不等待 Repaint(); // 让出一帧的控制权,保持编辑器响应 yield return null; } } _currentProgress = 1.0f; Repaint(); }处理 EditorGUIUtility.ExitGUI():有些 EditorGUI 操作(如弹出模态对话框EditorUtility.DisplayDialog)会抛出ExitGUIException来立即退出当前 GUI 循环。如果你的协程在OnGUI中被启动,然后内部又调用了这类函数,需要妥善处理。
IEnumerator RoutineWithDialog() { // ... 一些操作 bool result = false; // 在主线程上显示对话框 EditorApplication.delayCall += () => { result = EditorUtility.DisplayDialog(“确认”, “是否继续?”, “是”, “否”); // 如何将结果传回协程?需要用更复杂的状态机或回调。 // 一种简单方式是设置一个成员变量标志。 _userConfirmed = result; }; // 等待用户操作完成 while (!_userResponseReceived) // 需要另一个标志位 { yield return null; } if (_userConfirmed) { // 继续执行 } else { yield break; } }这种方式稍显复杂,对于简单的“是/否”确认,有时直接在启动协程前用阻塞式的DisplayDialog询问用户会更简单。
4.3 性能考量与优化
- 避免每帧都
Repaint()所有窗口:如果工具很多,频繁重绘会影响编辑器性能。只重绘需要更新的窗口。 yield return null的频率:对于非常密集的循环,每帧都yield return null可能会让操作变得很慢。可以考虑每处理 N 个元素再让出一帧,或者在耗时超过某一阈值(如 10 毫秒)后让出一帧,在速度和响应度之间取得平衡。- 使用
EditorUtility.DisplayProgressBar替代自定义进度条:这是一个全局的、模态的进度条,适用于长时间的后台任务。它会阻塞用户与编辑器的其他交互,但实现起来更简单,且能防止用户在任务执行时进行其他操作。try { for (int i = 0; i < total; i++) { // 检查用户是否点击了取消按钮 if (EditorUtility.DisplayCancelableProgressBar(“处理中”, $“正在处理项目 {i}”, (float)i/total)) { break; // 用户取消了 } // ... 处理逻辑 } } finally { EditorUtility.ClearProgressBar(); // 务必清理! }重要警告:
DisplayProgressBar必须在finally块中或用其他方式确保被ClearProgressBar,否则即使协程异常退出,进度条也会一直卡在那里,导致编辑器无法操作。
5. 常见问题排查与解决方案实录
即使使用了成熟的库,在实际开发中还是会遇到一些古怪的问题。这里记录几个典型案例和解决思路。
问题1:协程启动了,但似乎没执行,或者只执行了一次就停了。
- 可能原因A:
yield return了错误的对象。在编辑器环境下,yield return new WaitForSeconds(1)是无效的,因为WaitForSeconds依赖于游戏时间缩放。必须使用编辑器协程库提供的等待类(如EditorWaitForSeconds)或yield return null。 - 排查:在协程开始和每个
yield语句后加Debug.Log,观察输出。确认你使用的库支持你使用的yield类型。 - 可能原因B:协程的“所有者”(owner)对象被提前销毁了。如果你启动协程时传入了一个
MonoBehaviour(编辑器脚本中很少)或ScriptableObject作为 owner,而这个对象在协程结束前被销毁了,某些库的实现可能会自动停止该协程。 - 排查:检查启动协程的代码所在对象的生命周期。对于编辑器窗口,在
OnDestroy中停止协程是好的,但要确保不是窗口意外关闭导致。
问题2:进度条不更新,或者界面卡住不动。
- 可能原因A:没有调用
Repaint()。编辑器窗口不会自动刷新。你必须在协程中或通过某种机制(如设置一个每帧更新的EditorApplication.update委托)来请求窗口重绘。 - 解决:在更新进度变量后,调用
windowInstance.Repaint()或EditorWindow.focusedWindow.Repaint()。 - 可能原因B:协程内的计算太耗时,阻塞了主线程。即使你用了
yield return null,但在两次yield之间执行的代码如果花费了数秒钟,编辑器在这期间仍然是卡住的。 - 解决:将耗时的计算拆分。例如,如果你要处理 10000 个顶点,不要在一个循环里算完再
yield,而是每处理 100 个或每消耗超过 10 毫秒就yield return null一次。IEnumerator ProcessManyItems(List<Item> items) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); for (int i = 0; i < items.Count; i++) { sw.Restart(); ProcessItem(items[i]); // 处理单个项目 // 如果单个处理时间超过帧预算,或者每处理N个后,让出一帧 if (sw.ElapsedMilliseconds > 16 || i % 50 == 0) // 16ms ~ 60FPS的一帧 { _progress = (float)i / items.Count; Repaint(); yield return null; } } }
问题3:在协程中操作 Unity 对象(如 GameObject、Texture)时报空引用或无效操作异常。
- 可能原因:对象在协程等待期间被销毁了。比如,你
yield return等待了 2 秒,但在这 2 秒内,用户可能删除了场景中你正在处理的那个物体。 - 解决:在每次
yield回来后,以及访问对象前,进行空值检查。GameObject targetObj = Selection.activeGameObject; yield return new EditorWaitForSeconds(2.0f); // 等待2秒后,对象可能已经不存在了 if (targetObj == null) // 在编辑器模式下,直接检查是否为null通常是有效的 { Debug.LogWarning(“目标对象已不存在,操作中止。”); yield break; } // 继续安全地操作 targetObj
问题4:使用了EditorUtility.DisplayProgressBar,但即使任务完成或出错,进度条也不消失。
- 原因:没有在
finally块中调用EditorUtility.ClearProgressBar()。协程可能因异常而提前退出,跳过了清理代码。 - 解决:永远将
ClearProgressBar放在try...finally块中。try { for (...) { if (EditorUtility.DisplayCancelableProgressBar(...)) break; // ... yield return null; // 协程可能在这里因异常退出 } } finally { EditorUtility.ClearProgressBar(); // 保证执行 }
问题5:在 Play Mode 切换时,编辑器协程行为异常。
- 背景:当点击播放按钮进入运行模式时,编辑器会重新加载部分状态。一些简单的、基于
EditorApplication.update的协程实现可能会被打断或产生重复实例。 - 解决:使用健壮的库,它们通常会处理
EditorApplication.playModeStateChanged事件,在进入播放模式前停止所有编辑器协程。如果你自己实现,也需要监听这个事件。void OnEnable() { EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } void OnDisable() { EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; } void OnPlayModeStateChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.ExitingEditMode) { // 停止所有正在运行的编辑器协程 StopAllCoroutines(); } }
掌握编辑器协程,相当于为你 Unity 工具开发的武器库增添了一件利器。它让原本生硬、阻塞的编辑器操作变得流畅、友好。从简单的延迟执行,到复杂的多步骤向导式工具,再到后台资源处理流水线,其应用场景非常广泛。花点时间选择一个稳定可靠的实现方案,并理解其背后的原理和陷阱,这将极大提升你开发的编辑器工具的专业度和用户体验。