65
64

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

QualiArtsAdvent Calendar 2020

Day 18

Python for Unityを使ってみる

Last updated at Posted at 2020-12-17

はじめに

この記事はQualiArts Advent Calendar 2020の18日目の記事になります。

去年のアドカレでは「テクニカルアーティストが3Dのワークフローを改善した話」と題してQualiArtsにおけるテクニカルアーティスト(TA)の業務に関してお話しましたが、今年なんと新たに「テクニカルアーティスト(TA)室」という組織が新設されました。
20年度入社の新卒TAも生まれ、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

また、PackageManagerにも表示されるようになり、こちらからはサンプルをインポートすることもできます。
Package Manager

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

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

Mayaを使用している場合、「mayapy.exe」を指定すれば楽してPySideも使用可能になります。
PythonSettings
詳しくは以下をご覧ください。

Python for Unity - Installation

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

Python Consoleを使ってみる

上部がログ表示エリア、下部がコード記述エリアになります。

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
Python Consoleの上部のログ表示エリアとConsoleにログが表示されました。

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

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

Python Consoleで書いたコードを簡単に実行できるようにする

Python Console上部の「Save & Create Shortcut」をクリックします。
Save & Create Shortcut

ダイアログが表示されるので、メニューバーに表示するラベルを「/」区切りで記述します。
Create Menu Shortcut
スクリプトの保存場所を選択するダイアログが表示されるので、適当な場所を指定して完了です。

メニューバーに指定したラベルでメニューが追加されました。かんたんですね!
Menu Created

さて、これはどのように実現しているのでしょうか。
保存したディレクトリを見てみると…
Created scripts
保存したPythonファイルと同名で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
Package Managerの「PySide Camera Selector」の「Import into Project」をクリックすると、PySideのサンプルがImportされます。

PySide Files
ImportされるファイルはAssemblyDefinition、C#スクリプト、Pythonスクリプト、PySide用UIファイルです。

Python>Examples>PySide Exampleから起動できます。
PySide Example
挙動としては以下のような感じです。

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

PySideExample.cs

[MenuItem("Python/Examples/PySide Example")]
public static void OnMenuClick()
{
    PythonRunner.SpawnClient(
            file: $"{__DIR__()}/PySideExample.py", 
            wantLogging: true);
}

こんな感じでPythonを実行しています。
先程と違い、SpawnClientを使用することで新しいPythonインタプリタを作成します。
なので、ファイルパスもプロジェクトルートからの相対パスではなくフルパスになっています。

引数などに関しては以下をご参照ください。
SpawnClient

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

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との連携には以下のモジュールを使用します。

import unity_python.client.unity_client as unity_client

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

PySideTestClientService

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

def exposed_client_name(self):
    return "PySide Example"

def exposed_on_hierarchy_changed(self):
    _PYSIDE_UI.populate_camera_list()

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

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を使用する方法です。

camera = self.service.UnityEngine.GameObject.Find('{}'.format(selected_items[0].text()))

このserviceは上の PySideTestClientService のインスタンスです。
これを経由してUnityEngineやUnityEditorの機能を使用することができます。

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

self.connection.root.execute(inspect.cleandoc(
    """
    import UnityEditor
    UnityEditor.EditorApplication.ExecuteMenuItem('GameObject/Align View to Selected')
    """))

Python Consoleと同じ書き方でコードを埋め込んで実行する形になります。
コメントを見る限り、serviceを使用する方法よりこちらのほうが高速で動作するようです。

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

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-testcom.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.

コードを書いていく

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にパスを通す必要があります。
既に何らかの自前のツールがある場合はそちらでパスを通すようにしてもよいですが、今回は手っ取り早くスクリプトエディタで実行する形にします。

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

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

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

おわり

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

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

Python for Unityに関する情報は以下のフォーラムをご覧ください。
External Tools Previews
Python for Unityを活用したShotgunとの連携を行うプラグインに関するスレッドもあるので、非常に実装の参考になると思います。

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

65
64
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
64

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?