unity-工具-csharp与python交互

很多时候写工具都是使用 python 来写, unity 工具需要调用 python 脚本并获取到执行结果.


非弹窗式

可以从 Python 的 os 的 标准输出 (sys.stdout.write) 中返回给 cshap

python 的 print 其实就是调用 sys.stdout.write + 换行符

  1. csharp 执行 命令行 的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // 可以获取到 py 脚本 print 的值
    public static string ProcCmd(string command, string argument) {
    ProcessStartInfo psi = new ProcessStartInfo(command);
    psi.Arguments = argument;
    psi.CreateNoWindow = true;
    psi.ErrorDialog = true;
    psi.UseShellExecute = false

    psi.RedirectStandardOutput = true;
    psi.RedirectStandardError = true;
    psi.RedirectStandardInput = true;
    psi.StandardOutputEncoding = System.Text.UTF8Encoding.UTF8;
    psi.StandardErrorEncoding = System.Text.UTF8Encoding.UTF8;

    Process p = Process.Start(psi); 

    StringBuilder sb1 = new StringBuilder();
    StreamReader reader = p.StandardOutput;
    while (!reader.EndOfStream) {
    string line = reader.ReadLine();
    if (!line.StartsWith("---")) { // 过滤掉自定义的日志, 方便 py 调试
    sb1.Append(line);
    }
    if (!reader.EndOfStream) {
    sb1.Append("\n");
    }
    }
    reader.Close();

    StringBuilder sb2 = new StringBuilder();
    StreamReader errorReader = p.StandardError;
    while (!errorReader.EndOfStream) {
    sb2.Append(errorReader.ReadLine()).Append("\n");
    }
    errorReader.Close();

    p.WaitForExit();
    p.Close();

    if (sb2.Length > 0) {
    throw new Exception(string.Format("--- Error, python error, msg:{0}", sb2.ToString()));
    }
    return sb1.ToString();
    }
  2. python 工具脚本

    1
    2
    3
    4
    5
    6
    7
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-

    import sys

    if __name__ == "__main__":
    print("hello world, arg1: {}".format(sys.argv[1])) # 这个就能输出给 csharp
  3. 测试 csharp 执行 python 结果

    1
    2
    3
    string pyPath = Path.Combine(PackConfig.PyToolDir, "tool.py");
    string res = EditorUtils.ProcCmd("python3", string.Format("{0} {1}", pyPath, "getProjBranch"));
    Debug.LogFormat("--- py res: {0}", res);


弹窗式

单独一个命令行窗口, 标准输出不会返回给 csharp

Windows

  1. csharp 执行 命令行 的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 弹窗执行 py
    public static void ProcCmdOnWin(string command, string argument) {
    ProcessStartInfo psi = new ProcessStartInfo(command);
    psi.Arguments = argument;
    psi.CreateNoWindow = true;
    psi.ErrorDialog = true;
    psi.UseShellExecute = true
    psi.RedirectStandardOutput = false;
    psi.RedirectStandardError = false;
    psi.RedirectStandardInput = false;
    Process p = Process.Start(psi); 
    p.WaitForExit();
    p.Close();
    }

数据结构的传递

一般使用 json 作为数据结构的格式传递给 Python, 但是命令不能正常传递 json 字符串 给 Python 获取, 所以得用曲线救国的方式 base64 encode 一下给 py, 然后在 py 在 decode 解出正常的 json

  • csharp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static string Base64Encode(string plainText) {
    var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
    return System.Convert.ToBase64String(plainTextBytes).Replace("\n", "");;
    }

    public static string Base64Decode(string base64EncodedData) {
    var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData);
    return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
    }
  • python

    1
    2
    3
    4
    5
    6
    def base64Encode(txt: str) -> str:
    # 参考: https://blog.csdn.net/u010953266/article/details/52590570
    # 根据RFC822规定,BASE64Encoder编码每76个字符,还需要加上一个回车换行
    return base64.encodebytes(txt.encode()).decode().replace("\n", "")
    def base64Decode(encodedData: str) -> str:
    return base64.decodebytes(encodedData.encode()).decode()

踩坑

base64 encode 后有换行符

根据RFC822规定,BASE64Encoder编码每76个字符,还需要加上一个回车换行

解决办法: 将换行符去掉即可. 参考: https://blog.csdn.net/u010953266/article/details/52590570


macOS

mac 系统下就不能使用 Windows 那种方式直接执行 python 脚本. 但可以执行 shell 脚本, 但是也不可以传递多个参数.

所以曲线救国的思路就是

  1. 创建一个可执行 shell 脚本, 如: mac_shell.sh, 赋予 +x 执行权限

    1
    $ touch mac_shell.sh && chmod +x mac_shell.sh
  2. 执行的命令写入到 shell 里面

  3. open 命令执行这个 shell

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 弹窗执行 py, 新开线程, 不阻塞 gui
    public static void ProcCmdOnMacOs(string command, string argument) {
    // 曲线救国, mac 下无法唤起带参数的命令行窗口, 只能先写到 shell 里面再执行 shell
    string shellPath = EditorUtils.Join(PackConfig.TempSave, "mac_shell.sh");
    Utils.WriteFileUTF8(shellPath, string.Format("{0} {1}", command, argument)); // 写入到 mac_shell.sh

    new Thread(() => {
    Process.Start("open", shellPath);
    }).Start();
    Thread.Sleep(500); // 睡眠一下 500ms, 防止多条指令同时执行
    }