t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

Unityの機械学習演習「RollerBall」を少し改造して理解を深める。

前回なんとか機械学習のスタートラインに付くことができた。
thom.hateblo.jp

その際に演習内容どおりに作ったのがこちら。
f:id:t-hom:20210123235914g:plain

ただし演習をなぞって作っただけなので、これは動いて当たり前である。
これだとまだ私自身がスキルを身に着けてる感じがしないので、これを少し改造してみることにした。

今回は床を斜めにして滑りすくしてみる。ひとまず前回平坦な滑らない床で学習させたモデルを使って実験してみると、次のようになった。
f:id:t-hom:20210124221148g:plain

なんとか耐えられるケースもあるけど、基本的にはポロポロと簡単にこぼれてしまう。
条件が変わったのに相変わらずTargetとの位置関係と自分のスピードしか見ておらず、以前と同様の基準で加減速するので当然の結果である。

なので、床の傾きも考慮して学習するように改造してみた。

UnityのGameObjectの変更

まずやることは、UnityのHierarchyウインドウでTargetとRollerAgentをFloorにドロップして子オブジェクトにしてしまう。
f:id:t-hom:20210124221712p:plain

こうすると、TargetもRollerAgentもFloorを基準とした座標軸で動くことができる。Floorを傾けるとTargetもRollerAgentも一緒に傾く。

次にTargetにRigidbody(剛体)を付加。
f:id:t-hom:20210124223618p:plain
これをつけることで様々な物理法則の影響を受けるようになる。

あとPhysic Materialを作ってDynamic Frictionを0.05、Static Frictionを0.1に設定し、Floor・Target・RollerAgentにすべて設定した。
f:id:t-hom:20210124223545p:plain
これはどれくらい滑るかという設定値で、0.05とか0.1とかは適当に調整しながらPlayして決めた。

次にRollerAgentのBehavior ParametersスクリプトのVectorObservationにあるSpace Sizeを12に増やす。
このとき演習で使った3DBallモデルは外しておく。
f:id:t-hom:20210124222514p:plain

このSpace Sizeというのは観測する値の数を指していて、演習で8を指定するのは中身を次のように格納するためだ。

  1. Targetのx座標
  2. Targetのy座標
  3. Targetのz座標
  4. RollerAgentのx座標
  5. RollerAgentのy座標
  6. RollerAgentのz座標
  7. RollerAgentのx速度
  8. RollerAgentのz速度

実際に格納しているコードはこちら。
f:id:t-hom:20210124222930p:plain
命令自体は4つしかないけど、最初の2つの命令はlocalPositionという3つのデータ(x, y, z)が入った型なので3,3,1,1で計8個である。

今回はFloorのlocalRotation情報を格納する。
f:id:t-hom:20210124223234p:plain

このlocalRotationの型は曲者で、Unity画面上のInspectorで見るとx, y, zの3軸なのに、実際にはx, y, z, wの4つの値が入るらしい。
それで8個から4つ増えて12個のデータが必要なのでSpace Sizeが12になる。

コードの修正

RollerAgent.csのコード全体は次のとおり。
日本語で「~を追加」とコメントしてるところが私が追加した部分である。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;

public class RollerAgent : Agent
{
    Rigidbody rBody;
    void Start()
    {
        rBody = GetComponent<Rigidbody>();
    }

    public Transform Target;

    // フロアを参照させるためのTransform型変数を追加
    public Transform Floor;

    public override void OnEpisodeBegin()
    {
        // If the Agent fell, zero its momentum
        if (this.transform.localPosition.y < 0)
        {
            this.rBody.angularVelocity = Vector3.zero;
            this.rBody.velocity = Vector3.zero;
            this.transform.localPosition = new Vector3(0, 0.5f, 0);
        }

        // Move the target to a new spot
        Target.localPosition = new Vector3(Random.value * 8 - 4,
                                           0.5f,
                                           Random.value * 8 - 4);
        
        //最初にローテーションをリセットする処理を追加
        Floor.transform.localRotation = new Quaternion(0,0,0,0);

        //-10°~10°の範囲でx軸・z軸をそれぞれランダムでローテートさせる処理を追加
        Floor.transform.Rotate(Random.Range(-10f,10f), 0, Random.Range(-10f, 10f));

    }

    public override void CollectObservations(VectorSensor sensor)
    {
        // Target and Agent positions
        sensor.AddObservation(Target.localPosition);
        sensor.AddObservation(this.transform.localPosition);


        //フロアのローテーション観察を追加
        sensor.AddObservation(Floor.transform.localRotation);

        // Agent velocity
        sensor.AddObservation(rBody.velocity.x);
        sensor.AddObservation(rBody.velocity.z);
    }

    public float forceMultiplier = 10;
    public override void OnActionReceived(ActionBuffers actionBuffers)
    {
        // Actions, size = 2
        Vector3 controlSignal = Vector3.zero;
        controlSignal.x = actionBuffers.ContinuousActions[0];
        controlSignal.z = actionBuffers.ContinuousActions[1];
        rBody.AddForce(controlSignal * forceMultiplier);

        // Rewards
        float distanceToTarget = Vector3.Distance(this.transform.localPosition, Target.localPosition);

        // Reached target
        if (distanceToTarget < 1.42f)
        {
            SetReward(1.0f);
            EndEpisode();
        }

        // Fell off platform
        else if (this.transform.localPosition.y < 0)
        {
            EndEpisode();
        }
    }

    public override void Heuristic(in ActionBuffers actionsOut)
    {
        var continuousActionsOut = actionsOut.ContinuousActions;
        continuousActionsOut[0] = Input.GetAxis("Horizontal");
        continuousActionsOut[1] = Input.GetAxis("Vertical");
    }

}

Public変数「Floor」にGameObjectのFloorをドラッグして設定すると、学習の準備は完了。
f:id:t-hom:20210124224508p:plain

学習させる。

演習と同じ要領で学習を開始する。
学習に使用するconfigファイル(yaml)は特に修正不要だが、run-idは使いまわしできないかと思うのでRollerBall2とした。

以下は学習中の様子。
f:id:t-hom:20210124224726g:plain

前回平坦な床で学習させたとき、Mean Reward(平均報酬)はわずか20000ステップ(149秒)で0.991と達人級の学習能力を見せつけてくれた。
報酬は成功したら1、失敗したら0と設定しているため、平均0.991ということは概ね100回中99回は成功するという意味になる。

2021-01-23 19:57:06 INFO [stats.py:139] RollerBall. Step: 10000. Time Elapsed: 78.755 s. Mean Reward: 0.672. Std of Reward: 0.469. Training.
2021-01-23 19:58:16 INFO [stats.py:139] RollerBall. Step: 20000. Time Elapsed: 149.032 s. Mean Reward: 0.991. Std of Reward: 0.093. Training.

今回は以下のようなログになった。

2021-01-24 20:00:37 INFO [stats.py:139] RollerBall. Step: 10000. Time Elapsed: 79.503 s. Mean Reward: 0.371. Std of Reward: 0.483. Training.
2021-01-24 20:01:49 INFO [stats.py:139] RollerBall. Step: 20000. Time Elapsed: 150.908 s. Mean Reward: 0.661. Std of Reward: 0.473. Training.
2021-01-24 20:03:01 INFO [stats.py:139] RollerBall. Step: 30000. Time Elapsed: 222.803 s. Mean Reward: 0.791. Std of Reward: 0.406. Training.
2021-01-24 20:04:13 INFO [stats.py:139] RollerBall. Step: 40000. Time Elapsed: 295.321 s. Mean Reward: 0.767. Std of Reward: 0.423. Training.
2021-01-24 20:05:25 INFO [stats.py:139] RollerBall. Step: 50000. Time Elapsed: 367.443 s. Mean Reward: 0.777. Std of Reward: 0.416. Training.
2021-01-24 20:06:37 INFO [stats.py:139] RollerBall. Step: 60000. Time Elapsed: 439.412 s. Mean Reward: 0.764. Std of Reward: 0.425. Training.
2021-01-24 20:07:49 INFO [stats.py:139] RollerBall. Step: 70000. Time Elapsed: 510.808 s. Mean Reward: 0.733. Std of Reward: 0.443. Training.
2021-01-24 20:08:59 INFO [stats.py:139] RollerBall. Step: 80000. Time Elapsed: 581.359 s. Mean Reward: 0.774. Std of Reward: 0.418. Training.
2021-01-24 20:10:10 INFO [stats.py:139] RollerBall. Step: 90000. Time Elapsed: 652.560 s. Mean Reward: 0.779. Std of Reward: 0.415. Training.
2021-01-24 20:11:23 INFO [stats.py:139] RollerBall. Step: 100000. Time Elapsed: 725.470 s. Mean Reward: 0.751. Std of Reward: 0.433. Training.
2021-01-24 20:12:37 INFO [stats.py:139] RollerBall. Step: 110000. Time Elapsed: 799.360 s. Mean Reward: 0.794. Std of Reward: 0.404. Training.
2021-01-24 20:13:52 INFO [stats.py:139] RollerBall. Step: 120000. Time Elapsed: 874.147 s. Mean Reward: 0.791. Std of Reward: 0.406. Training.
2021-01-24 20:15:06 INFO [stats.py:139] RollerBall. Step: 130000. Time Elapsed: 948.227 s. Mean Reward: 0.797. Std of Reward: 0.402. Training.
2021-01-24 20:16:20 INFO [stats.py:139] RollerBall. Step: 140000. Time Elapsed: 1022.675 s. Mean Reward: 0.852. Std of Reward: 0.355. Training.
2021-01-24 20:17:33 INFO [stats.py:139] RollerBall. Step: 150000. Time Elapsed: 1095.735 s. Mean Reward: 0.885. Std of Reward: 0.319. Training.
2021-01-24 20:18:47 INFO [stats.py:139] RollerBall. Step: 160000. Time Elapsed: 1169.150 s. Mean Reward: 0.875. Std of Reward: 0.330. Training.
2021-01-24 20:20:01 INFO [stats.py:139] RollerBall. Step: 170000. Time Elapsed: 1243.355 s. Mean Reward: 0.901. Std of Reward: 0.298. Training.
2021-01-24 20:21:15 INFO [stats.py:139] RollerBall. Step: 180000. Time Elapsed: 1317.187 s. Mean Reward: 0.904. Std of Reward: 0.295. Training.
2021-01-24 20:22:28 INFO [stats.py:139] RollerBall. Step: 190000. Time Elapsed: 1390.495 s. Mean Reward: 0.908. Std of Reward: 0.289. Training.
2021-01-24 20:23:41 INFO [stats.py:139] RollerBall. Step: 200000. Time Elapsed: 1463.643 s. Mean Reward: 0.891. Std of Reward: 0.312. Training.
2021-01-24 20:24:55 INFO [stats.py:139] RollerBall. Step: 210000. Time Elapsed: 1536.943 s. Mean Reward: 0.907. Std of Reward: 0.290. Training.
2021-01-24 20:26:08 INFO [stats.py:139] RollerBall. Step: 220000. Time Elapsed: 1610.528 s. Mean Reward: 0.907. Std of Reward: 0.290. Training.
2021-01-24 20:27:21 INFO [stats.py:139] RollerBall. Step: 230000. Time Elapsed: 1683.587 s. Mean Reward: 0.898. Std of Reward: 0.303. Training.
2021-01-24 20:28:37 INFO [stats.py:139] RollerBall. Step: 240000. Time Elapsed: 1758.921 s. Mean Reward: 0.903. Std of Reward: 0.297. Training.
2021-01-24 20:29:50 INFO [stats.py:139] RollerBall. Step: 250000. Time Elapsed: 1832.258 s. Mean Reward: 0.893. Std of Reward: 0.309. Training.
2021-01-24 20:31:04 INFO [stats.py:139] RollerBall. Step: 260000. Time Elapsed: 1905.822 s. Mean Reward: 0.909. Std of Reward: 0.288. Training.
2021-01-24 20:32:17 INFO [stats.py:139] RollerBall. Step: 270000. Time Elapsed: 1979.798 s. Mean Reward: 0.895. Std of Reward: 0.306. Training.
2021-01-24 20:33:31 INFO [stats.py:139] RollerBall. Step: 280000. Time Elapsed: 2053.540 s. Mean Reward: 0.872. Std of Reward: 0.334. Training.
2021-01-24 20:34:45 INFO [stats.py:139] RollerBall. Step: 290000. Time Elapsed: 2127.722 s. Mean Reward: 0.880. Std of Reward: 0.325. Training.
2021-01-24 20:35:59 INFO [stats.py:139] RollerBall. Step: 300000. Time Elapsed: 2201.468 s. Mean Reward: 0.861. Std of Reward: 0.346. Training.
2021-01-24 20:37:13 INFO [stats.py:139] RollerBall. Step: 310000. Time Elapsed: 2275.476 s. Mean Reward: 0.868. Std of Reward: 0.339. Training.
2021-01-24 20:38:28 INFO [stats.py:139] RollerBall. Step: 320000. Time Elapsed: 2350.178 s. Mean Reward: 0.864. Std of Reward: 0.343. Training.
2021-01-24 20:39:42 INFO [stats.py:139] RollerBall. Step: 330000. Time Elapsed: 2423.811 s. Mean Reward: 0.854. Std of Reward: 0.353. Training.
2021-01-24 20:40:55 INFO [stats.py:139] RollerBall. Step: 340000. Time Elapsed: 2497.704 s. Mean Reward: 0.828. Std of Reward: 0.378. Training.
2021-01-24 20:42:13 INFO [stats.py:139] RollerBall. Step: 350000. Time Elapsed: 2575.427 s. Mean Reward: 0.844. Std of Reward: 0.363. Training.
2021-01-24 20:43:28 INFO [stats.py:139] RollerBall. Step: 360000. Time Elapsed: 2650.043 s. Mean Reward: 0.811. Std of Reward: 0.392. Training.
2021-01-24 20:44:42 INFO [stats.py:139] RollerBall. Step: 370000. Time Elapsed: 2724.516 s. Mean Reward: 0.799. Std of Reward: 0.401. Training.
2021-01-24 20:45:59 INFO [stats.py:139] RollerBall. Step: 380000. Time Elapsed: 2801.075 s. Mean Reward: 0.804. Std of Reward: 0.397. Training.
2021-01-24 20:47:17 INFO [stats.py:139] RollerBall. Step: 390000. Time Elapsed: 2879.487 s. Mean Reward: 0.851. Std of Reward: 0.356. Training.
2021-01-24 20:48:34 INFO [stats.py:139] RollerBall. Step: 400000. Time Elapsed: 2956.177 s. Mean Reward: 0.851. Std of Reward: 0.357. Training.
2021-01-24 20:49:49 INFO [stats.py:139] RollerBall. Step: 410000. Time Elapsed: 3031.613 s. Mean Reward: 0.861. Std of Reward: 0.346. Training.
2021-01-24 20:51:04 INFO [stats.py:139] RollerBall. Step: 420000. Time Elapsed: 3106.688 s. Mean Reward: 0.872. Std of Reward: 0.334. Training.
2021-01-24 20:52:18 INFO [stats.py:139] RollerBall. Step: 430000. Time Elapsed: 3180.005 s. Mean Reward: 0.870. Std of Reward: 0.336. Training.
2021-01-24 20:53:37 INFO [stats.py:139] RollerBall. Step: 440000. Time Elapsed: 3258.956 s. Mean Reward: 0.854. Std of Reward: 0.354. Training.
2021-01-24 20:54:53 INFO [stats.py:139] RollerBall. Step: 450000. Time Elapsed: 3335.078 s. Mean Reward: 0.886. Std of Reward: 0.318. Training.
2021-01-24 20:56:08 INFO [stats.py:139] RollerBall. Step: 460000. Time Elapsed: 3410.529 s. Mean Reward: 0.905. Std of Reward: 0.294. Training.
2021-01-24 20:57:22 INFO [stats.py:139] RollerBall. Step: 470000. Time Elapsed: 3484.081 s. Mean Reward: 0.888. Std of Reward: 0.316. Training.
2021-01-24 20:58:36 INFO [stats.py:139] RollerBall. Step: 480000. Time Elapsed: 3558.386 s. Mean Reward: 0.879. Std of Reward: 0.326. Training.
2021-01-24 20:59:49 INFO [stats.py:139] RollerBall. Step: 490000. Time Elapsed: 3630.810 s. Mean Reward: 0.880. Std of Reward: 0.325. Training.
2021-01-24 21:01:03 INFO [stats.py:139] RollerBall. Step: 500000. Time Elapsed: 3705.281 s. Mean Reward: 0.887. Std of Reward: 0.317. Training.

既定の学習回数50万回を経ても、よくて9割といったところ。要は10回に1回くらいは落ちる。
今回は箱自体が滑り落ちるので、場所が悪いと落ちる箱を追い続けて玉も必然的に落ちるというパターンも多くあった。

推論の実行結果

学習したモデルをAgentのBehavior ParametersのModelに指定し、実際に推論によって操作させてみたのがこちら。
f:id:t-hom:20210124225631g:plain
落ちるときは落ちるけどそれなりにうまく立ち回っている。

「まぁ所詮こんなもんかな」という感想だったんだけど、試しに私が操作してみた。

↓私の操作
f:id:t-hom:20210124230240g:plain

すみませんでしたぁぁぁ。

以上。

Unityで機械学習になんとか入門できた話

先日購入した書籍でUnityを使って機械学習に入門しようと思ったのだが、環境構築で派手に躓いてしまった。最終的にライブラリの公式サイトに従ってなんとかスタートラインに立つことができたのでそのあたりの苦労を記しておこうと思う。

購入した書籍

こちらの書籍で今回躓いたのだが、けっして内容や構成が悪い本ではない。

ただ機会学習の分野はまだまだ伸びているとろこなので新陳代謝が激しく、ごく短い期間でツールやライブラリのバージョンがどんどん上がってしまう。

今から約5か月前(2020年の8月)に出版されたこの書籍で使用されているのはml-agentsというUnity用のライブラリのRelease 3だが、現在の安定版リリースはRelase 12である。あまりにも早い。

中で使われている機械学習ライブラリもTensorFlow(Google製)からPyTorch(Facebook製)に変わってるようで、このことからもml-agentsライブラリにも大きな変更があったものと思われる。

上手く行かなかった環境構築の方法

まずは最新の環境でとりあえず試してみた。Unity2019とml-agentsのRelease 12とPython3.8である。
結果は、書籍にない設定項目がでてきたり書籍のコードが古いためエラーになったりと、書籍に沿った学習が進められなかった。

次に、ml-agentsをRelease 3に変更してみた。画面は書籍通りになったが、機械学習用のツールであるmlagents-learnが動かない。
Unityを2018にしてみたが効果なし。まさかPythonは関係ないだろうと決めつけていたのでここでも数時間溶かした。あとから調べて分かったのだが、どうやらml-agentsのRelease 3で使用されているTensorFlowのバージョンがPython3.7以下でないと動かないらしい。

それで、Pythonも書籍に合わせて3.6を導入。とりあえずmlagents-learnは動くようになった。
しかしUnity側でPlayボタンを押しても何も始まらない。しばらく待っているとmlagents-learnがタイムアウトして終了してしまう。

Unity側で次のようなエラーが出ていたので、一応ファイアウォールも全部開けたり、検索して出てきた英語サイトを片っ端から読んでいったけど解決しなかった。

Couldn't connect to trainer on port 5004 using API version 1.0.0. Will perform inference instead.

構成は以下の通り。書籍の環境になるべく近づけたのだがダメだったのでここで一旦断念。

  • Windows 10 Home バージョン2004(OSビルド 19041.746)
  • Unity: 2018.4.30f1
  • Anaconda: 4.9.2
  • Python: 3.7.6
  • TensorFlow: 2.4
  • ML-Agents:Release 3

上手くいった環境構築の方法

書籍を参考にセットアップするのは諦めて、根気よく公式のGitHubからInstallation Guideを読むことにした。
セットアップをやり直すにあたり、まずはML-AgentsのRelease 3を削除し、Anacondaをアンインストール(Pythonも消えた)。
Unityはそのまま。

以下のサイトを読みながらセットアップを進めていく。
一応Release 12のインストールガイドにリンクしたが、この記事にたどり着いたタイミングによっては既により新しいバージョンが出ている可能性があるのでその場合はGoogle検索等で探していただけると良いかと思う。
github.com

ステップごとにごく簡単に解説してみる。

Install Unity 2018.4 or Later

Unityをインストールする。
元のサイトが2018版で解説されていたので素直にUnity2018を入れたけれど、2019でも同じように進められるかもしれない。
ここは特に私はハマらなかったので解説することはない。

Install Python 3.6.1 or Higher

書籍だとAnacondaをインストールする手順になっていて確かに便利だったけどまた変なところで躓いても困るので公式にしたがって素でインストールすることにした。
他にPythonで運用しているものがある方はpyenvとかAnacondaとか使うと良いかと思う。
3.6にしようかと思ったけれど公式サイトでは既にソースコードしか配布されていないようなのでインストーラーが配布されている3.7.9にした。

Clone the ML-Agents Toolkit Repository (Optional)

Gitでクローンしろという指示。
Optionalと書いてあるが、ml-agentsに付いてくるサンプルを動かすのに必要で、初心者はまずサンプルを動かさないことには右も左も分からないので実質必須。
これはまずGitをダウンロードする必要がある。Gitのバージョンは特にこの後の作業と関係ないので普通に最新版で良い。
インストール時にやたらと細かい設定を聞かれるのだが、全部デフォルトで良い。大丈夫。私もあんまり分かってない。
そしてコマンドプロンプトでml-agentsフォルダーを作りたい場所に移動し、以下のコマンドでml-agentsフォルダーがダウンロードされる。

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

GitHubから直接ダウンロードじゃだめなのか?→GitでCloneしないと上手く行かないという英語の意見を1件だけ検索で見つけてしまったので念のためインストールガイドに案内の通りGitでクローンした。

Install the com.unity.ml-agents Unity package

これは書籍のとおりでうまくいくので割愛。

(Windows) Installing PyTorch

Windowsの場合はPyTorchを別途インストールする必要があるのでインストレーションガイドに従ってpip3コマンドでインストールする。

pip3 install torch==1.7.0 -f https://download.pytorch.org/whl/torch_stable.html

Installing mlagents

mlagentsは次のコマンドでインストールする。

pip3 install mlagents

Advanced: Local Installation for Development

Advanced(高度な~)なので特にやってない。

次にやること

Getting Started Guideに従って、サンプルを使った演習を進めることになる。
ml-agents/Getting-Started.md at release_12_docs · Unity-Technologies/ml-agents · GitHub

内容的には以下の記事でも同じことをやってるので英語が辛い場合はこちらをおススメ。
qiita.com

その次にやること

Making a New Learning Environmentという記事に進むと、ようやく書籍で躓いたボール転がしの演習が出てくる。
ml-agents/Learning-Environment-Create-New.md at release_12_docs · Unity-Technologies/ml-agents · GitHub

書籍のコードだとRelease 12では動作しないのと設定項目が若干変わっているので書籍と上記のサイトを見比べながら、基本的にはサイトのやり方に合わせて進めると良いと思う。

完成したもの

以下はボールが四角いターゲットを目指すだけのもので、機械学習中の様子。学習が終わるまでだいたい40分くらいかかるのでバックグラウンドにしてYouTubeを視聴しながら待機する。
f:id:t-hom:20210123235630g:plain

学習が終わるとモデルが出来上がるので、ボールにそれを設定してPlayボタンを押す。するとフロアーから脱落することなく綺麗にターゲットを追いかけるようになる。
f:id:t-hom:20210123235914g:plain

これの何が凄いのかというと、ターゲットを目指して進むという直接的な処理をどこにもコーディングしていない点である。
エージェントはターゲットの位置・自分の位置・自分のベクトル(速度と方向)を観察することができ、その結果を自分が持つポリシーに照らし合わせて動くことができる。
この部分は確かにコーディングした。


しかし、ターゲットの方向に近づこうというポリシー自体は人がプログラムしたのではない。
エージェントがターゲットと接触したら報酬が与えられる(これはコーディングした部分)、エージェントはいろんなパターンを試す中で、観察された条件ごとに報酬がより多くもらえる行動を強化するようポリシーを変更する(これは機械学習ライブラリの担当する領域)。それの積み重ねによって徐々に床から落ちることなくターゲットを追い続ける仕組みが出来上がるのだ。機械学習のなかでも、強化学習と呼ばれる手法である。


とりあえず無事に機械学習のスタートラインに立てて良かった。
購入した本も、コードはそのままじゃ動かないけど解説は役に立ちそうなのでしばらく色々やってみようと思う。

以上。

Unityに入門してみたので、やりたいことと使用した参考書籍を紹介

先週からUnityに入門してみた。

Unityはゲームエンジンと呼ばれるツールで、ゲーム作成を強力にサポートしてくれる開発環境である。非商用の場合は無償で利用できるのでゲーム作成に興味があればとりあえず気軽に手を出してみることができる。サポートされている開発言語はC#で、こちらもVisual Studio Communityで開発できるので費用は掛からない。

Unityで作られている有名なゲームにはスーパーマリオランとか、ドラゴンクエストVIIIがある。

ゲームに欠かせない物理演算や当たり判定はゲームエンジンが用意してくれるのでプログラマーはゲームコンテンツの作りこみに専念できる。
最近はコンシューマーゲームのほとんどが何等かのゲームエンジンを用いて作られているようだ。

といっても、私の場合はゲームを作りたくてUnityを始めた訳ではない。まずはその話から。

Unityを触り始めたきっかけとやりたいこと

きっかけは「こーじ」さんという方のYouTube動画。
こーじさんはUnityで物理シミュレーションとか機械学習とか、物の仕組み(あれどういう仕掛けで動いてるの?)的なことをよく解説されているYouTuberだ。
www.youtube.com

以前からこのブログでも何かの概念を説明するときにモデル図を用いてきたけれど、アニメーションがあると良いなという思いはあった。
たとえば過去に書いたこういう記事とか、図にしてもなかなか難しいものがあるので動画にできないだろうかと。
thom.hateblo.jp

それで、動画で説明するのにどういう手段があるか色々と模索していた時期がある。候補に挙がったのは以下の5つの手段。

  • Power Pointアニメーション
  • 液晶ペンタブで書きながら録画
  • Adobe Animate CC
  • Blenderで3Dアニメ
  • Unityで3Dアニメ

まぁ今からVBAの解説を作るかと言われると今のところそんな気力はないんだけど、概念説明とかに使えるスキルとして何かしらアニメーションのスキルを習得しておきたい。
そんなときにこーじさんの、「かぎの内部構造」の説明動画を発見。
私が一番やりたいこと(仕組みの説明)に近かったのでUnityを選択することにした。
www.youtube.com

Unityの参考書籍

最初に買ったのはこちら。しかしこの本は現段階(初版 第2刷)ではChapter4での記述の誤りが多すぎて、おススメできない。

作って学べる Unity 超入門

作って学べる Unity 超入門

  • 作者:鈴木 道生
  • 発売日: 2019/04/16
  • メディア: 単行本(ソフトカバー)
主に物理演算機能を活用した題材が多くて、私がやりたいことに近い本だっただけにとても残念だが、正誤表を確認しながらやってみようというレベルじゃなかったので諦めて別の書籍を買うことしにした。

次に買ったのはこちら。やりたいことに直行するのは諦めてまずは2DでUnityそのものをしっかり学ぶ作戦。この本は結果的に大正解だったと思う。

【★特典付き】楽しく学ぶ Unity 2D超入門講座(特典:姉妹本の特別試し読みPDF)

【★特典付き】楽しく学ぶ Unity 2D超入門講座(特典:姉妹本の特別試し読みPDF)

  • 作者:森 巧尚
  • 発売日: 2019/02/21
  • メディア: 単行本(ソフトカバー)
特に躓くことなく、土日の2日間で最終チャプターまでコンプリートできた。
チュートリアル本なので書かれたとおりに操作していけば誰でも2日くらいで完了できると思う。ただその操作の意味をどこまで理解できるかは過去のプログラミング経験にもよると思うので、前提知識としてCのような記述スタイルの言語をひとつでも書いたことがあると望ましい。C#の入門書を1冊完了させたくらいの経験があるとベストだけど、C・C++・Java・JavaScriptあたりでもなんとかなると思う。オブジェクト指向は多用されてるので基本的なところは抑えておく必要がある。

そして本日読了したのがこちら。こちらもAmazonでの評価どおりの良書で、最後までサクサクと進められた。

2D版で既に済ませている内容の解説も多いので、差分学習したい場合は若干うんざりする場面も多いけど、2D版を済ませたからって1発で記憶するってこともないだろうから似たような内容でも復習だと思ってやってみた。もうそれ2Dでやったよって思って実際にその通りのこともあるけど、実は3D版だと勝手が違ったりする場面も結構あったので思い込みで読み飛ばしてたら重要な項目をスキップしていたかもしれない。真面目にやって良かった。

あとは3Dならではの不可解な事象にも悩まされた。たとえばキャラのX軸・Z軸の回転をロックしてるのにY軸回転を続けると0.0001とかの単位でX軸・Z軸が微妙に傾いてきて、ジャンプも組み合わせると最終的に傾きが大きくなってコケるとか。サポートサイトからダウンロードしたコードでもコケてたので気にしないことにしたけど。

この本は月~金の平日1時間ずつと、土日3時間ずつくらいで読了したのでトータルで13時間くらいかなと思う。

さて、もともとUnityでゲームを作るつもりはなかったんだけど、やってみるとそれはそれで楽しい。最近ゲーミフィケーションという概念を知ったのだが、ゲームを活用したエデュケーション(教育)のことを指すらしい。Minecraft Education Editionなんかがまさにゲーミフィケーションである。純粋な娯楽としてのゲームではなくて、そういう何か有意義なものを作ってみたいような気はする。

今後読む本(購入済)

まずはこちら。Blenderというオープンソースの3Dモデリングソフトの解説書である。

入門Blender2.9 ~ゼロから始める3D制作~

入門Blender2.9 ~ゼロから始める3D制作~

Unityで3Dゲームは作れるけど3Dモデルは作れない(ProBuilderという機能でシンプルなものはモデリングはできるらしいので訂正)。キャラを自分で作りたければ別途モデリングソフトが必要になる。
モデリングソフトもピンキリだけど無償で使用できて本格的な機能をそろえているものはBlenderくらいしかないので道楽でやるにはこれ一択。
以前は情報量の少なさがハードルになっていたようだけど現在はだいぶ解説書が揃ってきているように思える。
良書かどうかはこれから読むので分からないけどAmazon評価は良かったので期待している。

次にこちら。

Unityでわかる! ゲーム数学

Unityでわかる! ゲーム数学

  • 作者:加藤 潔
  • 発売日: 2018/06/18
  • メディア: 単行本(ソフトカバー)
これは材料力学の学習で微積分を使うので、どうせなら物理シミュレーションで弾道計算とかやりながら学習したいと思っていて、ちょうどよさげなので購入した。
Unityを使っているものの中味は数学書で、数式が沢山出てくるので正直読了できるかどうかはまだ自信がない。

最後にこちら。

これはUnityでAIを使用するための書籍。

UnityとAIで何ができるかというと、たとえばコンピューターにゲームを自動的に操作させたり、
a1026302.hatenablog.com

対戦ゲームの戦術をシミュレーションしたりといったことができる。
www.youtube.com


機械学習というとこれまで理論書や実験的なものが多くて、入門しようにも数学の知識がないと太刀打ちできなかったり、書籍のサンプルを発展的に使用するということが難しかった。(私が遅れているだけかもしれないけど。)

最近ようやく一般的なプログラマーが「使う」というフェーズに入ってきたように思う。
先日書店でScratchでAIを活用するというテーマの書籍を見つけてしまい、強烈な危機感を覚えた。

Scratchといえば、主に子供をターゲットにしたビジュアルプログラミング言語である。
つまり機械学習はもう学者や研究者だけのものではなく、既に子供でも活用できるレベルにまで浸透してきているということだ。

ここまで来たら、もう手を出さないと次の時代に乗り遅れる感じがする。
ということでUnityから初めてみようかと思う。


AIといえば、実はこれとは別に以前にJetsonNanoも購入していてそちらはまだインストールしてサンプル1つ動かしただけで眠っている。
JetsonNanoはAIプログラムを実行するためにグラフィックまわりの演算スペックに特化した安価なシングルボードコンピューターである。ラズパイでもAIプログラムを実行することはできるらしいけど、JetsonNanoはAIに特化しているだけあって参考書籍やWeb記事もAIの活用例が沢山みつかるのが強み。

NVIDIA Jetson Nano 開発者キット B01

NVIDIA Jetson Nano 開発者キット B01

  • メディア: Personal Computers


Jetsonで作りたいものは、部屋をカメラで撮影して物体認識させ、どれくらい散らかってきたかを数値化し、基準を超えたら警告を出すシステム。
できるかどうかは検討付いていないけど、よく見かける物体が何であるかを認識するプログラムよりシンプルになるのではないかと思っている。私がやりたいことを実現するには、モノが何であるかを識別する必要はなく、モノの数だけ数えれば良いんじゃないかと思っている。

さて、脱線してしまったけど今とにかく興味が色々な方向に拡張している。
このまま霧散させてしまわないようにちゃんと着地点を見つけてひとつのスキル体系として収束させていくことが今後の課題である。

材料力学:モールの応力円をVBAで検証する。

前回の記事で書いたように、円柱状の棒を垂直に切った断面積Aと、角度θで斜めに切った断面積Bの関係が B = A / cosθとなることが理解できた。
thom.hateblo.jp

今回はここから、モールの応力円を理解するためにVBAを書いてみた。
モールの応力円とは、ドイツの応用力学者モールによって発見されたもので、仮想切断する角度θを変えながら垂直応力とせん断応力の関係をグラフにプロットすると、プロットされた点を結んだときに円になるというものである。

その前に前提の話をいくつか書いておく。

前提

以下のように、外力と内力は等しい力で釣り合っている。
f:id:t-hom:20210109115149p:plain

この力を断面に垂直な方向Pnと断面に水平な方向Psに分解する。
f:id:t-hom:20210109115802p:plain

原点とPnとPを結ぶ線は直角三角形になるので、角度θから求められる辺の比によって力の強さが求まる。
Pn=Pcosθ

同様に、
Ps = Psinθ

応力は力÷面積なので、垂直応力σ、せん断応力τはそれぞれ次の式で求まる。
※A0は断面積を表す。

 \displaystyle \sigma = \frac{P_n}{A_0 \div cos\theta}

 \displaystyle \tau = \frac{P_s}{A_0 \div cos\theta}

モールの応力円をVBAコードで検証

本当に円になるのかVBAコードを書いて試してみた。VBAはマルチバイト文字の変数に対応しているのでこういう時に便利である。
πもθもσもτも全角文字としてそのまま書けるので数式を見ながらコードに変換していくのが楽だ。

Option Explicit
Sub MohrsStressCircle()
    Const A0 = 150
    Const P = 300
    Dim θ As Double
    
    Dim r As Range: Set r = ThisWorkbook.Sheets(1).Range("A1")
    r.Value = "σ"
    r.Offset(0, 1).Value = "τ"
    
    For θ = -90 To 90 Step 1
        Set r = r.Offset(1, 0)
        
        Dim Pn: Pn = P * Cos(Radian(θ))
        Dim Ps: Ps = P * Sin(Radian(θ))
        Dim σ: σ = Pn / (A0 / Cos(Radian(θ)))
        Dim τ: τ = Ps / (A0 / Cos(Radian(θ)))
        
        r.Value = σ
        r.Offset(0, 1).Value = τ
    Next
End Sub

Function Radian(degree As Double) As Double
    Const π = 3.14159265359
    Radian = degree * (π / 180)
End Function

これを実行するとExcelシートに値がずらっと出力されるので、散布図を挿入する。

楕円が現れるけど、これは縦横のマス目が揃っていないためなので、単位とサイズをそろえると綺麗な円になる。
f:id:t-hom:20210109122316p:plain

ということで、今回はプログラミングの力を借りて本当に円になるということが実証できた。
ちゃんとした書籍に円になるって書いてあるんだから、なるに決まってるんだけどそれでも自分でわざわざ確認することで強烈な納得感を得ることができる。
これが記憶の定着に繋がるような気がする。

材料力学の問題を解くときに気づいたことをダラダラと

今読んでいる「マンガで分かる材料力学」で昨晩から格闘していた箇所がようやく理解できた。今回、私が記事に残しておきたかったのは問題の答えではなく、「分からない」から「分かった」へ移行する際の思考の道筋である。

端的に問題と解説だけ掲載しても他の問題に応用できることって限られているので、それほど価値があることとは思えない。それよりも、何かまた自分が壁にぶち当たった際に考え方のヒントになるような記事にしたいと思った。


分からなかったのは以下の赤枠で囲った一文。
f:id:t-hom:20210108230900p:plain

ちなみに設問ですらなく、さも当然かのように書かれている。
しかしなぜそうなるのか分からない。いくら考えても分からない。

ここで一度立ち止まって原因を考えてみた。
なんで分からないのか?何を知らないから分からないのか?自問してみると答えは見つかった。

「そもそも楕円の面積の求め方しらねぇ。。」

そしてググると、次のような形で求められるらしい。
f:id:t-hom:20210108232136p:plain

なるほど、楕円も正円を包含するので公式がそのまま当てはまる。

次にFusion 360でモデリングして考えてみた。
f:id:t-hom:20210108231254p:plain

これはなかなか良いヒントを与えてくれたが、もっともらしく式を書いてみたものの合ってる確証が持てない。
まだまだ腑に落ちるというところまでは遠い。

次に平面で考えてみたが全然うまくいかない。
f:id:t-hom:20210108233041p:plain

描いてる途中でモヤっとしてくる。
この「モヤっ」となる原因を冷静に考えてみたところ、角度・長さを示す記号や文字がどこを指しているいるのか途中で混乱していることに気づいた。正確にいうともっと印をつけたいけどこれ以上書くと判別できなくなるため無意識にブレーキをかけていた。
つまり、図が小さすぎて十分なスペースが確保できていないのだ。
更に、記号の使い分けも洗練されていないことに気づいた。例えば角度を表す記号をシータしか知らないため表現力に乏しい。

さらにググって「ファイ(φ)」という記号があることを知り、使ってみることにした。
極端に大きく書いてみたところ、かなり理解が進んだ。
f:id:t-hom:20210108233615p:plain

一見些細なようでいて、実は結構思考の妨げになっているということがある。
たとえばプログラミングでも、初心者はインデントが分からなかったり変数の名前付けが適当だったりする。それでもロジックが正しければプログラムは動くので些細な事だと思いがちなんだけど、難しい問題にぶち当たったときに実はインデントの乱れや適当な変数名が思考にモヤをかけていたなんてことは良くある。
クリアな思考を保つためにはやはり情報の表現力を磨いていく必要があるなと思った。

さて、次がn度目の正直。ようやくスッキリ理解できたときのメモである。
f:id:t-hom:20210108235032p:plain


折角なので解説してみる。
まず正円Aの面積を半径a×半径b×πと表現する。正円なのでaとbの長さは同じだが、楕円に応用できるようにあえて記号を分けている。

次に楕円Bの面積を半径a×半径b'×πと表現する。

正円Aは棒を垂直に切った断面、楕円Bは同じ棒を角度をつけて切った断面である。

棒の断面を真横から見た図に、角度θ、正円の半径b、楕円の半径b'を記入する。
f:id:t-hom:20210109003000p:plain

次に下図の青いθ赤いθが同じ角度であることを確認する。
f:id:t-hom:20210109003148p:plain

青θ+直角+φが180°であることが見て取れるが、三角形の内角の和も180°なのでφと直角を引くと当然θになる。よって青θと赤θは同一角である。

この三角形を取り出す。φは赤青θが同一であることを求めるための中間材料なのでこの後はもう使わない。
f:id:t-hom:20210109003815p:plain

すると、cosθはbとb'の辺の比を表していることがわかる。
つまりcosθはb/b' である。

今分からないと言っている件は、面積B = 面積A / cosθであるということなので、この式に実際の辺の記号を割り当てて成り立つかどうかを確認していく。

 \displaystyle 面積A = abπ
 \displaystyle 面積B = ab'π
 \displaystyle 面積B = \frac{面積A}{cosθ}

 \displaystyle ab'π = \frac{abπ}{cosθ}

 \displaystyle ab'π = abπ \div \frac{b}{b'}

 \displaystyle b' = b \div \frac{b}{b'}

 \displaystyle b' = \frac{b}{1} \times \frac{b'}{b}

 \displaystyle b' = b'

ということで成り立ってる。

以上、解説おわり。

終わりに

今回の問題が複雑かどうかは、それを考える人の数学レベルによると思う。私は数学が苦手な方なのでかなり難しく感じた。たった1行書かれていることを理解するのに4~5時間かかった。非常に疲れた。

しかし解決に至るまでの、なぜ分からないのか・どこで躓いているのかを自問しながら原因を探るプロセスは他の様々な問題解決に役立つとても良い経験になったような気がする。

3DプリンターでArduino Pro Micro with USB Host Shieldのケースを作成

以下の記事で作成したPS4コントローラーをマクロキーボードとして使うためのツールについて、これまで基盤むき出しで使ってたのだが今回思い腰を上げて3Dプリンターでケースを作ってみた。
thom.hateblo.jp

まずはFusion360でモデリングし、
f:id:t-hom:20210106191819p:plain

Curaで3Dプリンター用データに変換して印刷。
f:id:t-hom:20210106192029p:plain

基盤をはめ込んで、
f:id:t-hom:20210106191956p:plain

蓋をしめたら完成。
f:id:t-hom:20210106192248p:plain

使用イメージはこんな感じ。電源LEDが透けてしまうけどまぁこれはこれで分かりやすいのでOK。ちなみに光を透過させたくない場合にアルミホイルを使う方法があるようだけど、導電体なので基盤に接触する可能性がある場合は絶縁体で挟むなど工夫は必要になると思う。
f:id:t-hom:20210106191659p:plain

今まで別に基盤剥きだしで困ったわけではないけどやはりケースがあるとプロダクト感が出てとても愛着が湧く。

以下、失敗談など

これだけ見ると簡単そうに思えるかもしれないけど、実際には色々と失敗もあった。

初号はこちら。
f:id:t-hom:20210106193246p:plain
そもそも寸法ミスッて全然ハマらなかったらどうしようと心配してた割にカチっとはまってなかなか良いのでは?と思ったんだが。。

このとおり。
f:id:t-hom:20210106192517p:plain
USBマイクロ側の穴をつくる際にフタ側の高さを測る基準線を間違えてハマらず。またボディーとフタの接合部が細すぎて破断。

二号は壁の厚みを1ミリから2ミリに増やしたのだが、Micro USBの穴のX位置を片側の壁からの距離で中心決めをしてた関係で位置がズレて失敗。
f:id:t-hom:20210106193850p:plain

三号はフタもかっちり締まり、穴の位置もいい感じ。
f:id:t-hom:20210106194316p:plain

これは勝ったな!と思ったんだけど、Micro USBを挿そうとしたら基盤がスライドしてニョキっとなった。。
f:id:t-hom:20210106194239p:plain

初号と二号はそこまでたどり着くまえに没だったので気付いてなかった。

そして四号。基盤がズレないようにストッパーを入れて、これが今回の最終形となった。
f:id:t-hom:20210106194450p:plain

しかし一つ問題が。。このケースは基盤がしっかり固定されるのだが、一度取り付けるとかなり外すのに苦労する。
3Dプリント時にベッドに塗ったノリを洗うのに一度外したが、精密マイナスドライバーで何とか外れたと思ったらUSB差し込み口が微妙に曲がってた。(力をくわえて直した。)

まぁ問題って言っても頻繁に外すものではないしとりあえずこのまま使うつもりである。

蓋の固定をどうするか悩んだけど、リセットボタンをつけてないので接着剤固定だとアクセスできなくなるし、今回蓋とボディーの接合部の隙間がゼロで印刷してるので閉めにくいけど閉ったあとはうまくかみ合ってカッチリ固定されている。落とさない限りは多分大丈夫なのでこのまま運用しようと思う。


今回色々失敗して学んだこととして、寸法決めするときは後で修正が入るかもしれない場所を基準に選ばないこと。今回のケースではケースの壁の厚みを後から調整したことでいろんな部位の寸法が影響を受けて位置ズレを起こした。

あとこれまで知らなかったのだが、寸法は計算式で入れられる。
f:id:t-hom:20210106195840p:plain
電卓と格闘するというのも間違いやすい原因になるのでこの機能は積極的に活用していこうと思う。

PS4コントローラーのアナログレバー(L2、R2)によるスクロールの改良案

今回は以前作成したPS4コントローラーをマクロキーボードとして使う件の改良案を紹介。
thom.hateblo.jp

従来のコードでスクロールを実現する仕組み

ブラウザや電子メールの閲覧ウインドウをスクロールする際は通常マウスホイールを使うが、上記のマクロキーボードではカーソルキーの「↓」と「↑」で代用している。
スクロールのスピードは、キー入力とキー入力の間に「↓、delay、↓、delay、↓、delay」のように遅延を挟み、この遅延量によってコントロールしている。

以前のコードは次のとおりである。

    if (PS4.getAnalogButton(R2)) {
      Keyboard.press(KEY_DOWN_ARROW);
      delay(256-PS4.getAnalogButton(R2));
      Keyboard.releaseAll();
    }

L2、R2キーはレバー式のボタンになっていて、押し込み量によって0~255までの値を取得できるので、256からボタンの押し込み値をマイナスした数値をDelayに使うことで押し込み量によってDelay値が1~255までの値になる。

問題点

問題その1

1つ目の問題はリモート環境でこれを使うと、最大まで押し込んでしまうとボタンを離してもしばらくスクロールが続いてしまうこと。
実際には記事を書いてリリースした直後には既に気づいていたんだけどまぁ使えるからいいやって感じで放置してた。。

原因は、Delayが少なすぎてアプリ側の処理が追い付かず、キー入力が遅れて到達する為だ。
パソコンのOSはマルチタスクで動作しているのでアプリの遅延などは日常茶飯事であるが、だからと言って入力したはずのキーがスキップされてしまうようなことがあっては使い物にならない。よって、キーボード入力はシステムメッセージキューに蓄えられ、それからアプリに送信される仕組みになっているようだ。
蓄えられるといっても通常人間が入力するようなスピードだと溜まることなくアプリケーションに到達するが、今回のようにプログラムから間髪入れずにキーが届く場合はアプリ側の処理が追い付かず、ボタンを離しても既にキューに溜まっている分がすべて出ていくまではアプリケーションにキーが送られ続けるということが起きる。

(参考)
wisdom.sakura.ne.jp

以前のコードではDelayの最小値が1となり、少なすぎた。色々検証した結果、私のケースではDelay が12以上あればアプリ側の処理を超えることが無いことが分かったので、最小Delayを12にすることにした。

問題その2

2つ目の問題はボタンの押し込み量で適切なスピードに調節するのが難しいこと。
そもそも1~255までの値を取れるといっても、狙って1ずつUpさせるのは無理である。
せいぜいこれくらい↓の操作が限界。
f:id:t-hom:20210105201028p:plain

そして、画面スクロールに必要なスピードの種類を考えてみると、3種類で十分であることが分かった。

低速スクロール:表示位置に拘りたい場合の位置合わせ用
中速スクロール:普通に文章を読む際のページ送り用
高速スクロール:読み飛ばしながら目的箇所を探す用

解決策

最終的に次のようなコードでやりたいことができた。

    int r2 = PS4.getAnalogButton(R2);
    if (r2) {
      Keyboard.press(KEY_DOWN_ARROW);
      if (r2 < 100) {
        delay(80);
      } else if (r2 < 255) {
        delay(24);
      } else {
        delay(12);
      }
      Keyboard.releaseAll();
    }

今回没にしたがアイデア備忘録として

L2とR2それぞれ3状態が取れるとしたら、それぞれの強弱を組み合わせて5段階くらいのスピード調節もできることに気づいたので一応アイデア備忘録として書いておく。
f:id:t-hom:20210105204240p:plain
まぁ今回は3段階で十分なのでこのアイデアは使わないんだけど。

また、押し込み量のみでなく押し込み時間で加速させていくという案も考えたので備忘録っておく。
たとえば最初の1秒は低速、2秒目から1秒ごとに加速する。緻密な操作が必要なときは、1秒押し込む度に放せば良い。

今回の学び

さて、今回学んだのはアナログボタンだからといってアナログ値をそのまま使うことに拘らなくて良いということ。
実現したいことが何なのかという目的に立ち返って考えることが重要だと思った。

こういう試行錯誤を繰り返すことで、直感的に操作できるデバイスになっていくんだろうなと思う。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。