Python for Unityを使ってみる

# はじめに
この記事は[QualiArts Advent Calendar 2020](https://qiita.com/advent-calendar/2020/qualiarts)の18日目の記事になります。

去年のアドカレでは「[テクニカルアーティストが3Dのワークフローを改善した話](https://qiita.com/tm8r/items/6ff27659700a51a5207b)」と題してQualiArtsにおけるテクニカルアーティスト（TA）の業務に関してお話しましたが、今年なんと新たに「テクニカルアーティスト（TA）室」という組織が新設されました。
[20年度入社の新卒TA](https://yshio.hatenablog.com/entry/2020/12/12/000000)も生まれ、TA組織の規模と業務範囲を広げながら、引き続きプロジェクト横断で活動しています。

このTA室の業務の一環として、Unity公式で開発されている「Python for Unity」について調査したので、本稿ではこちらを紹介します。

PythonはMaya、Houdini、Blenderなど多くの3Dツールで使用でき、事実上TA、TD（テクニカルディレクター）の標準言語となっています。
「Python for Unity」もこの流れに則ってパイプラインTAのために開発されています。
（UE4も同様の理由でプラグインがリリースされていますね）
そのため、実際のゲームにおいてPythonスクリプトによる制御をすることはスコープに入っていません。

# Python for Unityをインストールする
Package Managerでインストール…と言いたいところですが、まだ出てこないので、導入したいUnityプロジェクト直下のPackagesディレクトリにあるmanifest.jsonを開き、dependenciesに以下を追記します。

```
{
  "dependencies": {
    （略）
    "com.unity.scripting.python": "2.1.1-preview.1"
  }
}
```

追記して保存したらUnityを開けばWindow>Generalに「Python Console」が追加されます。
![Start Python Conscole](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/bf93ea32-6263-1ee8-40d2-ad6f0f2c0361.png)

また、PackageManagerにも表示されるようになり、こちらからはサンプルをインポートすることもできます。
![Package Manager](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/3d516a02-12c2-05c4-8572-5663639c82dc.png)

次に使用するPythonの実行ファイルを指定します。

Edit>Project Settings>Python for Unityを選択
「…」をクリックしてpythonの実行ファイルを指定します。

Mayaを使用している場合、「mayapy.exe」を指定すれば楽してPySideも使用可能になります。
![PythonSettings](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/1a495e18-45f3-819f-5ded-5b3647bf9fb4.png)
詳しくは以下をご覧ください。


[Python for Unity - Installation](https://docs.unity3d.com/Packages/com.unity.scripting.python@2.1/manual/installation.html)

あとは指示に従ってUnityを再起動すればインストールは完了です。

# Python Consoleを使ってみる
上部がログ表示エリア、下部がコード記述エリアになります。

```python
import UnityEditor
import UnityEngine

guids = UnityEditor.AssetDatabase.FindAssets("t:prefab", None)
for guid in guids:
    path  = UnityEditor.AssetDatabase.GUIDToAssetPath(guid)
    print(str(path))
    UnityEngine.Debug.Log(path)
```
このようなコードを書いて「Execute」を実行、もしくはCtrl+Enterをすると…

![Execute Python Console](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/6af176ee-cc71-dbb2-129b-7c64f5a8b9fa.png)
Python Consoleの上部のログ表示エリアとConsoleにログが表示されました。

この通り、UnityEditorやUnityEngineというモジュールをimportすることで、普段書いているC#のコードをPythonに置き換えてUnityを操作することができます。


次にこれをPythonを書けない人でも簡単に実行できるようにしましょう。

# Python Consoleで書いたコードを簡単に実行できるようにする
Python Console上部の「Save & Create Shortcut」をクリックします。
![Save & Create Shortcut](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/ee7353a8-e00b-d668-0c1a-dcfc0a8f6faa.png)

ダイアログが表示されるので、メニューバーに表示するラベルを「/」区切りで記述します。
![Create Menu Shortcut](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/99f6a4f5-f781-bac7-7d29-255d988f5f49.png)
スクリプトの保存場所を選択するダイアログが表示されるので、適当な場所を指定して完了です。

メニューバーに指定したラベルでメニューが追加されました。かんたんですね！
![Menu Created](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/b82b7be1-8375-9f72-836c-ace3f3c8e309.png)

さて、これはどのように実現しているのでしょうか。
保存したディレクトリを見てみると…
![Created scripts](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/73846337-ee30-cf49-4100-d072c17b8184.png)
保存したPythonファイルと同名でC#ファイルが作成されています。

中身を見てみると…

```c#
using UnityEditor;
using UnityEditor.Scripting.Python;

public class MenuItem_SearchPrefabs_Class
{
   [MenuItem("Python Scripts/SearchPrefabs")]
   public static void SearchPrefabs()
   {
       PythonRunner.RunFile("Assets/Scripts/search_prefabs.py");
       }
};
```

普通にエディタ拡張でメニューを作るのと同様に「MenuItem」を使用したコードが自動生成されています。
作成したpythonファイルは「PythonRunner.RunFile」でパスを指定する形で実行されていることがわかります。

というわけで、Python Consoleから保存せずとも、自身でお好みのエディタでPythonスクリプトを作成し、それを「PythonRunner.RunFile」を用いて実行することで同様の挙動が得られそうですね！

# PySide Camera Selectorを読んでみる
![Import PySide Example](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/4ce208a1-2b70-3043-129a-a0d9cd0c1d85.png)
Package Managerの「PySide Camera Selector」の「Import into Project」をクリックすると、PySideのサンプルがImportされます。

![PySide Files](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/5f6769e0-01ae-24fa-96bd-2ce61818755b.png)
ImportされるファイルはAssemblyDefinition、C#スクリプト、Pythonスクリプト、PySide用UIファイルです。

Python>Examples>PySide Exampleから起動できます。
![PySide Example](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/59ab5407-cf77-a9f9-0eaa-db86e9974122.png)
挙動としては以下のような感じです。

- Hierarchyに存在するカメラをリスト表示
- 「Use Camera」をクリックするとリストでアクティブなカメラを選択し、「GameObject/Align View to Selected」を実行
- Hierarchyの変更を検知してリストを自動更新

## PySideExample.cs
```C#
[MenuItem("Python/Examples/PySide Example")]
public static void OnMenuClick()
{
    PythonRunner.SpawnClient(
            file: $"{__DIR__()}/PySideExample.py", 
            wantLogging: true);
}
```
こんな感じでPythonを実行しています。
先程と違い、SpawnClientを使用することで新しいPythonインタプリタを作成します。
なので、ファイルパスもプロジェクトルートからの相対パスではなくフルパスになっています。

引数などに関しては以下をご参照ください。
[SpawnClient](https://docs.unity3d.com/Packages/com.unity.scripting.python@2.1/api/UnityEditor.Scripting.Python.PythonRunner.html#UnityEditor_Scripting_Python_PythonRunner_SpawnClient_System_String_System_Boolean_System_String___)

このスクリプトのもう1つの役割はイベントのハンドリングです。

```C#
static public void Subscribe()
{
    EditorApplication.hierarchyChanged -= OnHierarchyChanged;
    EditorApplication.hierarchyChanged += OnHierarchyChanged;
}

public static void OnHierarchyChanged()
{
    PythonRunner.CallAsyncServiceOnClient("PySide Example", "on_hierarchy_changed");
}
```

hierarchyChangedにこれをフックすることで、Python側のコードを実行しています。
第一引数はクライアントの名前、第二引数はサービスの名前になるのですが、詳しくはこのあとで説明します。

## PySideExample.py
Python側には以下の2つのクラスがあります。

* PySideTestClientService
* PySideTestUI

Unityとの連携部分以外はPySideを利用した見慣れた感じの構成です。

Unityとの連携には以下のモジュールを使用します。

```python
import unity_python.client.unity_client as unity_client
```

基本的には `unity_client.UnityClientService` を継承したクラスを作成し、このクラスでUnityと接続してやりとりを行う形になります。


### PySideTestClientService
というわけで `unity_client.UnityClientService` を継承したこのクラスを見ていきます。

```python
def exposed_client_name(self):
    return "PySide Example"

def exposed_on_hierarchy_changed(self):
    _PYSIDE_UI.populate_camera_list()
```

先頭にこんな感じのコードが記述されていますが、「PySideExample.cs」のコードを改めて見てみましょう。

```C#
PythonRunner.CallAsyncServiceOnClient("PySide Example", "on_hierarchy_changed");
```

`CallAsyncServiceOnClient` の第一引数であるクライアント名は `exposed_client_name` が返す値に相当します。
したがって、docstringにも書いてありますが、この値は他と被らない一意なものを指定する必要があります。

次に`CallAsyncServiceOnClient` の第二引数ですが、こちらに対して「exposed_」接頭辞をつけたメソッドが実行されます。
したがって、 `on_hierarchy_changed` を指定した場合、このサンプルの通り `exposed_on_hierarchy_changed` を実装する形になります。

### PySideTestUI
これはいわずもがな、UIを制御するクラスです。
PySideに関しては本題から逸れるので特に触れず、Unity連携部分を見ていきます。

ざっと読んでいくとUnity側のコード実行の方法が大きく2パターンあることがわかります。
まずパターン1、serviceを使用する方法です。

```python
camera = self.service.UnityEngine.GameObject.Find('{}'.format(selected_items[0].text()))
```
このserviceは上の `PySideTestClientService` のインスタンスです。
これを経由してUnityEngineやUnityEditorの機能を使用することができます。

次にパターン2、connectionを使用する方法です。

```python
self.connection.root.execute(inspect.cleandoc(
    """
    import UnityEditor
    UnityEditor.EditorApplication.ExecuteMenuItem('GameObject/Align View to Selected')
    """))
```
Python Consoleと同じ書き方でコードを埋め込んで実行する形になります。
コメントを見る限り、serviceを使用する方法よりこちらのほうが高速で動作するようです。

第2引数を指定すれば指定コード内で使用した変数を辞書で取得することができます。

```python
vars = self.connection.root.dict()
self.connection.root.execute(inspect.cleandoc(
    """
    import UnityEngine
    cameras = [x.name for x in UnityEngine.Camera.allCameras]
    """), vars)
camera_list = vars["cameras"]
```

ちなみにC#側のSubscribeメソッドもconnection経由でpythonから実行しています。

## PySide Camera Selectorまとめ
というわけで、公式のPySideのサンプルは

1. メニューバーから実行するとC#でPythonを実行
2. 実行されたPythonの中でC#のSubscribeメソッドを実行
3. Unityのイベント発行時にPythonのメソッドを実行

というような流れになっていることがわかりました。
Unity側からの実行とUnityのイベント発行時のPython実行が不要あれば、Unity側に特にコードを用意する必要もなさそうです。

# MayaからUnityの情報にアクセスしてみる
なんとなく構成がわかったところでMayaからUnityの情報にアクセスする方法について考えてみます。

## unity_pythonにパスを通す
早速コードを書いていきたいところですが、unity_pythonモジュールはどこにあるんでしょうか。
調べてみると、どうやらPython for Unityを導入したプロジェクトの `Library/PackageCache` 以下にあるようです。

プロジェクトが `C:/Users/{ユーザー名}/Documents/dev/unity/python-test` で `com.unity.scripting.python@2.1.1-preview.1` を導入した場合、具体的には以下になります。

```
C:/Users/{ユーザー名}/Documents/dev/unity/python-test/Library/PackageCache/com.unity.scripting.python@2.1.1-preview.1/Python~/site-packages
```

これをIDEのパスに追加することである程度のオートコンプリートもきいて書きやすくなりました。
参考：[Python for Unity first impression and VSCode setup.](https://forum.unity.com/threads/python-for-unity-first-impression-and-vscode-setup.815502/)

## コードを書いていく
```python
import unity_python.client.unity_client as unity_client

class MayaUnityClientService(unity_client.UnityClientService):
    def exposed_client_name(self):
        # 一意な名前を返す
        return "MayaUnityClient"

    def search_cameras(self):
        cameras = [x.name for x in service.UnityEngine.Camera.allCameras]
        print(cameras)


service = MayaUnityClientService()
client = unity_client.connect(service)
service.search_cameras()
# 終わったら切断
client.close()
```

前述の通り `UnityClientService` を継承したクラスを用意し、 `exposed_client_name` で一意な名前を返します。
`search_cameras` というHierarchy上に存在するカメラを取得して表示するだけのメソッドを用意。
あとはこのインスタンスを生成してUnityに接続して `search_cameras` を実行したのち切断するだけのかんたんな実装です。

## Mayaで実行する
Mayaで実行する際ももちろんunity_pythonにパスを通す必要があります。
既に何らかの自前のツールがある場合はそちらでパスを通すようにしてもよいですが、今回は手っ取り早くスクリプトエディタで実行する形にします。

というわけで以下のようなコードを用意してパスを通します。

```python
import sys
script_path = "C:/Users/{ユーザー名}/Documents/dev/unity/python-test/Library/PackageCache/com.unity.scripting.python@2.1.1-preview.1/Python~/site-packages"
if script_path not in sys.path:
    sys.path.insert(0, script_path)
```

その上で先程用意したコードを実行すればMayaからUnityの情報にアクセスすることができます。
![Exec Python on Maya](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18707/f4b000ee-7491-5e48-c9a3-49a7cfd4b11a.png)

無事 `[u'Main Camera', u'UICamera3', u'Camera']` と表示されました！

# おわり
まだExperimentalではありますが、UnityをPythonで操作することができました。
今回はMayaからUnityの情報にアクセスする方法を紹介しましたが、mayapyを使用することでその逆も可能です。

UnityからMayaシーンにアクセスして再エクスポートの実行などもできそうで、夢が広がりますね！
また、Unityは標準で小さなコードを実行する術がスクリプトファイルの共有以外にないので、Python Consoleによってコードを各々の環境でコピペすることで簡単にコードが実行できるというのも嬉しいシーンがありそうです。

Python for Unityに関する情報は以下のフォーラムをご覧ください。
[External Tools Previews](https://forum.unity.com/forums/external-tools-previews.441/)
Python for Unityを活用したShotgunとの連携を行うプラグインに関するスレッドもあるので、非常に実装の参考になると思います。

正式リリースに期待しつつ、引き続き活用方法を考えていく予定なので、また知見がたまったら共有できればと思います。