🐍

UnityとPythonを連携する【Python.NET】

2023/04/13に公開
4

はじめに

Python.NETを利用するとC#からPythonの関数を呼び出したり、逆にPythonからC#のメソッドを呼び出したりできるのでNumpyなどの資産をそのまま利用できます。
Unityからも問題なく使えるのですがアプリ配布先にPythonがインストールされていないと動きません。
そこでWindows限定ですがPython Embeddable PackageをStreamingAssetsフォルダに入れて配布先の環境に依存せずに実行できるプロジェクトを作ってみました。

サンプルだけを見たい方はこちらからご覧ください。
https://github.com/shiena/Unity-PythonNet

余談ですがPython Scriptingでも内部でPython.NETが使われていますがこちらはエディタ専用なのでランタイムから利用できません。

開発環境

Pythonの導入

Unityプロジェクトを作ったら以下のフォルダにPython Embeddable Packageのzipを展開したフォルダをコピーします。

Assets
+- StreamingAssets
   +- python-3.11.3-embed-amd64

pipの導入

Python Embeddable Packageはpipが入っていないので個別に導入する必要があります。
デフォルトでは実行できないのですが python311._pth ファイルの import site のコメントアウトを削除すると実行できるようになります。

変更前

python311.zip
.

# Uncomment to run site.main() automatically
#import site

変更後

python311.zip
.

# Uncomment to run site.main() automatically
import site

次にget-pip.pyを使ってインストールします。

cd Assets\StreamingAssets\python-3.11.3-embed-amd64
curl -L https://bootstrap.pypa.io/get-pip.py | .\python

pip list コマンドでインストールできているか確認します。

cd Assets\StreamingAssets\python-3.11.3-embed-amd64
.\Scripts\pip list

私の環境では以下が出力されました。

Package    Version
---------- -------
pip        23.0.1
setuptools 67.6.1
wheel      0.40.0

Pythonのライブラリをインストール

pywin32 のように sys.path を追加するライブラリがあるので、ここで必要なライブラリをインストールします。

PYTHONPATHの確認

C#側でPYTHONPATHを初期設定するのでsys.pathを確認します。

cd Assets\StreamingAssets\python-3.11.3-embed-amd64
./python -c "import sys;print('\n'.join(sys.path))"

私の環境では F:\UnityProjects\Unity-PythonNet へUnityプロジェクトを作って以下が出力されました。

F:\UnityProjects\Unity-PythonNet\Assets\StreamingAssets\python-3.11.3-embed-amd64\python311.zip
F:\UnityProjects\Unity-PythonNet\Assets\StreamingAssets\python-3.11.3-embed-amd64
F:\UnityProjects\Unity-PythonNet\Assets\StreamingAssets\python-3.11.3-embed-amd64\Lib\site-packages

Python.NETの導入

Python.NETのDLLはNuGetで提供されていますがUnityでは直接利用できないので以下のいずれかの方法で導入します。

UnityNugetを利用する

UnityNuGet はnugetパッケージの一部をパッケージマネージャとして利用できるようにScoped Registryを提供しています。そのためProject Settings > Package Managerを開き以下の設定を追加します。
UnityNuGet

name: Unity NuGet
URL: https://unitynuget-registry.azurewebsites.net
Scope(s): org.nuget

次にパッケージマネージャーを開き、マイレジストリ > Unity NuGetを開き、pythonnet をインストールします。検索欄に python を入力すると探しやすいです。

Package Manager

NuGet For Unityを利用する

NuGet For Unity はUnityからnugetを利用できるエディタ拡張です。
まずはReleases のunitypackageもしくはOpenUPM経由でupmからインストールできます。

  1. インストールしたらUnityのメニューの NuGet > Manage NuGet Package からNuGet画面を開き、pythonnetSearchボタンを押します。
  2. 先頭にpythonnetが表示されるのでリスト内右上のInstallボタンを押します。
    nuget1
  3. インストールが完了すると依存ライブラリも一緒に入るのですが不要なので Installed タブに移動して pythonnet 以外をアンインストールします。
    nuget2

nugetパッケージを展開して利用する

nugetパッケージはzipとして展開する事ができます。そのためpythonnetDownload package からnugetパッケージをダウンロードしてzipとして展開します。すると lib/netstandard2.0 フォルダに Python.Runtime.dll が含まれているのでUnityプロジェクトの Assets/Plugins フォルダへコピーします。

unzip

初期化スクリプト

以下のような初期化スクリプトを用意しました。
それぞれ以下を想定および設定しています。

  • アプリ起動時にPython.NETを初期化してアプリ終了時に停止
  • Pythonスクリプトは Assets/StreamingAssets/myproject に入れる
  • 環境変数PATHは.exeと.dllがあるパスを追加
  • 環境変数DYLD_LIBRARY_PATHはpipのdllがあるパスを設定
  • 環境変数PYTHONNET_PYDLLはpython311.dllの絶対パスを設定
  • エディタから実行する時だけ環境変数PYTHONDONTWRITEBYTECODEを設定して.pyc更新を抑制
  • PythonEngine.PythonHomeはpythonを導入した絶対パスを設定
  • PythonEngine.PythonPathはsys.pathの値を設定
using Python.Runtime;
using System;
using UnityEngine;

namespace UnityPython
{
    public static class PythonLifeCycle
    {
        private const string PythonFolder = "python-3.11.3-embed-amd64";
        private const string PythonDll = "python311.dll";
        private const string PythonZip = "python311.zip";
        private const string PythonProject = "myproject";

        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        private static void PythonInitialize()
        {
            Application.quitting += PythonShutdown;
            Initialize(PythonProject);
        }

        private static void PythonShutdown()
        {
            Application.quitting -= PythonShutdown;
            Shutdown();
        }

        public static void Initialize(string appendPythonPath = "")
        {
            var pythonHome = $"{Application.streamingAssetsPath}/{PythonFolder}";
            var appendPath = string.IsNullOrWhiteSpace(appendPythonPath) ? string.Empty : $"{Application.streamingAssetsPath}/{appendPythonPath}";
            var pythonPath = string.Join(";",
                $"{appendPath}",
                $"{pythonHome}/Lib/site-packages",
                $"{pythonHome}/{PythonZip}",
                $"{pythonHome}"
            );

            var scripts = $"{pythonHome}/Scripts";

            var path = Environment.GetEnvironmentVariable("PATH")?.TrimEnd(';');
            path = string.IsNullOrEmpty(path) ? $"{pythonHome};{scripts}" : $"{pythonHome};{scripts};{path}";
            Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.Process);
            Environment.SetEnvironmentVariable("DYLD_LIBRARY_PATH", $"{pythonHome}/Lib", EnvironmentVariableTarget.Process);
            Environment.SetEnvironmentVariable("PYTHONNET_PYDLL", $"{pythonHome}/{PythonDll}", EnvironmentVariableTarget.Process);
#if UNITY_EDITOR
            Environment.SetEnvironmentVariable("PYTHONDONTWRITEBYTECODE", "1", EnvironmentVariableTarget.Process);
#endif

            PythonEngine.PythonHome = pythonHome;
            PythonEngine.PythonPath = pythonPath;

            PythonEngine.Initialize();
        }

        public static void Shutdown()
        {
            PythonEngine.Shutdown();
        }
    }
}

サンプル

以下はmatplotlibでグラフを描画してその画像をUnityで表示するサンプルです。
matplotlibでグラフ画像のピクセル値の配列作り、そのアドレス値からUnity側で配列を読み出しているので高速に動作します。
アドレス値からの読み出しは「C#にpythonで作った処理を組み込む【pythonnetによるtensorflowモデルの組込】」が参考になりましたが、更にC#側で配列自体も参照してpythonの関数を抜けてもGCされないように工夫しています。

https://github.com/shiena/Unity-PythonNet
plot

注意点

以下はPython.NET自体の注意点です。

  • using (Py.Gil()) ブロックを同時に2つ以上実行すると後から実行した方がクラッシュするので排他制御が必要
  • PythonEngine.Shutdown() の後に再度初期化しても sys.path が更新されない

また、アドレス値からバイト列を読み出す場合は以下の注意点があります。

  • C#からnumpy配列をアドレス値から読み出す場合はメモリ上に連続したバイト列が必要なので numpy.copy() などで配列を作り直す
  • アドレス値だけを返した場合は配列本体の参照カウントが0になりpythonがいつGCしてもおかしくないので配列本体も返してC#で保持しておき読み出しが終わるまで参照カウントを増やしておく

参考

Discussion

kamekamekamekame

Unityからpythonを使用したいと思い記事を拝見いたしました。プログラミング初心者のため初歩的な質問で大変申し訳ありません。初期化スクリプトの項目で分からなくなりました。いくつか教えて頂きたいのですが、
・初期化スクリプトの置き場所はAssetes内でよろしいでしょうか?
・環境変数PATHに設定する.exeと.dllがあるパス、環境変数DYLD_LIBRARY_PATHに設定するpipのdllがあるパス、環境変数PYTHONNET_PYDLLに設定するpython311.dllのパスの探し方を教えて頂けないでしょうか?
ご多忙の中申し訳ありませんが宜しくお願い致します。

Mitsuhiro KogaMitsuhiro Koga
  • 初期化スクリプトについて、Unityで通常のスクリプトと同様に配置できます。
  • .exeと.dllのパスについて、Python Embedded Packageのzipを展開してAssets/StreamingAssetsフォルダに配置しています。この中から探してください。

また、記事中にリンクした https://github.com/shiena/Unity-PythonNet は動作するプロジェクトなのでこちらも参考にしてください。

kamekamekamekame

早速のご返答ありがとうございます。GitHubの方を参考にさせて頂き学んでいきたいと思います。ありがとうございます。

Mitsuhiro KogaMitsuhiro Koga

1つ注意する事があり、pipで追加するライブラリにネイティブライブラリが入っているとsys.pathの結果が増えます。
そのため必要なライブラリをpipでインストールした後のsys.pathをスクリプト側のPythonEngine.PythonPathへ追加してください。