Unityで自走式ロボットアームを作ろう！

[株式会社サイシード](http://www.sciseed.jp/)のインターンシップとしてUnity上で自走式ロボットアームを作成しました。ここでは、そのロボットアームをどのように作成したかを書きます。
##やったこと
下の動画が完成したアームです。積み上がったキューブをハノイの塔のルールに従って移動させているのが分かるかと思います。さらに、キューブを置く位置を初めに変更できる機能もあります。
![cut_gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529468/48adc960-6e67-a17d-d79a-245d2cf418bb.gif)
####今後
今回はUnity上の仮想的なロボットアームでしたが、次のようなロボットアームを用いると十分実現可能のように考えられます。__今後はこういったハードウェアに当記事のような技術を適用していきたいと考えています。__
![detail_2675_1492765210.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529468/877af37c-5d17-b10c-0528-0cc04f7814a9.gif)
※Physical Computing Lab の「DOBOT Magician」のページ
https://www.physical-computing.jp/page/14

#作成手順
なかなか複雑なので、以下のように手順を分けて説明します。

1. ハノイの塔の解き方について
2. キューブを取るためのアームの角度計算
3. 「掴む」「放す」について
4. 「移動」について
5. 状態の管理

##1. ハノイの塔の解き方について

情報系の方々ではよく扱われる問題のようですね。
アルゴリズムについては[こちらのページ](http://www13.plala.or.jp/kymats/study/C++/Hanoi/Hanoi.html)に詳しく記述されており、参考にしました。

今回はアームを使ってキューブを移動させるため、予めハノイの塔を解いた時に移動させる順番を記録しておき、あとで５.に示すフローチャートのループごとに、ひとつずつターゲットとなるキューブをゴールとなる塔へ移動させます。
つまり始めに動かすべきキューブの順序を決定しておき、あとはそれに従いアームを動作させます。

##2. キューブを取るためのアームの計算角度
アームを動かすために、各アームの角度計算をしなければなりません。
ターゲットとなるキューブの座標やアーム自身の座標から(逆)三角関数を解いて角度を求めていきます。
![mg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529468/2ff645fd-399d-7bc5-90ab-74a62d026f94.png)
上図に示すように、角度$\theta, \phi$とおきます。また、アームを構成する3つの腕の長さを根元に近い方からそれぞれ$L, l, \epsilon$と設定します。
いま、アームが動く図にある平面内において、ターゲットとなるキューブの座標を$(x, y)$とおきます。この場合次の2式が成立します。

~~~math
\begin{align}
x &= L\sin\theta + l \sin \phi\\
y &= L\cos\theta + l\cos\phi - \epsilon
\end{align}
~~~
ここで、

~~~math
\begin{align}
\beta &=\cos^{-1}(\frac{x^2+(y+\epsilon)^2-l^2-L^2}{2lL})\\A&=\sqrt{(\frac{x^2+(y+\epsilon)^2-L^2+l^2}{2l})^2+L^2\sin^2\beta}
\end{align}
~~~
とおく。$\phi, \theta$について解くと、

~~~math
\begin{align}
\phi &= \tan^{-1}\left(\frac{x}{A}\right)-\tan^{-1}\left(\frac{2lL\sin\beta}{r^2+(y+\epsilon)^2}\right)\\
\theta &= \phi+\beta
\end{align}
~~~
と表すことができます。

##3. 「掴む」「放す」について
実際に掴んだり離したりなど主要な動作についてです。これらを表現するために、以下のような処理をするスクリプトをアームにアタッチしています。

####アームの角度

いま、「2.キューブを取るためのアームの角度計算」における長さが$L$のアームをアーム1、$l$のアームをアーム2とします。2つのアームにはそれぞれ別のスクリプトをアタッチさせます。
ここで$x$には$\theta$または$\phi$を次のように代入します。

~~~C#
if (x - theta < -0.5) {
    x += 0.5f;
}
else {
    //this.GetComponent<RotationConstraint>().enabled = true;       
    //のようにして、Constraintを有効にする
}
~~~
if文の条件である`x-theta<-0.5`が`false`になったときに`Position Constraint`(後述)が有効になるようにあらかじめ設計しておくと、キューブを掴むことができます。掴んだ後、先ほどとは逆に`x -= 0.5f`のようにして`x`に値を代入し直して持ち上げることができます。

`Update()`内で次のようにして角度を更新します。これにより角度`x`が反映されます。

~~~C#
transform.rotation = Quaternion.Euler((int)x, 0, 0);
~~~
※`x`をfloat型で代入すると小数の桁のせいで震えが生じる為、わざとint型で代入しています。
####掴む
先に述べましたが、__結論から言うとUnityのConstraintというコンポーネントが便利です。__PositionやRotationなどを指定したオブジェクトとリンクさせることができます。ここではターゲットのキューブに`Position Constraint`をアタッチし、アームの先が十分近づいたときに`Position Constraint`が有効になるようにしました。これにより「掴む」を表現できます。`Constraint`については[こちらのページ](http://tsubakit1.hateblo.jp/entry/2018/01/12/233522)を参考にしました。
####放す
「放す」ときも同じで、`Position Constraint`を無効にすれば放すことができます。
アームの角度について、掴むときは__2.__の$x$, $y$をターゲットとなるキューブの座標として$\theta$, $\phi$を求めました。放す際はこれと同様にして__2.__の$x$, $y$にゴールとなる塔の座標を入れ、$\theta$, $\phi$を求めます。

##4. 「移動」について
ターゲットとなるキューブ、またはゴールとなる塔が、アーム自身と離れていた場合はアーム自体が移動する必要があります。

###アームの土台回転
ターゲットに向けてアームを回転させます。
まずコードを以下に示します。

~~~C#
void Catch() {
    floor_rot = Mathf.Atan2(x_relative, z_relative) * Mathf.Rad2Deg;

    if (y - floor_rot < 180 && y - floor_rot > 0) {
        y -= 1.5f;
    }
    else if (y - floor_rot > -360 && y - floor_rot <= -180) {
        y -= 1.5f;
    }
    else if (y - floor_rot >= 360) {
        y -= 1.5f;
    }
    else if (y - floor_rot <= -360) {
        y += 1.5f;
    }
    else if (y - floor_rot < 0 && y - floor_rot >= -180) {
        y += 1.5f;
    }
    else if (y - floor_rot < 360 && y - floor_rot >= 180) {
        y += 1.5f;
    }
    else {
        y += 1.5f;
    }
}
~~~
上のような`Catch()`関数をメインループ`Updata()`内で呼び出すことによりアームを旋回させます。キューブの位置によって、最短での回転方向が右回りか左回りに決まります。このため、回転の方向が変わるように条件を細かに分けます。このとき、条件の境界に空白が生まれないように注意します。ただし、`x_relative`, `z_relative`は相対的なキューブの位置です(後述)。

###土台の移動

いま、取りたいキューブの座標とキューブを持っていきたい塔（ゴール）のアームからみたときの相対的な座標を

~~~C#
x_relative = target_pos_x - transform.position.x;
y_relative = target_pos_y - transform.position.y;
z_relative = target_pos_z - transform.position.z;

x_goal_relative = goal_script.pos_x - transform.position.x;
y_goal_relative = goal_script.pos_y - transform.position.y + adjust;
z_goal_relative = goal_script.pos_z - transform.position.z;
~~~
とします。`transform.position.x`などはアーム自身の座標です。`target_pos_x`などはキューブの座標です。ここで`adjust`はキューブを積むときの置く高さです。既にキューブがおいてあれば、その分高く積む必要があります。そこで下の`Height()`で高さを決定しました。

~~~C#
void Height() {
    adjust = 0;
    if (target[work_times + 1] != 0 && Mathf.Abs(goal_script.pos_z - target_script.pos_z) < 2f) {
        adjust += 2.0f;
    }
    if (target[work_times + 1] != 1 && Mathf.Abs(goal_script.pos_z - target_script.pos_z) < 2f) {
        adjust += 2.0f;
    }
    if (target[work_times + 1] != 2 && Mathf.Abs(goal_script.pos_z - target_script.pos_z) < 2f) {
        adjust += 2.0f;
    }
}
~~~

といったようにキューブの数だけ置くべき高さを調整(adjust)しましょう。
ハノイの塔をあらかじめ解いてあるので、いまキューブがどこにあるかはわかると思います。if文の条件はこれらから見積もります。

求めた相対的な座標から以下のように実際に移動します。

~~~C#
void Move() {
    Vector3 pos = transform.position;
    velocity_x = 0.2f * Mathf.Abs(x_relative) / Mathf.Sqrt(x_relative * x_relative + z_relative * z_relative);
    velocity_z = 0.2f * Mathf.Abs(z_relative) / Mathf.Sqrt(x_relative * x_relative + z_relative * z_relative);

    if (x_relative > 0) {
        pos.x += velocity_x;
    }
    else if (x_relative < 0) {
        pos.x -= velocity_x;
    }

    if (z_relative > 0) {
        pos.z += velocity_z;
    }
    else if (z_relative < 0) {
        pos.z -= velocity_z;
    }
    transform.position = pos;
}
~~~
ここで`velocity_x`, `velocity_z`は移動速度です。取りたいキューブにまっすぐ進んでいくようにノルムをとって割っています。
下部のif文で`velocity_x`, `velocity_z`をアームの座標である`pos.x`, `pos.z`に足し合わせて移動します。

キューブを取った後の塔(ゴール)への移動も同様です。`x_relative`, `z_relative`をそれぞれ`x_goal_relative`, `z_goal_relative`に置換するだけで大丈夫です。

##5.状態の管理
####動作の流れ
１〜４で各動作について説明しました。これらは、次のようなフローで動作します。
![flow.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529468/98fbcdfb-44e5-820e-8103-7d05f6ef737f.jpeg)
図のダイヤ型の判断部分ではbool型の変数をおいて、動作が完了していれば`true`にする、といったような処理を行いました。次にその例を示します。

~~~C#
if (catch_state == false && Floor_script.floor_state == true && Floor_script.move1_state == true) {
    Catch();
}
~~~
これらのフローは`Update()`内部においた関数を毎ループ呼び出すことにより動作しています。関数を実行するかどうかは上のようなbool型などの変数で状態管理を行っています。つまり、グラフィックのクオリティに動作速度が依存します。ビルドしたファイルを低質の画質で実行した場合、早回しのようになることに注意されたいです。
####実行前のアクション
先に述べました通り、ロボットアームの動作実行前に、今回はゴールとなる３つの塔の位置をスライダーでそれぞれ自由に移動させることができます。これは毎ループ、ターゲットとなるキューブとゴールとなる塔の座標を参照しているためです。
![cut2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529468/b4661d51-947e-0a3b-0beb-2963397d43d2.gif)
UIについて、特にスライダーの使い方は[こちら](https://algorithm.joho.info/unity/ugui-slider/#i)を参考にいたしました。
#まとめ
今回のために作成したC#スクリプトはすべて[github](https://github.com/YamaYama00/Arm)に上げておきました。

今回はUnityを用いました。Unityを使う場合はモデルを作るところが難関だと感じました。また、複数のコンポーネントを複雑に作動（例えば、HingeJointをアタッチしたオブジェクトにさらにHingeJointをアタッチしたオブジェクトをつける）させると、想定した動きをしないことがあります。

以上では自走式ロボットアームの制作を行いました。お読みいただきありがとうございました。