你是否已经掌握了编写基础的 Unity 编辑器脚本,甚至懂得使用 AssetDatabase.Start/StopAssetEditing 来进行批量性能优化?恭喜你,你已经是一名合格的编辑器工具开发者了!(不清楚的可查看这篇文章)
但是,当你的工具面临更复杂的挑战——比如需要处理耗时操作而不能卡死UI,或者要被集成到自动化构建流程(CI/CD)中时,两个新的“大Boss”便会悄然出现。
今天,我们就来直面这两个高级挑战:异步操作的“伪装” 和 打通命令行(Batch Mode)的“任督二脉”。
第一幕:识破异步的“伪装” —— EditorCoroutine 的正确用法
我们先来看一个非常具有迷惑性的场景。为了优化一个耗时很长的批量资源处理任务,我们学会了使用 AssetDatabase.Start/StopAssetEditing 来合并资源导入,代码看似是这样的:
// 一个看似完美,实则错误的版本
public void ProcessLotsOfAssets()
{
AssetDatabase.StartAssetEditing(); // 暂停导入
try
{
// 为了不卡死UI,我们使用了编辑器协程
EditorCoroutine.Start(MyAsyncProcess());
}
finally
{
// 陷阱!这行代码会立即执行,而不是等协程结束后!
AssetDatabase.StopAssetEditing(); // 恢复导入
}
}
IEnumerator MyAsyncProcess()
{
// ... 在这里循环成百上千次,进行加载、修改、保存资源 ...
Debug.Log("正在处理资源...");
yield return null; // 只是为了举例,实际逻辑会更复杂
}
当你满怀期待地运行它,却发现 Unity 仍然在为每一个你保存的资源单独执行“Importing”,说好的批量导入呢?
这就是异步操作的“伪装”。
EditorCoroutine.Start() 的作用仅仅是“启动”一个协程,把它挂载到编辑器的更新循环上,然后它会立刻返回,主线程会毫不停歇地继续向下执行。这意味着,finally 块里的 AssetDatabase.StopAssetEditing() 在 MyAsyncProcess 协程里的任何文件都还没来得及保存时,就已经被调用了!我们精心设计的批量模式,就这样被提前终结了。
【正确的异步收尾姿势】
要驯服这头异步猛兽,你必须记住一个核心原则:异步操作的“终点”在它的回调函数里。
EditorCoroutine.Start 提供了两个关键的参数:一个在协程成功完成时调用(onComplete),另一个在协程抛出异常时调用(onException)。我们的收尾工作(比如 StopAssetEditing()),必须放在这两个回调中。
// 正确的版本
public void ProcessLotsOfAssets()
{
AssetDatabase.StartAssetEditing();
EditorCoroutine.Start(
MyAsyncProcess(), // 1. 要执行的协程
// 2. 成功完成时的回调
() =>
{
try
{
Debug.Log("所有任务成功完成!");
}
finally
{
// 在这里停止,才是真正的批量处理结束!
AssetDatabase.StopAssetEditing();
}
},
// 3. 发生异常时的回调
(e) =>
{
try
{
Debug.LogError($"发生错误: {e.Message}");
}
finally
{
// 即使出错了,也要确保“刹车”被松开,避免Unity卡住
AssetDatabase.StopAssetEditing();
}
}
);
}
通过将收尾逻辑移入回调,我们确保了无论协程是正常结束还是意外终止,StopAssetEditing() 都会在所有异步任务真正尘埃落定之后,被可靠地执行。
第二幕:打通任督二脉 —— 兼容UI与命令行(Batch Mode)
现在,我们的工具在编辑器里表现完美,既不卡顿,性能又高。老板很高兴,决定把它集成到 Jenkins 的自动化构建管线里,通过命令行来调用它。
然后,构建失败了。日志里充满了刺眼的红色错误。
这是因为,你的工具在设计之初,可能只考虑了“人类用户”在编辑器UI下的使用场景。而当它进入“机器人模式”(无人值守的命令行),环境就完全变了。
【让工具具备“双重人格”】
要让工具既能服务于开发者,又能融入自动化流程,我们需要让它学会“看人下菜碟”。
1. 学会“看环境”:Application.isBatchMode
Unity 提供了一个极其有用的静态属性:Application.isBatchMode。当脚本在命令行模式下运行时,它为 true;在正常的编辑器UI下运行时,它为 false。这是我们实现双轨制逻辑的总开关。
2. 隔离“视觉”元素
命令行模式下没有图形界面,任何试图渲染UI的代码都会报错。因此,所有UI相关的API调用,都必须被包裹在环境检测中:
if (!Application.isBatchMode)
{
EditorUtility.DisplayProgressBar("处理中", "请稍候...", 0.5f);
}
3. “同步”执行你的协程
最大的挑战来了:EditorCoroutine 依赖于编辑器的 Update 心跳,它在命令行模式下根本无法运行。那我们那些复杂的、包含 yield return 的逻辑怎么办?重写一套完全同步的版本吗?
大可不必!我们可以巧妙地创建一个同步协程执行器。它的原理很简单:用一个 while 循环来“榨干”一个协程,一次性把它从头到尾跑完。
private static void RunCoroutineSynchronously(IEnumerator coroutine)
{
// 用一个栈来处理嵌套的协程 (yield return anotherCoroutine)
var stack = new Stack<IEnumerator>();
stack.Push(coroutine);
while (stack.Count > 0)
{
var current = stack.Peek();
if (current.MoveNext())
{
// 如果yield return返回的是另一个协程,就把它压入栈顶
if (current.Current is IEnumerator next)
{
stack.Push(next);
}
// 注意:我们忽略了 yield return null 等待帧的指令,因为在同步模式下没有“下一帧”
}
else
{
// 当前协程执行完毕,弹出
stack.Pop();
}
}
}
4. 组装你的双轨制入口
最后,在你的工具入口函数里,布下“天罗地网”:
public static void MyAwesomeToolEntry()
{
// 如果是命令行模式...
if (Application.isBatchMode)
{
Debug.Log("进入命令行模式...");
AssetDatabase.StartAssetEditing();
try
{
// ...就用同步执行器跑完所有逻辑
RunCoroutineSynchronously(MyCoreLogicCoroutine());
}
finally
{
AssetDatabase.StopAssetEditing();
Debug.Log("命令行处理完成。");
}
}
// 否则,就是熟悉的编辑器UI模式...
else
{
// ...就用异步协程,并把收尾工作放进回调里
AssetDatabase.StartAssetEditing();
EditorCoroutine.Start(MyCoreLogicCoroutine(),
() => { AssetDatabase.StopAssetEditing(); },
(e) => { AssetDatabase.StopAssetEditing(); }
);
}
}
通过这番改造,你的编辑器工具就真正“进化”了。它既保留了在UI环境下的交互性和流畅性,又获得了在自动化管线中稳定运行的强大能力,成为了一个真正专业的生产力工具。
希望这两个进阶技巧,能帮助你在Unity编辑器开发的道路上走得更远、更稳!
525

被折叠的 条评论
为什么被折叠?



