ユニティちゃん2Dで2Dキャラクターの動きの仕組みを理解する

#TL; DR
はじめて2Dゲームを作るにあたって、ユニティちゃん2Dの仕組みを理解することがとても参考になったので、
本記事はユニティちゃん2Dを触りつつ、改変を加えることでどういう動きになるのかを試しながら、2Dキャラの動きを理解していこう、という趣旨になります。


#ユニティちゃん2D
<img width="957" alt="スクリーンショット 2018-02-06 22.25.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/97286/fbb4abdf-389c-5ce8-3e85-21872f1eb24b.png">

ユニティちゃん2Dは、みなさんご存知オープンソース系アイドル「ユニティちゃん(Unity-Chan)」の2Dゲームのサンプルプロジェクトです。
2014年6月に最新版のv1.1が公開されて以降更新はありませんが、2Dキャラの移動だけでなく、敵キャラクターの存在や破壊できるブロック、取得できるコインなど、2Dゲームの要素として基本的なものが揃っている大変いい教材だと思います。
http://unity-chan.com/download/releaseNote.php?id=UnityChan2D

#導入手順
導入手順については詳細は省きますが、簡単に以下のような流れでできます。
1. ユニティちゃん2DのDLページ(上記URL)から、「`UnityChan2D.unitypackage`」をダウンロードする
2. Unityから空の2Dプロジェクトを開き、DLした`unitypackage`を読み込む
3. Titleのシーンを開き、実行して起動できれば完了
※なお、確認時は5.5.4p3を使用しています

#とりあえず動かしてみる
「左右キー」や「スペースキー」を押すことで、ユニティちゃんが左右に動いたり、ジャンプしたりします。
<img width="721" alt="スクリーンショット 2018-02-06 22.33.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/97286/36228c6c-8d9a-7eab-c061-7d4955d2a6ac.png">


#どうやって動いているのか
なぜ左右キーを押すと左右に動き、スペースキーを押すとジャンプするのかを見ていこうと思います。
`Inspector`のユニティちゃんを見てみると、`UnityChan2DController.cs`が`AddComponent`されています。
ユニティちゃんの制御はこのコントローラーによって行われているようですので、中身を見てみると、下記のような処理があります。

```c#
void Update()
    {
        if (m_state != State.Damaged)
        {
            float x = Input.GetAxis("Horizontal");
            bool jump = Input.GetButtonDown("Jump");
            Move(x, jump);
        }
    }
```

どうやら`Update()`の中で常に呼ばれているこの`Move()`というメソッドがユニティちゃんを動かしているような気がしますね。
では、より細かく内容を見ていきます。

## なぜ横キーを押すと横に動くのか？
なぜユニティちゃんは右キーを押すと右に、左キーを押すと左に動くのでしょうか。
これは、`Move()`に引数として渡されている`x`によって決まっています。

```c#
float x = Input.GetAxis("Horizontal");
```
ここの`Input.GetAxis`は、キーボードやジョイスティックの入力によって、-1から1の間で値を返すメソッドです。そして、引数の`String`によって、下記のように使い分けられます。
- `Horizontal` -> 左右の入力を対象とする
- `Vertical` -> 上下の入力を対象とする
要するにここは、「何も押してなければ0、左右キーが押されれば、-1〜1の間の値を返す」という処理のようです。

例えばここを以下のように書き換えます

```c#
float x = Input.GetAxis("Vertical");
```

これでゲームを実行すると、ユニティちゃんは**左右キーを押しても反応せず、上キーを押すと右に、下キーを押すと左に**動きます(`Horizontal`の"右"と同様に、`Vertical`の"上"が「正」の値を取るためです)

というわけで、(`Horizontal`なら)`x`には左右キーを押すことで-1〜1の値を取ることがわかりました。
おそらくこの`x`が`Move()`の中でユニティちゃんを左右に動かしているのでしょう。

## なぜ徐々に加速するのか？
ユニティちゃんはキーを押すと、緩やかに加速して最高速度に達すると一定の速度になっていることがわかります。
これも、`Input.GetAxis()`の仕様によるものです。
試しに`Debug.Log`を差し込み、キーの「右」を押し込んでみます。
すると、`x`の値は0から徐々に1になっていくことがわかります。

<img width="882" alt="スクリーンショット 2018-02-07 19.12.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/97286/3c6d2bcd-6e31-2a8f-59ab-005bdd3bfa2a.png">

また、離すと、`x`は徐々に0に近づいていきます。
ジョイスティックなどの場合は傾きの度合いを考慮して値を取得できるのですが、キーボードの場合はこのように、押し続けることで徐々に値が上昇するという特性があります。
その結果、ユニティちゃんは横キーを入れることで一瞬で最速点に到達になるわけではなく、徐々に速度が上がり最高速度に到達するようです。

では、実際に決定した`x`がどう使われているのか、`Move()`関数の中を見て行こうと思います。

```c#
void Move(float move, bool jump)
    {
        if (Mathf.Abs(move) > 0)
        {
            Quaternion rot = transform.rotation;
            transform.rotation = Quaternion.Euler(rot.x, Mathf.Sign(move) == 1 ? 0 : 180, rot.z);
        }

        ...
    }
```

まずはじめにここの処理を見て行こうと思います。
`Mathf.Abs()`は絶対値を取るので、この`if`は`move`が0以外、要するに何かしらのキーが押されていたら`true`になるようです。
`Mathf.Sign()`は引数が0と正の値の場合は1、負の値の場合は-1を返します。
`if`の中の処理は、自身の`Quaternion`(回転情報)を取得して、y軸に関してのみ、正の値であれば0、負の値であれば180としています。
要するにここは、入力されたキーにより、ユニティちゃんの「右向き」か「左向き」かを制御している部分だということがわかりました。

次に下記の処理

```c#

void Move(float move, bool jump)
    {
        ...

        m_rigidbody2D.velocity = new Vector2(move * maxSpeed, m_rigidbody2D.velocity.y);

        m_animator.SetFloat("Horizontal", move);

        ...
    }
```

`m_rigidbody2D`という変数の`velocity`に新しい`Vector2`を入れています。
まずこの`m_rigidbody2D`ですが、これはクラスの最初に定義されていて、ユニティちゃん自身の`GameObject`の`Component`の`Rigidbody2D`をさしています。
`Rigidbody2D`は簡単に言うとUnity標準搭載の物理エンジンで、重力や加速度や衝突などを制御するコンポーネントです。
ユニティちゃんにはこの`Rigidbody2D`が`AddComponent`されています。

で、問題はその`Rigidbody2D.velocity`に値を代入している点です。
`Rigidbody2D.velocity`は「速度」のことで、型は`Vector2`です。
要するにここはx軸とy軸に対しての速度情報を更新している処理になります。

y軸に対しては、自身の`velocity`のy値を入れているので、つまり「変更なし」ということになります。
一方、x軸は`move * maxSpeed`(クラスの上のほうで`10f`と宣言されています)を入れています。
`move` は `Input.GetAxis("Vertical")`によって得られた`float`値でしたよね。
つまり、ここは、入力された`-10〜10`の値が代入されています。

横キーを押したことでユニティちゃんが横に動くのは、まさにここで速度情報である`velocity`に値が代入されているから、というわけですね。

##なぜスペースキーを押すとジャンプするのか？
続いて、スペースキーを押すとなぜユニティちゃんがジャンプをするのかを見ていこうと思います。
まずは`move()`に入る前の`Update()`内の処理から

```c#
bool jump = Input.GetButtonDown("Jump");
```

なるほど、`Jump`を押したら`jump`が`true`になるんですね(？)

イメージだと、ここは「スペースキーを押すと」、`jump`が`true`になる、といった処理にしそうですが、この`Input.GetButtonDown`の引数には、`Jump`という`string`が渡されています。
つまり、どこかでこの`Jump`というのは「スペースキー」のことである、ということが設定されている、ということになります。

その場所は、下記を開くと設定されています。
`Edit > Project Setting > Input`
<img width="422" alt="スクリーンショット 2018-03-27 23.05.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/97286/51f16163-fa90-76e9-2d7e-92aaadeb2513.png">

上記を開くと、`Inspector`に`Axes`という項目が表示され、この中に先ほどの`Jump`という記述があり、その中の`Positive Button`というキーに対して、`space`と設定されていることがわかります。
これは`Input Manager`と呼ばれるもので、キーやJoystickの入力による各種設定が入っています。

<img width="311" alt="スクリーンショット 2018-03-27 22.55.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/97286/75b4d79d-3298-e997-bece-41fc16e26bbb.png">

ここで`space`という文言を、例えば`w`としてみると、ユニティちゃんはスペースキーを押してもジャンプしなくなり、代わりに`w`キーを押すことでジャンプすることがわかります。
このように、キーバインドはこの`Input Manager`で管理されている、ということですね。
`Positive Button`以外の各種細かな設定は別途Unityの`Documentation`の`Input Manager`を見られるといいと思います。

では、実際にこのスペースキーを押すと`true`になる`jump`は`move()`内でどのように使われているのかを見ていきます。

```c#
void Move(float move, bool jump)
{
        ...

    if (jump && m_isGround)
    {
        m_animator.SetTrigger("Jump");
        SendMessage("Jump", SendMessageOptions.DontRequireReceiver);
        m_rigidbody2D.AddForce(Vector2.up * jumpPower);
    }
}
```

まず、`if (jump && m_isGround)`ですが、この中の`m_isGround`は、名前からしておそらく「地面に立っているかどうか」を判定している`bool`だと思われますが、実際にどこでどう設定されているかというと、
`MonoBehaviour.FixedUpdate()`にて

```c#
void FixedUpdate()
{
    Vector2 pos = transform.position;
    Vector2 groundCheck = new Vector2(pos.x, pos.y - (m_centerY * transform.localScale.y));
    Vector2 groundArea = new Vector2(m_boxcollier2D.size.x * 0.49f, 0.05f);
    m_isGround = Physics2D.OverlapArea(groundCheck + groundArea, groundCheck - groundArea, whatIsGround);
}
```

といった記述があります。
`Physics2D.OverlapArea`とは、第一引数`Vector2`と第二引数`Vector2`によって作られた四角の領域が、コライダーと衝突しているかを返します。
`pos`はユニティちゃんの`position`、
`groundCheck`はユニティちゃんのちょっと下(`m_centerY`は`1.5f`)、
つまりこの`Physics2D.OverlapArea`は、
**ユニティちゃんのちょっと下にある横長い四角形**というわけですね。

<img width="427" alt="スクリーンショット 2018-03-28 0.43.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/97286/f0fa42f6-9626-678a-aa40-eabfa2819ec0.png">

そして、この`Physics2D.OverlapArea`が、第三引数の`whatIsGround`に限定したレイヤーのコライダーに重なっているかどうか？が、`m_isGround`というわけです。
このように、ユニティちゃんの足元に`OverlapArea`を用意しておき、これが地面に接していれば、結果的にユニティちゃんが「地面に立っている」と判断しているわけですね。

さて、ようやくこれで
`if (jump && m_isGround)`
の中に入る条件がわかりました。
つまり、「地面に接している状態でかつジャンプがtrueになった瞬間」というわけです。

では、肝心のその中の処理ですが

```c#
m_rigidbody2D.AddForce(Vector2.up * jumpPower);
```

ユニティちゃんがジャンプする処理は、まさにここになります。
ここでまた新しく、`Rigidbody2D.AddForce`というものが登場しました。
これはその名の通り、`Rigidbody2D`に「`Vector2`の力を加える」メソッドです。

`Vector2.up`とは、`Vector2(0f, 1f)`と同義なため、つまりこの処理は
y軸方向に、固定の`jumpPower`(`1000f`と定義されています)分、力を加える、という意味ですね。

これにより、ユニティちゃんは`1000f`のパワーをy軸の正の方向に受けることで、上に飛び上がり、
そして`Rigidbody2D`の物理演算により、落下する、といった原理になります。

##velocityとAddForce
最後に、今回ユニティちゃんが移動するにあたって左右の移動には`Rigidbody2D.velocity`を、ジャンプには`Rigidbody2D.AddForce`が使われていました。
ここでふと思うのが、

- 左右移動に`Rigidbody2D.AddForce`を使ってもいいのでは？
- ジャンプに`Rigidbody2D.velocity`を使ってもいいのでは？

という点です。
そもそもこの２つはどういった違いがあって、どのように使い分けるべきなのか？について触れていきたいと思います。

###velocityとAddForceの違い
`velocity`は**速度情報のプロパティ**であり、`AddForce`は**指定のベクトルに対し力を加えるメソッド**です。
つまり、そもそも`velocity`は重力、摩擦など物理演算によって算出される情報にあたります。そのため、`velocity`を直接書き換えるということは、**物理演算を無視して速度そのものを直書きする行為**となるわけです。
`AddForce`に`100f`の力を加えると、物理演算が考慮されますが、`velocity`に`100f`を加えると、突然速度`100f`になる、というわけです。

この違いは、下記のようにコードを書き換えるとわかりやすいです。

```c#
//m_rigidbody2D.AddForce(Vector2.up * jumpPower);
m_rigidbody2D.velocity = Vector2.up * jumpPower;
```
ユニティちゃんのジャンプで、「y軸方向に`1000f`の力を与える」という既存の処理を、「y軸方向を`1000f`の速度にする」という処理に書き換えてます。

この状態でユニティちゃんがジャンプをするとどうなるかというと、**消えます**。正しくはy軸の正の方向にとんでもない速度で吹っ飛んでいきます。重さなどが考慮されずに突然`1000f`の速度になるからですね。
ちなみに、velocityに`1000f`ではなく`20f`くらいにすると、だいたい`AddForce`に`1000f`を入れるのと同じくらいになります。

さて、ではなぜユニティちゃんは横移動の際に`velocity`を使っているのか？横移動も`AddForce`を使った方が良いのでは？と思うわけですが、これについては下記のような理由があるかと思われます（あくまで憶測です）

まず、試しに下記のようにコードを書き換えてみます。

```c#
//m_rigidbody2D.velocity = new Vector2(move * maxSpeed, m_rigidbody2D.velocity.y);
m_rigidbody2D.AddForce(new Vector2(move * maxSpeed, m_rigidbody2D.velocity.y));
```

ジャンプの時と同様、同じ値を`velocity`直書きではなく、`AddForce`の引数に渡すパターンです。
これで実行して横キーを少し押しっぱなしにしてみると、ユニティちゃんは入力されたキーの方向に力が加わるので動き始めるわけですが、キーを離しても止まらず、すーーーっと滑り続けます。

この理由は、ユニティちゃんの`Rigidbody2D`の`Linear Drag`(位置移動の減衰値)が0に設定されているためです(滑る床などで使えそうなプロパティですね)。
では試しに、この`Linear Drag`を2に変更してみます。
すると、ユニティちゃんはキーを話すことで徐々に減速して止まるようになるわけですが、今度は最高速度が下がったように感じます。
それもそうで、この`Linear Drag`はキーを押し続けているときももちろん有効ですので、移動中も常に減衰が入っているわけですね。
しかも、妙にキーの入力に対して加速度が遅くもっさりしています。

と、こんな感じで、横移動に関しては変に物理演算が入ることが、かえって妙なもっさり感や違和感を出してしまう場合もあるようです。
もちろん、もっと`AddForce`で加える力を大きくしたり、`Input.GetAxis("Horizontal");`で得た値をそのまま使わず`Mathf.Sign`などにかけてより急に力を加えるようにする、などのチューニングにより、自然さを出すことができるかと思いますが、それならもう`velocity`に直接速度を書き込むほうが簡単なようにも感じます。

もっと横を押したらスッと動き出して、止まるときもスッと止まって欲しい。
そんなスッキリした操作感に、`velocity`を直接書き換えることで対応しているのではないかと思います。


#おわりに

というわけで、ユニティちゃん2Dを使って、2Dキャラクターの動きの基本に当たる「横移動とジャンプ」を学んで見ました。いろいろと応用ができそうなものや、まだ触ってないプロパティなども多数あったので、それをうまく使って、楽しく2Dゲームを作れればと思います。

#ライセンス
本記事に登場したキャラクター(ユニティちゃん)、ゲーム画面、ソースコード等は[ユニティちゃんライセンス](http://unity-chan.com/contents/license_jp/)で提供されています。

![imageLicenseLogo.png](https://qiita-image-store.s3.amazonaws.com/0/97286/a434ecc4-495a-79de-e4ea-17fff6c4fb0e.png)