20190828のUnityに関する記事は7件です。

Unity ML-Agentsで強化学習をやってみる。(環境構築〜サンプル実行編)

サンプル的にブロック崩しの設定厳しめゲームを作った。機械学習の勉強も兼ねて強化学習を学んでみようと、このゲームをML-Agentsを使って学習させてみようと試みた記事。。。
始まりはこの本。
 https://www.borndigital.co.jp/book/6702.html
 しかし・・・ml-Agents色々変わってるし。。結局GitHubのドキュメントみながら一つ一つやっていくことになる。。
 (強化学習の概要を、マップ探索で説明してあって個人的にはわかりやすかった。)

環境構築

OS:macOS Mojave(10.14.5)

Unity

まずは、Unityのインストール。既に2018.03が入っていたがバージョンアップしてみる。。(バージョンアップで非対応コードが出ないか心配。。)=>Unity-Hubを導入。
 https://unity3d.com/get-unity/update
今まで使ってなかったけど、各種バージョンを一元管理できるので便利ですね。
(前バージョンからのマイグレーションも自動。。)
※大したことしていなかったので、マイグレーションが問題なく完了。

ML-Agents

UnitySDKのサンプルを動かしてみる

 次は、ML-Agents Toolkitをclone。適当にディレクトリを作成して。

 git clone https://github.com/Unity-Technologies/ml-agents.git

 落としてきたら、Unityでサンプルを動かしてみる。

  • UnityHubで「リスト追加」→ml-agents/UnitySDKを開く
  • Unityバージョンを最新に設定

開いたあとは(とりあえず3DBallでも・・・)

  • Assets/ML-Agents/Examples/3DBall/Scenesの3DBallを開いてシーンをロード
  • Ball 3D AcademyにセットされいてるBrains Assets/ML-Agents/Examples/3DBall/Brains/3DBallLearningのModelにAgents/Examples/3DBall/TFModels/3DBallLearningをセッ
  • Playを実行

 プレートがバランスを取りながらボールを落とさないように動くサンプルが確認できる!!

PythonとmlagentsPackageを入れる

 Pythonは既にpyenvを入れていたので、ml-agentsを入れたディレクトリ配下のpython環境を3.6.6に設定

$ pyenv local 3.6.6
$ python --version
Python 3.6.6
$ pip3 install mlagents

※ここで”You are using pip version 10.0.1, however version 19.1.1 is available.”アップグレードしろと。

$ pip3 install --upgrade pip setuptools

※依存関係の諸々で”pip3 install --upgrade pip ”だとうまく更新できず。。色々ググったらこれでいけた。(upgradeの方にもpip"3"付けたくなりますよね。。。)
あとはそれぞれのフォルダ配下でインストール

$ cd ml-agents-envs
$ pip3 install -e ./
$ cd ..
$ cd ml-agents
$ pip3 install -e ./

ml-agentsを起動して学習の確認

Unity側

AcademyのControlチェックボックスのチェックを入れる(入れないと学習したBrainsをロードして動く)
スクリーンショット 2019-08-28 20.24.58.png

ml-agent側:

$ mlagents-learn ../config/trainer_config.yaml --run-id=sample[これはなんでもOK] --train

スクリーンショット 2019-08-28 20.29.54.png

こんな具合に実行されてれ以下メッセージが出るのでUnity側を実行すると動く!!

INFO:mlagents.envs:Start training by pressing the Play button in the Unity Editor.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 1000. Time Elapsed: 11.224 s Mean Reward: 1.207. Std of Reward: 0.728. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 2000. Time Elapsed: 22.257 s Mean Reward: 1.296. Std of Reward: 0.759. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 3000. Time Elapsed: 33.052 s Mean Reward: 1.523. Std of Reward: 0.838. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 4000. Time Elapsed: 43.845 s Mean Reward: 1.806. Std of Reward: 1.128. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 5000. Time Elapsed: 54.400 s Mean Reward: 2.777. Std of Reward: 1.975. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 6000. Time Elapsed: 65.174 s Mean Reward: 3.922. Std of Reward: 3.448. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 7000. Time Elapsed: 75.998 s Mean Reward: 7.204. Std of Reward: 6.900. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 8000. Time Elapsed: 86.734 s Mean Reward: 9.579. Std of Reward: 10.298. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 9000. Time Elapsed: 97.146 s Mean Reward: 17.802. Std of Reward: 18.521. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 10000. Time Elapsed: 108.207 s Mean Reward: 22.553. Std of Reward: 23.049. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 11000. Time Elapsed: 119.127 s Mean Reward: 54.259. Std of Reward: 35.250. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 12000. Time Elapsed: 129.911 s Mean Reward: 58.905. Std of Reward: 35.462. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 13000. Time Elapsed: 140.319 s Mean Reward: 64.107. Std of Reward: 38.436. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 14000. Time Elapsed: 151.080 s Mean Reward: 73.244. Std of Reward: 33.077. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 15000. Time Elapsed: 161.689 s Mean Reward: 74.694. Std of Reward: 35.027. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 16000. Time Elapsed: 172.868 s Mean Reward: 94.123. Std of Reward: 20.358. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 17000. Time Elapsed: 183.989 s Mean Reward: 95.277. Std of Reward: 16.361. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 18000. Time Elapsed: 193.713 s Mean Reward: 94.962. Std of Reward: 17.454. Training.
INFO:mlagents.trainers: ponponRun-0: 3DBallLearning: Step: 19000. Time Elapsed: 204.282 s Mean Reward: 86.887. Std of Reward: 22.600. Training.

スクリーンショット 2019-08-28 20.31.44.png
初めは落とす落とす。。。
からの
スクリーンショット 2019-08-28 20.31.44.png
プレートがバランスを取るようになるのがわかる。。。

学習したファイルのロード

学習ファイルは以下フォルダに格納される。

 ./models/sample[起動時に指定したrun-id]/3DBallLearning.nn

ファイルをUnity側プロジェクトのAssetsにコピーして以下で読み込む。

スクリーンショット 2019-08-28 20.42.23.png

・AcademyのControlチェックを外す
・Brainsをダブルクリックする

スクリーンショット 2019-08-28 20.44.24.png

3DBallLearningがInspectorに表示されるので、ここのModelにコピーしたXXXX.nnファイルをドラック&ドロップする。

最後に実行すれば学習したデータが読み込めます。

参考

 環境構築および動作確認
 https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Installation.md
 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityのVR SamplesのMazeでキャラクターを変更する

はじめに

前回記事 UnityのVR SamplesのMazeでカメラの位置をキャラクターに追従する の続きです。
今回は、キャラクターを変更してみたいと思います。
変更するキャラクターとしては、VRM形式の「ニコニ立体ちゃん」を使用します。

開発環境

Unity 2019.2.0f1
VR Samples - 1.4
Oculus Integration for Unity - 1.39
UniVRM v0.53.0
「ニコニ立体ちゃん」(VRM版)

ビルド手順

前回記事で作成したプロジェクトで引き続き作業します。

  1. 今回もThe Mazeを改造するので、サンプルのシーンMazeを開きます。
  2. まずはVRM形式のモデルのファイルを読み込むためのアセットUniVRMをvrm-c/UniVRM からダウンロードします。自分がダウンロードしたファイルは"UniVRM-0.53.0_6b07.unitypackage"ですが、数字の部分はバージョンにより変わります。
  3. Assets -> Import Package -> Custom Packageからファイル"UniVRM-0.53.0_6b07.unitypackage"を指定してインポートします。
  4. 次に、キャラクターのファイルをニコニ立体ちゃん (VRM) からダウンロードします。
  5. Projectウインドウで、Assetsフォルダの下にキャラクターを配置するフォルダ(今回はModelsとします)を作成し、ダウンロードしたファイル"AliciaSolid.vrm"エクスプローラーからドラッグアンドドロップすると、プレファブができます。
  6. できたプレファブをHierarchyウインドウのCharactersの下にドラッグアンドドロップします。image.png
  7. CharactersのMazeCharacterからAliciaSolidに赤枠で囲んだコンポーネントをコピーします。Inspectorウインドウ上で1個ずつ右クリックで"Copy Component"して"Paste Component As New"でコピーします。TransformとAnimatorの設定も合わせます。image.png
  8. Third Person Charactorコンポーネントの設定も元の値と変わっていた場合、元の値にあわせておきます(AI Charactor ControlでRequireComponentされているため、コピーする順番によりデフォルトの値になるようです)。Ground Check Distanceの値が0.1になっていると、キャラクターの動作が不自然になります。
  9. MazeCharacter側のチェックを外して無効にします。image.png
  10. あとは、あちこちで設定されているMazeCharacterへの参照をAliciaSolidに直していきます。image.png
  11. すべて変更したら、ビルド設定からビルドします。

com.oculus.vrshell-20190828-203156_Trim_300x300_fps10.gif

さいごに

今回キャラクターを変更することができたのですが、変更する箇所が多く、結構手間がかかりました。
VRMのファイルを配置するだけで簡単に変更できるようにする方法がないか、引き続き検討してみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでオンラインマルチプレイなゲームを作りたい その6 ルーム入室

前回の記事でルームの作成が行えるようになりました。
今回は作成されたルームへ入室を行う処理を作っていきます。

今回の目標

Untitled Diagram_4.png
赤〇で囲っている部分を作ります。

イメージ図
※ルーム名は気にしないでください。
キャプチャ6.JPG
ルームリストから選んで入室を行うものと、プライベート設定になっている部屋を指定して入室を行うものの2パターンを実装しています。

ルームリストから入室

http://www.monobitengine.com/doc/mun/contents/FeatureClient/JoinRoom.htm

MonobitEngine.MonobitNetwork.JoinRoom("roomName");

引数に入室したいルーム名を入力して使用するだけで任意のルームへ入室できます。

http://www.monobitengine.com/doc/mun/contents/FeatureClient/GetRoomData.htm

MonobitEngine.MonobitNetwork.GetRoomData()

入室可能なルーム群をRoomData[]型で取得できます。

MonobitEngine.MonobitNetwork.GetRoomDataで取得したものをリストとして表示し、選択したルームからルーム名取得しMonobitEngine.MonobitNetwork.JoinRoomの引数に入れてあげるという形で実装していきます。

まず、ルームリストに並べるボタンを作ります。

ScrollViewButton.cs
   /// <summary></summary>
    public class ScrollViewButton : UnityEngine.MonoBehaviour
    {
        /// <summary></summary>
        private RoomData m_Data;

        /// <summary>ボタンにルーム情報を適用する</summary>
        /// <param name="roomData">ボタンに適用させたいルームの情報</param>
        public void Set(RoomData roomData)
        {
            m_Data = roomData;

            gameObject.GetComponent<Button>().onClick.AddListener(OnClick);
            gameObject.GetComponentInChildren<Text>().text = m_Data.name + ", " + m_Data.playerCount + "/4";
        }

        /// <summary>ボタンクリック時に適用されたルーム情報を元に任ルームへ入室する</summary>
        private void OnClick()
        {
            MonobitNetwork.JoinRoom(m_Data.name);
        }
    }

uGUIのボタンをプレハブ化したものにアタッチして使ってください。
ルームリストに表示する際にルームデータを貰い、クリックされたときに貰ったルームデータを元に入室を行うようにしています。

次にボタンを並べるリストを作ります。
※リスト内に表示するボタンの位置調整などの処理は省いています。

ScrollView.cs
    /// <summary>ルームリスト及びボタンを表示するスクロールビュー</summary>
    public class ScrollView : UnityEngine.MonoBehaviour
    {
        /// <summary>ルームリストに表示するボタンのプレハブ</summary>
        private GameObject m_ButtonPrefub;

        /// <summary>ルームリストに表示するボタンのオブジェクトのリスト</summary>
        private List<GameObject> m_ButtonsList;

        // Start is called before the first frame update
        void Start()
        {
            m_ButtonPrefub = Resources.Load("UI_ScrollViewButton") as GameObject;

            m_ButtonsList = new List<GameObject>();

            UpdateRoomData();
        }

        /// <summary>ルームリストを更新する</summary>
        public void UpdateRoomData()
        {
            int roomCount = MonobitNetwork.GetRoomData().Length;
            for (int i = 0; i < roomCount; i++)
            {
                RoomData roomData = MonobitNetwork.GetRoomData()[i];

                // パスワードが設定されている = 非公開ルームとして扱うのでリスト表示から除外
                if (!roomData.customParameters["password"].Equals("empty")) { continue; }

                // リスト上に並べるボタンを生成
                GameObject button = (GameObject)Instantiate(m_ButtonPrefub);

                // ボタンにルーム情報を適用する
                button.GetComponent<ScrollViewButton>().Set(MonobitNetwork.GetRoomData()[i]);

                m_ButtonsList.Add(button);
            }
        }

        /// <summary>ルームリストを削除する</summary>
        public void DestroyAll()
        {
            int length = m_ButtonsList.Count;

            for (int i = 0; i < length; ++i)
            {
                if(m_ButtonsList[i] == null) { continue; }

                Destroy(m_ButtonsList[i]);
            }

            m_ButtonsList.Clear();
            m_ButtonsList.TrimExcess();
        }
    }

public void UpdateRoomData()がメインの処理になります。
これでルームリストにルーム情報が設定されたボタンが並べられます。
ボタンを押すと適用されたルームへ入室してくれます。

/// <summary></summary>
private void OnClickUpdateRoomList()
{
    ScrollView scrollView = m_ScrollViewObj.GetComponent<ScrollView>();

    scrollView.DestroyAll();

    scrollView.UpdateRoomData();
}

更新ボタンを作り、このようにしてルームリストの情報を任意で更新するようにするといいですね。

プライベート設定にしている部屋への入室

ルームリストを作ったときと同様で、MonobitEngine.MonobitNetwork.JoinRoomMonobitEngine.MonobitNetwork.GetRoomDataを使い実装していきます。

SceneJoinRoom.cs
public class SceneJoinRoom : MonobitEngine.MonoBehaviour
{
    /// <summary>ルーム名</summary>
    private string m_RoomName = string.Empty;

    /// <summary>パスワード</summary>
    private string m_Password = string.Empty;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inLobby) { Debug.Log("Not in lobby"); }

       /******** 表示するuGUIの初期化/配置等の処理 ******/
    }

    /// <summary>ルーム名入力時に呼ばれる</summary>
    /// <param name="roomName">入力された文字列</param>
    private void OnInputRoomName(string roomName)
    {
        m_RoomName = roomName;
    }

    /// <summary>パスワード入力痔に呼ばれる</summary>
    /// <param name="password">入力された文字列</param>
    private void OnInputPassword(string password)
    {
        m_Password = password;
    }

    /// <summary>作成ボタンが押された際に呼ばれる</summary>
    private void OnClickJoin()
    {
        RoomData[] roomData = MonobitNetwork.GetRoomData();

        int length = roomData.Length;
        for (int i = 0; i < length; ++i)
        {
            // 入りたいルームの名前に該当するルームを探し出す
            if (!roomData[i].name.Equals(m_RoomName)) { continue; }

            // 入りたいルームに設定されているパスワードを入力したパスワードと一致するかを確認する
            if (!roomData[i].customParameters["password"].Equals(m_Password))
            {
                Debug.Log("Incorrect Password ");

                return;
            }

            MonobitNetwork.JoinRoom(m_RoomName);

            break;
        }
    }
}

プライベート設定になっている入りたいルームの名前をMonobitEngine.MonobitNetwork.GetRoomDataで取得したルーム一覧から探し出し、見つかったルームに設定されているパスワードを、入力されたパスワードが一致するかを確認しています。
これで、パスワード設定がされた非公開ルームへの入室が実装できました。

仕上げにルーム入室時/失敗時/サーバー切断時のコールバックを実装していきます。
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnJoinedRoom%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

public void OnJoinedRoom()

ルーム入室に成功した際に呼ばれます。

http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnJoinRoomFailed%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

public void OnJoinRoomFailed(object[] codeAndMsg)

ルーム入室に失敗した際に呼ばれます。

コードにコールバックを追加します。

SceneJoinRoom.cs
public class SceneJoinRoom : MonobitEngine.MonoBehaviour
{
    /// <summary>ルーム名</summary>
    private string m_RoomName = string.Empty;

    /// <summary>パスワード</summary>
    private string m_Password = string.Empty;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inLobby) { Debug.Log("Not in lobby"); }

       /******** 表示するuGUIの初期化/配置等の処理 ******/
    }

    /// <summary>ルーム入室成功時に呼ばれる</summary>
    private void OnJoinedRoom()
    {
        SceneManager.LoadScene("InGame");
    }

    /// <summary>ルーム入室失敗時に呼ばれる</summary>
    private void OnJoinRoomFailed(object[] codeAndMsg)
    {
        Debug.Log("Join Room Failed : errorCode = " + codeAndMsg[0] + ", message = " + codeAndMsg[1]);
    }

    /// <summary>MUNサーバーとの接続を切った際に呼ばれるコールバック</summary>
    private void OnDisconnectedFromServer()
    {
        SceneManager.LoadScene("Title");
    }

    /// <summary>ルーム名入力時に呼ばれる</summary>
    /// <param name="roomName">入力された文字列</param>
    private void OnInputRoomName(string roomName)
    {
        m_RoomName = roomName;
    }

    /// <summary>パスワード入力痔に呼ばれる</summary>
    /// <param name="password">入力された文字列</param>
    private void OnInputPassword(string password)
    {
        m_Password = password;
    }

    /// <summary>作成ボタンが押された際に呼ばれる</summary>
    private void OnClickJoin()
    {
        RoomData[] roomData = MonobitNetwork.GetRoomData();

        int length = roomData.Length;
        for (int i = 0; i < length; ++i)
        {
            // 入りたいルームの名前に該当するルームを探し出す
            if (!roomData[i].name.Equals(m_RoomName)) { continue; }

            // 入りたいルームに設定されているパスワードを入力したパスワードと一致するかを確認する
            if (!roomData[i].customParameters["password"].Equals(m_Password))
            {
                Debug.Log("Incorrect Password ");

                return;
            }

            MonobitNetwork.JoinRoom(m_RoomName);

            break;
        }
    }
}

ルーム入室成功時に次の画面へ遷移するようにしています。
失敗時は失敗内容をログに出力しています。
サーバーが切断された際の処理もついでに追加しています。

これでルーム入室ができるようになりました。
次回はゲーム開始前の、ルーム待機室のようなものを作っていきます。

参考

http://www.monobitengine.com/doc/mun/contents/FeatureClient/JoinRoom.htm
http://www.monobitengine.com/doc/mun/contents/FeatureClient/GetRoomData.htm
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnJoinedRoom%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnJoinRoomFailed%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】子の要素をFindを使わずにFor文で取得する。

はじめに(追記)

追記 : コメントでさらにスマートな処理を書いていただいたので、そちらも併せて紹介しておきます。


GameObjectをFindで取得するには、当然Findするオブジェクトを指定しなければいけないため、
取得したいオブジェクトの数だけ分が増えます。これが数が増えてくると面倒くさい...。

気付けばこんな事に...
コメント 2019-08-28 144732.png

これをなんとか解消したいと思い、色々試した結果For文を使って短く、それでいて管理が楽にできたので
記事としてまとめようと思った次第です。

Script(追記)

追記 :追記のスクリプト

GetChildNext.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GetChild : MonoBehaviour
{
    public GameObject[] Parents;
    List<GameObject> Paneles;

    void Start()
    {
        Paneles = new List<GameObject>();

        foreach(GameObject p in Parents)
        {
            foreach(Transform child in p.transform)
            {
                Paneles.Add(child.gameObject);
            }
        }

        for(int i = 0; i < Paneles.Count; i++)
        {
            Debug.Log(Paneles[i]);
        }
    }
}

この処理では親を1、子を2、子の子を3としたとき、Parentsにセットしている子を取得するので、
1をセットしている場合、2のオブジェクトは取得されるが、3は取得されません。
(下のGetChild.csでも同じです)

3を取得したい場合は2をセットすれば取得できます。


GetChild.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GetChild : MonoBehaviour
{
    public GameObject[] Parents;
    GameObject Panel;
    List<GameObject> Paneles;

    void Start()
    {
        Paneles = new List<GameObject>();

        for(int v = 0; v < Parents.Length; v++)
        {
            for(int i = 0; i < Parents[v].transform.childCount; i++ )
            {
                Panel = Parents[v].transform.GetChild(i).gameObject;
                Paneles.Add(Panel);
            }
        }

        for(int i = 0; i < Paneles.Count; i++) //確認用の表記
        {
            Debug.Log(Paneles[i]);
        }
    }
}

たったこれだけです。

使い方は以下の通りです。

1、何らかのオブジェクトにアタッチする
2、子を取得したい親オブジェクトをParentsインスペクターにセット
 (Sizeを変更すればセットできる数も増えます)
コメント 2019-08-28 144732.png

さいごに

この方法を考え付いたのは、インターフェースの記事を見ていて、こういう感じにうまく
まとめられないかなぁ~という漠然とした思い付きで試してみた結果、
「なんかできたわ」という感じでした。

自分の知識量が以前に比べて増えているんだなぁという実感を得られたので良かったです。
この方法が誰かの役に立ちますように。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでUnity ML-Agentsを動作させる(v0.9.1対応)

Unity ML-Agents(v0.9.1)をDocker上で動作させてみました。

UnityやUnity ML-Agentsの環境構築などは下記をご参考ください。

Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033

MacでUnity ML-Agentsの環境を構築する(v0.9.1対応) - Qiita
https://qiita.com/kai_kou/items/268ccf6f961f8ca8cba8

手順

基本的には公式のドキュメントに沿えばよい感じです。

Using Docker For ML-Agents
https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Using-Docker.md

Dockerのインストール

Dockerがインストールされていない場合、インストールします。

> brew cask install docker
(略)

> docker --version
Docker version 19.03.1, build 74b1e89

※Dockerを初回起動すると初期設定のためにパスワード入力が求められます。

UnityにLinuxビルドサポートコンポーネントを追加する

Unity Hubを利用してUnityにLinuxビルドサポートコンポーネントを追加します。
Unityのバージョンは2019.1.13f1 を利用しています。

  • Unity Hubアプリを起動する
  • [Installs] > [On my machine]からUnityリスト右側にある[...]をクリックして[Add Component]を選択する Unity
  • [Add components your install]ダイアログの[Platforms]にある[Linux Build Support]にチェックを入れて[Done]ボタンをクリックする Unity

学習用のappをダウンロードしてビルドする

ML-Agentsリポジトリをダウンロード

適当なディレクトリにリポジトリをダウンロードする。

> mkdir 適当なディレクトリ
> cd 適当なディレクトリ
> git clone https://github.com/Unity-Technologies/ml-agents.git

Unityアプリからサンプルプロジェクトを開く

Unity Hubでアプリを立ち上げます。Unity Hubがインストールされていない場合は下記をご参考ください。

Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033

ML-Agentsを利用するにはUnityのバージョン2017.4 以上が必要となります。今回は2019.1.13f1 を利用しました。

アプリが立ち上がったら「開く」ボタンから任意のディレクトリ/ml-agents/UnitySDK フォルダを選択します。

Unity_Hub.png

Unityエディタのバージョンによっては、アップグレードするかの確認ダイアログが立ち上がります。
スクリーンショット 2019-08-09 16.37.46.png

「Upgrade」ボタンをクリックして進めます。

アップグレード処理に少し時間がかかります。

スクリーンショット 2018-09-13 10.50.01.png

起動しました。

今回は、サンプルとして含まれている[3DBall]Scenesを利用します。

  • Unityアプリの下パネルにある[Project]タブから以下のフォルダまで開く
    • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Scenes]
  • 開いたら、[3DBall]ファイルがあるので、ダブルクリックして開く

Scenes選択

Scenes(シーン)の設定

ML-Agentsで学習させるための設定です。

  • Unityアプリの[Edit]メニューから[Project Settings]を開く
    スクリーンショット 2019-08-19 14.23.05.png

  • [Inspector]パネルで以下の設定を確認する

    • [Resolution and Presentation]の[Run In Background]がチェックされている
    • [Display Resolution Dialog]がDisableになっている スクリーンショット_2019_08_19_14_25のコピー.png

Prefabs(プレハブ)の設定

  • Unityアプリの下にある[Project]タブから以下のフォルダまで開く
    • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Prefabs]
  • Prefabsフォルダ内の[Game]をダブルクリックする
  • Unityアプリの左側にある[Hierarchy]パネルから[Game] > [Pratform]を選択する Unity
  • Unityアプリの右側にある[Inspector]パネルのBall 3D Agent(Script)にあるBrainが[3DBallLearning(LearningBrain)]であることを確認する
  • [3DBallLearning(LearningBrain)]ではない場合、
    • [Project]タブから以下のフォルダまで開き、
      • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Brains]
    • [3DBallLearning]を[Inspector]パネルのBrainにドラッグ&ドロップして指定する Unity
  • [Ctrl] + [s]キーでシーンを保存する
    • ※設定変更後、しっかりと保存しないと、ビルド時に設定が反映されなくてハマります。

Brainsの設定

  • Unityアプリの下パネルにある[Project]タブから以下のフォルダまで開く
    • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Scenes]
  • Unityアプリの左側にある[Hierarchy]パネルから[Ball3DAcademy]を選択する
  • Unityアプリの右側にある[Inspector]パネルの[Broadcast Hub] > [Brains]に「3DBallLearning (LearningBrain)」が指定されていることを確認する
  • 「3DBallLearning (LearningBrain)」横にある[Control]にチェックを入れる
    Unity

  • [Ctrl] + [s]キーでシーンを保存する

※設定変更後、しっかりと保存しないとビルド時に設定が反映されなくてハマります。

ビルド設定

  • Unityアプリの[File]メニューから[Build Settings]を選択する
  • [Build Settings]ダイアログで[Add Opne Scenes]をクリックする
  • [Scenes In Build]で[ML-Agents/Examples/3DBall/Scenes/3DBall]にチェックを入れる
  • [Platform]でPC, Mac & Linux Standalone が選択されていることを確認する
  • [Target Platform]をLinux に変更する
  • [Architecture]をx86_64 に変更する
  • [Server Build]にチェックを入れる
    • 以前は[Headless Mode]でした。
  • [Build Settings]ダイアログで[Build]ボタンをクリックする
  • ファイル保存ダイアログで以下を指定してビルドを開始する
    • ファイル名: 3DBall
    • フォルダ名: 任意のディレクトリ/ml-agents/unity-volume

Build_Settings
Build_Settings

すると、unity-volume に以下フォルダ・ファイルが出力されます。

> ls 任意のディレクトリ/ml-agents/unity-volume

3DBall.x86_64 3DBall_Data

ハイパーパラメーターファイルの用意

ハイパーパラメーターファイルをunity-volume フォルダにコピーしておきます。

> cd 任意のディレクトリ/ml-agents
> cp config/trainer_config.yaml unity-volume

Dockerコンテナを構築する

Dockerが起動していることを確認してから、ml-agentsリポジトリ直下でdockerコンテナを構築します。すでにDockerfileが用意されているので、docker build するだけ。楽々ですね。

> cd 任意のディレクトリ/ml-agents
> docker build -t 3dball-ml-docker .

(略)
Step 20/20 : ENTRYPOINT ["mlagents-learn"]
 ---> Running in 56532e0d2127
Removing intermediate container 56532e0d2127
 ---> 073d8b1040e9
Successfully built 073d8b1040e9
Successfully tagged 3dball-ml-docker:latest

Dockerコンテナの実行

Dockerコンテナが構築できたら実行してみます。

bashの場合
# unity-ml-docker-3dball: コンテナ名(任意)
# 3DBall: Unityでbuild時に付けたアプリの名前(拡張子なし)
# 3dball-ml-docker: Dockerでbuild時に付けた名前
# docker-first-run: 機械学習結果を保存する際の名称(任意)

> docker run -it \
  --name unity-ml-docker-3dball \
  --mount type=bind,source="$(pwd)"/unity-volume,target=/unity-volume \
  -p 5005:5005 \
  -p 6006:6006 \
  3dball-ml-docker:latest \
    trainer_config.yaml \
    --docker-target-name=unity-volume \
    --env=3DBall \
    --train \
    --run-id=docker-first-run

fishシェルで実行する場合は、"$(pwd)""$PWD" に置き換えます。

fishの場合
> docker run -it \
  --name unity-ml-docker-3dball \
  --mount type=bind,source="$PWD"/unity-volume,target=/unity-volume \
  -p 5005:5005 \
  -p 6006:6006 \
  3dball-ml-docker:latest \
    trainer_config.yaml \
    --docker-target-name=unity-volume \
    --env=3DBall \
    --train \
    --run-id=docker-first-run

注意点

Unity ML-Agents公式にあるdocker run コマンドのサンプルがおそらく動作検証していなくて、パラメータ指定エラーとなります。(2019/08/20時点)

以下はmlagents-learn コマンドのヘルプです。
Dockerで実行する場合には、Dockerイメージの指定(3dball-ml-docker:latest )の後からが、mlagents-learn コマンドのパラメータ指定となるため、--docker-target-nametrainer_config.yaml (<trainer-config-path>) の後ろに指定します。

mlagents-learn help

    Usage:
      mlagents-learn <trainer-config-path> [options]
      mlagents-learn --help

    Options:
      --env=<file>                Name of the Unity executable [default: None].
      --curriculum=<directory>    Curriculum json directory for environment [default: None].
      --sampler=<file>            Reset parameter yaml file for environment [default: None].
      --keep-checkpoints=<n>      How many model checkpoints to keep [default: 5].
      --lesson=<n>                Start learning from this lesson [default: 0].
      --load                      Whether to load the model or randomly initialize [default: False].
      --run-id=<path>             The directory name for model and summary statistics [default: ppo].
      --num-runs=<n>              Number of concurrent training sessions [default: 1].
      --save-freq=<n>             Frequency at which to save model [default: 50000].
      --seed=<n>                  Random seed used for training [default: -1].
      --slow                      Whether to run the game at training speed [default: False].
      --train                     Whether to train model, or only run inference [default: False].
      --base-port=<n>             Base port for environment communication [default: 5005].
      --num-envs=<n>              Number of parallel environments to use for training [default: 1]
      --docker-target-name=<dt>   Docker volume to store training-specific files [default: None].
      --no-graphics               Whether to run the environment in no-graphics mode [default: False].
      --debug                     Whether to run ML-Agents in debug mode with detailed logging [default: False].

実行すると、学習が始まります。
trainer_config.yamlmax_steps で指定されているステップ数が完了するか、ctrl + c キーで学習が終了します。

> docker run ()

INFO:mlagents.trainers:{'--base-port': '5005',
 '--curriculum': 'None',
 '--debug': False,
 '--docker-target-name': 'unity-volume',
 '--env': '3DBall',
 '--help': False,
 '--keep-checkpoints': '5',
 '--lesson': '0',
 '--load': False,
 '--no-graphics': False,
 '--num-envs': '1',
 '--num-runs': '1',
 '--run-id': 'docker-first-run',
 '--sampler': 'None',
 '--save-freq': '50000',
 '--seed': '-1',
 '--slow': False,
 '--train': True,
 '<trainer-config-path>': 'trainer_config.yaml'}


                        ▄▄▄▓▓▓▓
                   ╓▓▓▓▓▓▓█▓▓▓▓▓
              ,▄▄▄m▀▀▀'  ,▓▓▓▀▓▓▄                           ▓▓▓  ▓▓▌
            ▄▓▓▓▀'      ▄▓▓▀  ▓▓▓      ▄▄     ▄▄ ,▄▄ ▄▄▄▄   ,▄▄ ▄▓▓▌▄ ▄▄▄    ,▄▄
          ▄▓▓▓▀        ▄▓▓▀   ▐▓▓▌     ▓▓▌   ▐▓▓ ▐▓▓▓▀▀▀▓▓▌ ▓▓▓ ▀▓▓▌▀ ^▓▓▌  ╒▓▓▌
        ▄▓▓▓▓▓▄▄▄▄▄▄▄▄▓▓▓      ▓▀      ▓▓▌   ▐▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▌   ▐▓▓▄ ▓▓▌
        ▀▓▓▓▓▀▀▀▀▀▀▀▀▀▀▓▓▄     ▓▓      ▓▓▌   ▐▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▌    ▐▓▓▐▓▓
          ^█▓▓▓        ▀▓▓▄   ▐▓▓▌     ▓▓▓▓▄▓▓▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▓▄    ▓▓▓▓`
            '▀▓▓▓▄      ^▓▓▓  ▓▓▓       └▀▀▀▀ ▀▀ ^▀▀    `▀▀ `▀▀   '▀▀    ▐▓▓▌
               ▀▀▀▀▓▄▄▄   ▓▓▓▓▓▓,                                      ▓▓▓▓▀
                   `▀█▓▓▓▓▓▓▓▓▓▌
                        ¬`▀▀▀█▓


INFO:mlagents.envs:
'Ball3DAcademy' started successfully!
Unity Academy name: Ball3DAcademy
        Number of Brains: 1
        Number of Training Brains : 1
        Reset Parameters :
                scale -> 1.0
                mass -> 1.0
                gravity -> 9.8100004196167
Unity brain name: 3DBallLearning
        Number of Visual Observations (per agent): 0
        Vector Observation space size (per agent): 8
        Number of stacked Vector Observation: 1
        Vector Action space type: continuous
        Vector Action space size (per agent): [2]
        Vector Action descriptions: ,
2019-08-19 09:26:41.207263: I tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
INFO:mlagents.envs:Hyperparameters for the PPOTrainer of brain 3DBallLearning:
        trainer:        ppo
        batch_size:     64
        beta:   0.001
        buffer_size:    12000
        epsilon:        0.2
        hidden_units:   128
        lambd:  0.99
        learning_rate:  0.0003
        max_steps:      5.0e4
        memory_size:    256
        normalize:      True
        num_epoch:      3
        num_layers:     2
        time_horizon:   1000
        sequence_length:        64
        summary_freq:   1000
        use_recurrent:  False
        vis_encode_type:        simple
        reward_signals:
          extrinsic:
            strength:   1.0
            gamma:      0.99
        summary_path:   /unity-volume/summaries/docker-first-run-0_3DBallLearning
        model_path:     /unity-volume/models/docker-first-run-0/3DBallLearning
        keep_checkpoints:       5
INFO:mlagents.trainers: docker-first-run-0: 3DBallLearning: Step: 1000. Time Elapsed: 33.147 s Mean Reward: 1.107. Std of Reward: 0.588. Training.
INFO:mlagents.trainers: docker-first-run-0: 3DBallLearning: Step: 2000. Time Elapsed: 61.621 s Mean Reward: 1.227. Std of Reward: 0.680. Training.
INFO:mlagents.trainers: docker-first-run-0: 3DBallLearning: Step: 3000. Time Elapsed: 94.060 s Mean Reward: 1.511. Std of Reward: 0.973. Training.
INFO:mlagents.trainers: docker-first-run-0: 3DBallLearning: Step: 4000. Time Elapsed: 143.072 s Mean Reward: 1.959. Std of Reward: 1.307. Training.
INFO:mlagents.trainers: docker-first-run-0: 3DBallLearning: Step: 5000. Time Elapsed: 201.150 s Mean Reward: 2.899. Std of Reward: 2.278. Training.
(略)
INFO:mlagents.trainers: docker-first-run-0: 3DBallLearning: Step: 50000. Time Elapsed: 2084.527 s Mean Reward: 100.000. Std of Reward: 0.000. Training.
INFO:mlagents.envs:Saved Model
INFO:mlagents.trainers:List of nodes to export for brain :3DBallLearning
INFO:mlagents.trainers: is_continuous_control
INFO:mlagents.trainers: version_number
INFO:mlagents.trainers: memory_size
INFO:mlagents.trainers: action_output_shape
INFO:mlagents.trainers: action
INFO:mlagents.trainers: action_probs
INFO:tensorflow:Restoring parameters from /unity-volume/models/docker-first-run-0/3DBallLearning/model-50001.cptk
INFO:tensorflow:Froze 14 variables.
INFO:mlagents.trainers:Exported /unity-volume/models/docker-first-run-0/3DBallLearning.nn file
Converted 14 variables to const ops.
Converting /unity-volume/models/docker-first-run-0/3DBallLearning/frozen_graph_def.pb to /unity-volume/models/docker-first-run-0/3DBallLearning.nn
IGNORED: Cast unknown layer
IGNORED: StopGradient unknown layer
GLOBALS: 'is_continuous_control', 'version_number', 'memory_size', 'action_output_shape'
IN: 'vector_observation': [-1, 1, 1, 8] => 'sub_3'
IN: 'epsilon': [-1, 1, 1, 2] => 'mul_1'
OUT: 'action', 'action_probs'
DONE: wrote /unity-volume/models/docker-first-run-0/3DBallLearning.nn file.

TensorBoard を利用して学習の進捗状況を視覚的に確認することもできます。

> docker exec \
  -it unity-ml-docker-3dball \
  tensorboard \
  --logdir=/unity-volume/summaries \
  --host=0.0.0.0

2019-08-19 09:43:12.596652: I tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
TensorBoard 1.7.0 at http://0.0.0.0:6006 (Press CTRL+C to quit)

スクリーンショット 2019-08-19 19.07.11.png

学習結果をアプリに組み込む

学習結果は、ml-agents/unity-volume フォルダ内に保存されます。

> ls unity-volume/

3DBall.x86_64       3DBall_Data         models              summaries           trainer_config.yaml

Unityアプリの設定

Playerの設定を行います。

  • Unityアプリの[Edit]メニューから[Project Settings]を選択する
  • [Inspector]ビューの[Other Settings]欄で以下を確認・設定する
    • [Scripting Runtime Version]を[.Net 4.x Equivalent]にする Unity

学習結果ファイルの取り込み

ターミナルかFinderで学習結果を以下フォルダにコピーします。

  • 学習結果ファイル: unity-volume/models/docker-first-run-0/3DBallLearnig.nn
  • 保存先: UnitySDK/Assets/ML-Agents/Examples/3DBall/TFModels/

※すでに保存先に3DBallLearnig.nn ファイルが存在していますので、リネームしてください。

> cp unity-volume/models/docker-first-run-0/3DBallLearnig.nn UnitySDK/Assets/ML-Agents/Examples/3DBall/TFModels/
  • Unityアプリの[Project]パネルで以下ファイルを選択する
    • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Brains] > [3DBallLearning]
  • Unityアプリの[Project]パネルで以下フォルダを選択する
    • [Assets] > [ML-Agents] > [Examples] > [3DBall] > [TFModels]
  • Unityアプリの[Inspector]パネルにある[Model]という項目に[TFModels]フォルダ内の3DBallLearning.nnファイルをドラッグ&ドロップする Unity
  • Unityアプリの[Hierarchy]パネルから以下を選択する
    • [3DBall] > [Ball3DAcademy]
  • Unityアプリの[Inspector]パネルにある[Broadcast Hub] > [Brains] > [3DBallLearning(LearningBrain)]横の[Control]のチェックを外す Unity
  • Unity上部にある[▶]ボタンをクリックする

これで、学習結果が組み込まれた状態でアプリが起動します。

参考

Using Docker For ML-Agents
https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Using-Docker.md

MacでUnity ML-Agentsの環境を構築する(v0.9.1対応) - Qiita
https://qiita.com/kai_kou/items/268ccf6f961f8ca8cba8

Docker for Macをインストールしてみた
https://qiita.com/scrummasudar/items/750aa52f4e0e747eed68

Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ドット絵のキャラクターに逆光を当ててみる

前回の記事 の続きです。

こういうことをやりました

  • 前回記事で用意したSpriteRendererに 裏から光を当てて影を落とす
  • ボリュームライト を適用

SpriteRendererに裏から光を当てて影を落とす

 普通にライトの角度変えればいいんじゃないの?と思ってましたが

こっちは影が出るのに

こっちは出ない。。。

 これはどういうことかというと、カメラをSpriteRendererの後ろに回るとわかるのですが、裏面に表示用の面が無いため です。その結果、光を遮って影を作ることができることができていません。

Standardシェーダーに細工をする

 前回、もろもろの表現をするために、SpriteRendererにStandardシェーダーを適用しました。このStandardシェーダーは ポリゴンの面が無い方向には何も表示しない(カリング) という処理しているのですが、裏面にポリゴンの面がないSpriteRendererにおいてはそれだと都合が悪いです。

1. Starndardシェーダーをダウンロード

 そこで、カリング処理をしないStandardシェーダー を用意しましょう。StandardシェーダーはUnity組み込みのシェーダーで、普通は編集することはできません。Unity ダウンロード アーカイブ へアクセスし、今使っているUnityのバージョンから、ビルトインシェーダーをダウンロードしましょう。

image.png

 ダウンロードしたzipを展開し、フォルダ内から Standard.shader を見つけましょう。それをプロジェクト内にインポートしてください。

2. シェーダー名を変える

 インポートしたままだと、Unity内組み込みのStandardシェーダーと名前がバッティングしてややこしいです。ファイル名と、シェーダー名定義「StandardCullOff」とでも変えておきましょう。

image.png

StandardCullOff.shader
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "StandardCullOff" // ← ここがシェーダー名定義!
{
    Properties
    {
...

3.カリング処理をOFFにする

Shaderファイル内の54行目くらいの以下の場所に Cull Offと記述しましょう。

StandardCullOff.shader
    SubShader
    {
        Tags { "RenderType"="Opaque" "PerformanceChecks"="False" }
        LOD 300

        Cull Off // ← これを追加!

        // ------------------------------------------------------------------

4.シェーダーをマテリアルに設定

image.png

これで背面からの光でも影が出るようになります!

image.png

ボリュームライトを適用

 初回の記事でも触れましたが、ボリュームライトとは、本来見えない光の形が、霧やホコリによって見えることです。(参照)空間に奥行きを感じてリアリティが出て、リッチな印象を与えます。

 ボリュームライトの実現にはいくつか手段がありますが、今回はGitHubで公開されているVolumetric Lightsというアセットを利用します。

1. Volumetric Lightsをダウンロード

 GitHubのプロジェクトページ にアクセスし、Download ZIPをクリック。

2. 必要なファイルをプロジェクトにインポート

 フォルダごとプロジェクトにインポートしてもいいですが、最低限必要なのは以下のファイルと思われます。

  • Resources
  • Scripts
  • Shaders
  • Textures

適宜プロジェクト内に取り込みましょう。

3.コンポーネントを設定

  • Cameraに Volumetric Light Renderer
  • Lightに Volumetric Light

をアタッチして、シーンを実行するだけでOKです!ただし、Volumetric Lightのほうは、DirectionalSpot のタイプのライトでしか有効にならないとのことなので、ご注意を。

 

 光のシルエットが見えやすいように、光の通り道を部分的に作ったり、影のシルエットを意識したりするとその真価が発揮できる・・・かもしれません。
 このあたりコツを掴むのがやや難しいように感じたので、コンポーネントのパラメータの内容や、ライト・オブジェクトの配置の仕方について調べてみようと思います。

所感

 後ろから光が当たったときは、リムライト的な考え方でSpriteRendererのフチが明るくなるといいのですが、あくまで平面にノーマルマップを持たせたにすぎないので、今のところまだできていません。いよいよシェーダーをちゃんと触らないといけないかも…!
 今後はそのあたりと、Depth of Fieldを組み合わせて「オクトパストラベラー」や「The Last Night」っぽいジオラマシーンを作ってみようかなと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(もうちょっと) 楽に Unity ネイティブプラグイン を実装したい

はじめに

この記事は Unityゆるふわサマーアドベントカレンダー2019 の 28 日目の記事です。

昨年末のアドカレでネイティブプラグインの実装記事を書きました。

この記事では Unity Editor 上でネイティブプラグインを動的にロード/アンロードできるようにすることことで、いちいち Unity Editor を再起動することなくネイティブプラグインのリビルド~入れ替えをできるようになったわけですが、ぶっちゃけそれでもまだ面倒くさいわけです。

結局のところ Unity で動作確認しているから面倒くさくなるので Unity に持ってくる前に手軽に動作確認を・・・もっといえば単体アプリのように IDE 上で即 "デバッグ実行~停止" を高速に行えればよいわけです。

C++ で単体テストアプリを作成 ( Unity HDR Display Output plugin ではそうしてます) すればよいのですが、 C++ だと C# とのつなぎ込み部分が確認できず、 C# 部分は Unity にもっていってからという事になります。できればそこもテストアプリで確認したい。

ということで C# (.NET Framework) でテストアプリを作ってテストできるようにしたいと思います。対象は Windows です。

実例プロジェクトは後日上げます (すいません) 。

環境

  • Windows 10
  • Visual Studio 2019 (16.2.1)
  • Unity 2019.1.11f1

プロジェクトを作る

Unity プロジェクトとテストアプリとはテスト対象の C# コードを共有するようにしておきたいので、配置には配慮したいところです。ソースコードのコピーはしたくないので考えられるやり方としては

  1. シンボリックリンクをはる
  2. テストアプリプロジェクトのフォルダー内に Unity プロジェクトを配置する

今回は 2. でやっています。逆にする (Unity プロジェクト内にテストアプリプロジェクト) と Assets の下にテストアプリの .csproj などが配置されてしまうのであまりよろしくないのではないかと思います。

.csproj の直接編集をしているので注意してください。

  1. Visual Studio で Windows Forms プロジェクトを作成する
  2. Unity プロジェクトを 1. で作成した .csproj の位置を保存先にして作成する
  3. Assets の下にネイティブプラグイン用の C# コード (テストアプリと Unity プロジェクトで共有するもの) を配置するフォルダーを作成する
  4. テストアプリのプロジェクトを VS 上で "プロジェクトのアンロード" を行う
    image.png
  5. "編集" を選択して .csproj を開く
    image.png
  6. <ItemGroup> タグに 3. で作成したフォルダーを <Folder> タグで追加する。(下記は例)
    <Folder Include="UnityNativePlugin\Assets\NativePluginTest\Scripts\" /> image.png
  7. "プロジェクトの再読み込み" をする

以上でテストアプリと Unity プロジェクトで必要な C# コードの共有化準備ができました。 "Assets" までは Unity 側でフォルダーを作成してしまうので、 .csproj 直接編集しないとうまくいかないと思います。これ以後はテストアプリの .csproj で 6. のフォルダーにコードを追加していけば Unity 側からも参照できるようになります。

image.png

ネイティブプラグインとつなぎ込みコードの実装

ネイティブプラグイン自体は通常の手順で実装します。

つなぎ込みに関しては DllImport などの DLL 読み込み定義と C# で使いやすくするためのラッパー実装など Unity に非依存なコードまでを実装します。

この辺りに関しては私の過去記事もご一読ください。

テストアプリを作成する

テストアプリのプロジェクトにテストコードを実装していきます。

Unity で扱うのであれば GPU を利用したプラグインである事が多いはずです (もしそうでなければ本記事はここまで) 。よってテストアプリでもグラフィックス API を扱っていきます。

.NET で DirectX を扱う場合は SharpDX というライブラリを利用します。

SharpDX は機能別に細かくパッケージに分かれています。テストアプリの作成には上記くらいのパッケージは必要になります (SharpDX.Desktop は必須ではないですが使った方が楽) 。

SharpDX はプロジェクトが終了していますが、十分安定していると思うので Direct3D 11 レベルまでだったら SharpDX でよいと思います。 Direct3D 12 の最新を使用するのであれば別のライブラリを検討してください。

UnityPluginLoad / UnityPluginUnload に対応する

Unity のネイティブプラグインはロード時に UnityPluginLoad 、アンロード時に UnityPluginUnload が呼ばれます。UnityPluginLoad で渡される IUnittyInterfaces を使う (GPU の Device を必要とする) ネイティブプラグインをテストアプリで確認するには UnityPluginLoad に対応しなくてはならないわけです。

そこで問題になってくるのが "ネイティブプラグインから呼ばれる IUnityInterfaces をどうやって実装するか" です。 ネイティブから呼ばれるので C++ で書けば簡単確実ですが管理するものが増えてしまって最終的には面倒になるので C# で実装してみました。

using System;
using System.Runtime.InteropServices;
using D3D11 = SharpDX.Direct3D11;

namespace NativePluginTest
{
    class UnityInterface : IDisposable
    {
        private delegate IntPtr FnGetInterface(Guid guid);
        private delegate IntPtr FnGetDevice();

        private FnGetInterface _fnGetInterface;
        private FnGetDevice _fnGetDevice;

        private GCHandle _handleUnityInterfaces;
        private GCHandle _handleUnityGraphicsD3D11;

        private IntPtr[] _unityInterfaces;
        private IntPtr[] _unityGraphicsD3D11;

        private D3D11.Device _device;

        public UnityInterface(D3D11.Device device)
        {
            _device = device.QueryInterface<D3D11.Device>();

            _fnGetInterface = UnityInterfaceGetInteface;
            _fnGetDevice = UnityGraphicsD3D11GetDevice;

            _unityInterfaces = new IntPtr[] { Marshal.GetFunctionPointerForDelegate(_fnGetInterface) };
            _unityGraphicsD3D11 = new IntPtr[] { Marshal.GetFunctionPointerForDelegate(_fnGetDevice) };

            _handleUnityInterfaces = GCHandle.Alloc(_unityInterfaces, GCHandleType.Pinned);
            _handleUnityGraphicsD3D11 = GCHandle.Alloc(_unityGraphicsD3D11, GCHandleType.Pinned);
        }

        public void Dispose()
        {
            _device?.Dispose();
            _device = null;

            _handleUnityInterfaces.Free();
            _handleUnityGraphicsD3D11.Free();
        }

        public IntPtr GetUnityInterfaces()
        {
            return _handleUnityInterfaces.AddrOfPinnedObject();
        }

        private IntPtr UnityInterfaceGetInteface(Guid guid)
        {
            return _handleUnityGraphicsD3D11.AddrOfPinnedObject();
        }

        private IntPtr UnityGraphicsD3D11GetDevice()
        {
            return (_device?.NativePointer).GetValueOrDefault();
        }
    }
}

要点としては

  • IUnityInterfaces の実態は構造体定義の関数ポインターテーブルなので IntPtr 配列に Marshal.GetFunctionPointerForDelegate で取得したポインターを設定する
  • IntPtr 配列は GCHandle.Alloc でピン止めしてポインターを動かないようにしておく

とりあえず Direct3D Device を渡せればよいのでその関連のメソッドだけ実装しました。

本来、 IUntyInterfaces::GetInterface に対応する実装では渡される UnityInterfaceGUID に応じて適切なインターフェースポインターを返さなくてはいけないですが、ここも決め打ち実装にしています。

ピン止めですが、 .NET の Object は参照が有効でも実体の場所は保証されていないので、ピン止めすることでその Object の実体位置が固定化されます (そもそもピン止めしないとポインターは取得できないですが) 。ピン止めはその仕組み上、長時間ピン止めするのは望ましくないですが、今回の場合はアプリ終了まで維持する必要があります。

Marshal.GetFunctionPointerForDelegate で取得したポインターは個人的には結構謎なのですが、参照元のデリゲート自身を GCHandle.Alloc することができないようなので別の仕組みでポインターの有効性を保証しているのでしょう (多分) 。

使う時は次のようにします (抜粋) 。

using System;
using DXGI = SharpDX.DXGI;
using D3D11 = SharpDX.Direct3D11;

[DllImport("NativePlugin")]
static extern void UnityPluginLoad(IntPtr unityInterfaces);

[DllImport("NativePlugin")]
static extern void UnityPluginUnload();

static void Main(string[] args)
{
    DXGI.SwapChain swapchain = null;
    D3D11.Device device = null;
    UnityInterface unityInterface = null;
    try
    {
        var desc = new DXGI.SwapChainDescription() { /* 略 */ };
        D3D11.Device.CreateWithSwapChain(D3D.DriverType.Hardware, D3D11.DeviceCreationFlags.None, desc, out device, out swapchain);

        unityInterface = new UnityInterface(device);
        UnityPluginLoad(unityInterface.GetUnityInterfaces());

        //  テストコードをここに書く

        UnityPluginUnload();
    }
    finally
    {
        device?.Dispose();
        swapchain?.Dispose();
        factory?.Dispose();
        unityInterface?.Dispose();
    }
}

SharpDX で Direct3D Device を生成し、それを元に UnityInterface クラスのインスタンスを生成します。 UnityInterface からは IUnityInterfaces の実装コードのポインターが取得できるのでこれを引数にネイティブプラグインの UnityPluginLoad を実行します。

これにより初期化からテストアプリで動作確認ができるようになり、 Unity に持っていく前に行えるテスト範囲が大分広くなりました。

おわりに

ネイティブプラグインの実装は必要でないケースの方が多いとは思いますが、いざやろうとするとどうしても面倒と感じてしまう場合が多いように思います。

前回記事でも書きましたが事前準備が多いのでそこがすでに面倒くさい感じもするかもですが、一度準備すれば後は早いと思いますのである程度以上の規模になるなら見合った手間になるのではないかと思います。テストアプリ部分はテンプレート化しておきたいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む