如何在 Unity3D Editor 脚本中使用 Coroutine 进行异步操作

今天在做编辑器中一个功能时,碰到一个这样的需求,Unity3D Editor 脚本中会通过 System.Diagnostics.Process 这个类来调用外部的系统命令来执行一个操作,其实就是调用系统的 Python 命令执行一个 Python 脚本,来将 Excel 文件转换成文本数据。

[MenuItem ("LightHonor/Config/ImportExcel")]
static void ConvertXlsxToTxtConfig ()
{
    string txt2xlsxPythonFilePath = Directory.GetParent (Application.dataPath) 
        + Path.DirectorySeparatorChar.ToString () + "Docs" 
        + Path.DirectorySeparatorChar.ToString () + "Tools" 
        + Path.DirectorySeparatorChar.ToString () + "convert.py";
    if (Application.platform == RuntimePlatform.OSXEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo ("/usr/bin/python", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    } else if (Application.platform == RuntimePlatform.WindowsEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo (@"C:\Python27\python.exe", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    }
}

private static void ConvertProcessExited (object sender, System.EventArgs e)
{
    Debug.Log ("Convert Excel Xlsx Config To Text File Done!");
}

但是这样执行完 ImportExcel 菜单项之后,可能会出现我们通过外部的脚本修改了 Unity3D 需要使用到的文本文件,但是 Unity3D 未能及时更新 AssetDatabase,导致 Unity3D 中读取到的文本文件还是执行 ImportConfig 菜单项之前的数据。那么我们能否直接在这个外部程序执行完毕的回调中执行 AssetDatabase.Refresh() 方法呢?不行,因为这些函数都只能在主线程里头调用,而这个回调函数是在启动外部程序的线程中执行的,所以在 ConvertProcessExited() 方法中访问 AssetDatabase 对象会抛出异常。这个时候我们可以通过 EditorApplication 的 update 委托来配合 Coroutine 机制来完成我们想要的功能。

关于 EditorApplication 如何支持 Coroutine,这个是在 GitHub 上找到了一个很棒的参考,地址在这儿,链接页面中是一个完整的独立的 EditorCoroutine 的实现,借助于这个漂亮的工具,我们修改了代码,然后就能达成我们的需求了,在执行完外部转化的 Python 程序之后,及时通过 AssetDatabase 刷新 Unity3D 中需要使用到的文本文件,代码如下:

private static bool sConvertDone = false;

[MenuItem ("LightHonor/Config/ImportExcel")]
static void ConvertXlsxToTxtConfig ()
{
    string txt2xlsxPythonFilePath = Directory.GetParent (Application.dataPath) 
        + Path.DirectorySeparatorChar.ToString () + "Docs" 
        + Path.DirectorySeparatorChar.ToString () + "Tools" 
        + Path.DirectorySeparatorChar.ToString () + "convert.py";
    if (Application.platform == RuntimePlatform.OSXEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo ("/usr/bin/python", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    } else if (Application.platform == RuntimePlatform.WindowsEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo (@"C:\Python27\python.exe", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    }
    sConvertDone = false;
    EditorCoroutine.Start (RefreshAssetDatabase ());
}

static IEnumerator RefreshAssetDatabase ()
{
    while (!sConvertDone) {
        yield return 0;
    }
    AssetDatabase.Refresh ();
    Debug.Log ("Refresh Asset Database Done");
}

private static void ConvertProcessExited (object sender, System.EventArgs e)
{
    Debug.Log ("Convert Excel Xlsx Config To Text File Done!");
    sConvertDone = true;
}