话说, 为什么想不开要用Python开发Unity, 快跑, 全是坑. 也许……你已经在坑里了?

这篇文章粗浅地介绍用Python开发Unity的一种可行的姿势, 以及可能会踩的一些坑.

背景是最近在尝试用Unity开发独立项目, 说实话, 一开始打算使用Python与Unity的组合确实没有料想到会有这么多问题, 毕竟一边是当前最主流的商业游戏引擎, 而另一边则是当前最主流的编程语言, 官方支持暂且不提, 各种应用一定很丰富吧. 但当我开始检索不得不认清事实, 相关资料少得可怜, 这个技术路线更多被应用于科研, 这倒也并非不可理解.

如前所述, 本文只是粗浅介绍一种用Python开发Unity的姿势, 除自己折腾外实则并不推荐. 显然就商业项目而言, Lua+Unity才是各种成本最小的选择. 简单一提, 因为热更新的需要, 通常把需要频繁更新的逻辑用解释型语言来编码. 除此之外, 在Unity中引入动态语言的好处还有不少, 比如相比原生C#有更高编码效率, 更容易上手, 改写动态语言编码的逻辑也不会触发Unity耗时的重编译. 至于为什么选择用Python更多是个人喜好, Python原生支持面向对象, 基础设施较为完备, 语法优雅, 美中不足是牺牲一些运行效率.

本文并不会涉及如何构建基于Unity的Python框架, 及优化Unity和Python调用性能等高级话题, 或许等到我有了些许心得的时候再聊.

当我们决定用Python来开发Unity时, 我们在想什么? 首先, 这从理论上可行性如何? 因为目前主流已有不少使用Lua开发Unity的方案, 所以应该可行, 需要了解下Python相关的支持如何. 也许可以试试Unity官方Python插件?

Python Scripting插件

没错, 实际上Unity官方确实提供了一个Python插件, 利用该插件可以在Unity编辑器中执行Python脚本, 具体用法可参考官方文档与插件包内的例程. 安装可以参考这个讨论Python for Unity Install - Unity Forum. 截至目前, 这个插件竟然已迭代到了第七版(然而文档依然很简略), 安装插件会自带一个Python解释器.

然而经过简短的使用后发现一些问题, 首先这个Python解释器似乎是与Unity编辑器进程绑定的, 每次进入Play模式依然保留有上次运行的状态, 包括导入的module等, 更关键的是在代码中使用了Python插件相关接口Unity会无法build, 也就是无法利用这个插件开发独立的Unity应用. 也许是我看漏了, 我好像没有注意到官方文档有相关的说明, 直到我看到了这个讨论Python for Unity Editor Only - Unity Forum.

所以, 如果只是拿Unity和Python做个编辑器应用这个插件还是绰绰有余的, 但想更进一步就得自己动手了. 这个讨论也启示了一种使用Python开发Unity独立应用的方法, 最新的回帖坛友分享了他解决方案的一个例子Showcase - Python for Unity - Unity Forum.

启示, Python Scripting插件底层接口是依赖于一个叫Python.NET的开源项目, 借助这个开源项目能够实现Python与C#的相互调用, 这正是我们所需要的.

Python.NET

因为Unity本身是基于.Net平台的, 可以把思路转换成Python在.Net环境下开发. 检索了一下, 目前比较主流有两种方案. 其一是前面提到的Python.NET, 另一个是同样开源的IronPython项目. IronPython是C#的Python实现, 有更好的C#支持, 而Python.Net支持的是CPython实现, Python这边的支持更好, 接口调用方式来看差异倒不是很大, 都值得尝试. 这里选择更新稍勤奋的Python.NET进行测试.

为了便于测试, 首先创建一个的Unity目录结构, 只列举必要的文件夹.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Assets/
|- Plugins/ 第三方插件dll
| |- PythonRuntime/
|
|- PythonScripts/ Python代码
| |- main.py
|
|- Scripts/ C#代码
| |- Manager/
| | |- PythonMgr.cs
| |- GameLauncher.cs
|
|- SteamingAssets/
| |- Python/
|
...

然后需要下载一个Python实现及Python.NET项目编译输出的dll, 分别放在Python与PythonRuntime文件夹内.

以Python3.10为例, 从Python Releases for Windows | Python.org下载3.10版本的Windows embeddable package压缩包, 解压后放到Python目录即可, 这个package的python貌似是不带pip, 可能需要手动安装.

至于Python.NET编译的dll需要把项目源码从Github上下载下来, VS打开并编译一下Runtime这个子项目, 拿到Python.Runtime.dll并放入PythonRuntime目录.

Python.NET源码版本的选择这里有个问题, 即前面提到的, Unity编辑器进入Play模式运行Python脚本后, 再退出Play模式, Python运行时不会关闭, 依然会保留之前运行的状态(重新初始化也不会生效), 这自然对开发过程带来很大的不便. 而且可能因为Python侧引用失效, 存在第二次运行Play模式时崩溃的隐患. Python.NET在早前的一个版本中加入了SoftShutdown模式可用来处理上述情况, 但在最近的几次更新中把上述功能移除了(尽管不会再崩溃). 具体细节可参考以下资料

如果为了使用soft shutdown带来的便利, 我们可以暂时使用pythonnet 3.0.0 alpha-2版本来编译.
关于Python.NET开发相关资料可参考:

Python与C#互相调用

总算看到点希望了, 简单说下测试环境. Unity空场景中创建一个Game空物体, 给Game挂上一个GameLauncher.cs作为应用的启动脚本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// GameLauncher.cs
using UnityEngine;

public class GameLauncher : MonoSingleton<GameLauncher>
{
private void Awake()
{
PythonMgr.instance.InitEnv();
}

private void Start()
{
Debug.Log("[GameLauncher] EinS Start!");
PythonMgr.instance.StartGame();
}
}

PythonMgr类用于管理Python运行时, 内容如下

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
// PythonMgr.cs
using UnityEngine;

using Python.Runtime;
using System.IO;

public class PythonMgr : Singleton<PythonMgr>
{
public string python_code_path;

public void InitEnv()
{
Runtime.PythonDLL = Path.Combine(Application.streamingAssetsPath, "Python\\python310.dll");
PythonEngine.Initialize(mode: ShutdownMode.Soft);
Debug.Log("[PythonMgr] python interpreter version: " + PythonEngine.Version);

python_code_path = Path.Combine(Application.dataPath, "PythonScripts/");
Debug.Log("[PythonMgr] python_path: " + python_code_path);
}

public void StartGame()
{
Debug.Log("[PythonMgr] StartGame");

using (Py.GIL())
{
dynamic sys = Py.Import("sys");
sys.path.append(python_code_path);

dynamic main = Py.Import("main");
dynamic res = main.StartGame();
}

PythonEngine.Shutdown();
}
}

为了通过Python.NET调用Python运行时需要用到Python.Runtime命名空间. 根据Python.Net的文档, 初始化PythonEngine前需要设置Runtime.PythonDLL到对应Python版本dll的路径, 或者设置PYTHONNET_PYDLL环境变量. 接着使用ShutdownMode.Soft进行初始化. StartGame中运行Python代码前需使用using(Py.GIL())获取到Python的GIL锁. 然后就可以使用Py.Import导入模块, 并调用模块方法了. 这里我们把PythonScripts路径加入sys.path, 然后加载Python脚本启动模块main, 并调用其StartGame方法.

注意要让untiy支持dynamic关键字, 需要在Unity project settings/player 中把api compatibility level 调整为 .Net Framework.

来到Python一侧, main.py内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# main.py

# -*- coding: UTF-8 -*-
from clr import GameLauncher

import UnityEngine as ue
from System.IO import Path

def StartGame():
    ue.Debug.Log(Path.Combine(ue.Application.dataPath, "PythonScripts/main.py"))
   
    game = ue.GameObject.Find("Game")
    gameluncher = game.GetComponent(GameLauncher)
    ue.Debug.Log(gameluncher.name)

通过clr模块, 我们可以把自定义的类类型导入进来使用, 如果这些自定义类是在命名空间中, 则需要先导入命名空间.

关于这个话题的一些参考:

开发与调试环境

最后再简单聊一下另一个比较关键的问题, 开发与调试环境. 本来可以期待着可以All In One, 使用VS code作为开发环境, 但是Unity官方最近放弃了继续支持VS Code的插件, 具体见Official - Update on the Visual Studio Code package - Unity Forum. 所以对于C#的调试还是使用Visual Studio吧. 至于Python依然可以使用VS Code + Python插件来调试.

使用VS调试Unity的C#代码不用说是容易的, 而使用VS Code调试Python可以参考Python.NET的建议, 参考Various debugging scenarios of embedded CPython · pythonnet/pythonnet Wiki · GitHub, 我们可以使用最简单的第三种远程调试方式, 其中ptvsd已被微软所废弃, 作为替代可以使用debugpy库. 原理就是VS Code开启一个调试服务, 执行的Python脚本使用debugpy.connect进行连接调试.

VS Code新建一个Python调试配置, 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python debug",
            "type": "python",
            "request": "attach",
            "justMyCode": true,
            "listen": {
                "host": "0.0.0.0",
                "port": 5678,
            },
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "${workspaceFolder}"
                }
            ]
        },
    ]
}

然后在main.py中增加

1
2
import debugpy
debugpy.connect(("localhost", 5678))

完成后VS Code先开启调试, Unity进入Play模式运行python脚本即可, 与调试普通Python脚本相同.

Enjoy.
.

.

.

.

.

.

.

.

終わり