【Unity】快適なジャンプ操作について考えてみる

[PONOS Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ponos) 22日目の記事です。
昨日は@FW14Bさんの[【Unity】32bitアーキテクチャを切り捨てれば開発効率が上がる](https://qiita.com/FW14B/items/d9fc4c68072b87d3dfde)でした。

### はじめに

アクションゲームを始めとした様々なゲームには、ジャンプ操作があります。
ジャンプと言っても動かし方は色々ですが、今回はボタンを押し続けると上昇量が高くなるタイプのジャンプをUnityで実装します。

### 実装するもの

キーを押すと上昇し、一定時間飛ぶかボタンを離すと下降。地面に着地すると停止し、再度ジャンプが出来るようにします。
状態としては**「接地・上昇・落下」**の3つがある感じです。

この3つの状態を識別できるようにEnum型を作成し、その変数を使います。

``` c#
enum Status
{
    GROUND = 1,
    UP = 2,
    DOWN = 3
}
```

接地判定はOnCollisionで取ると楽です。
下記は「落下状態の時、名前が"Ground"の物体に触れると接地状態にする」関数です。

``` c#
void OnCollisionEnter2D(Collision2D collision)
{
    if (playerStatus == Status.DOWN && collision.gameObject.name.Contains("Ground"))
    {
        playerStatus = Status.GROUND;
    }
}
```

ただしこれだと壁に横からぶつかった時にも接地判定になってしまうので注意です。
下からの接触判定のみ取りたい場合、**ContactFilter2D**を使うと便利です。(この記事では割愛します)

### シーン設定

まず地面とジャンプさせたい物体を置きます。
![Scene.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924601/c59b63e7-b065-c281-d748-aaadba8d8f21.png)
地形にコライダーを、ジャンプさせたい物体にコライダーとRigidbodyをアタッチしておきます。

### 飛ばしてみる

PlayerJump.csとしてスクリプトを作ってみました。
Update内でキー入力を感知し、それに合わせてFixedUpdate内で物体の状態変化・速度付けを行っています。

ジャンプの際に初速を与え、地面に着地するまで重力をかけ続けます。

``` c#
public class PlayerJump : MonoBehaviour
{
    Rigidbody2D rigidbody2d;
    Status playerStatus = Status.GROUND; // プレイヤーの状態

    float firstSpeed = 16.0f; // 初速
    float gravity = 30.0f; // 重力加速度

    float timer = 0f; // 経過時間
    bool jumpKey = false; // ジャンプキー

    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        // キー入力取得
        jumpKey = Input.GetKey(KeyCode.Space);
    }

    void FixedUpdate()
    {
        Vector2 newvec = Vector2.zero;

        switch (playerStatus)
        {
            // 接地時
            case Status.GROUND:
                if (jumpKey)
                {
                    playerStatus = Status.UP;
                }
                break;

            // 上昇時
            case Status.UP:
                timer += Time.deltaTime;

                if (jumpKey && rigidbody2d.velocity.y >= 0f)
                {
                    newvec.y = firstSpeed;
                    newvec.y -= (gravity * timer);
                }

                else
                {
                    playerStatus = Status.DOWN;
                    timer = 0f;
                }
                break;

            // 落下時
            case Status.DOWN:
                timer += Time.deltaTime;

                newvec.y = 0f;
                newvec.y = -(gravity * timer);
                break;

            default:
                break;
        }

        rigidbody2d.velocity = newvec;
    }

    void OnCollisionStay2D(Collision2D collision)
    {
        if (playerStatus == Status.DOWN &&
            collision.gameObject.name.Contains("Ground"))
        {
            playerStatus = Status.GROUND;
            timer = 0f;
        }
    }
}
```

上記のコードでジャンプさせるとこんな感じになります。
![jump1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924601/d5afc7c2-8b11-d944-7b59-933284b1ad55.gif)
スペースキーを長押しすると高く跳び、途中で離すと低く跳んでいますね。

### さらに工夫してみる

これだけでもジャンプは出来ているのですが、もう少し調整します。

現状では、
1. 一瞬だけキーを押した時の上昇量が低すぎる。
2. 途中でジャンプを止めた際の動きが不自然。急に下に落ちる。
3. キーを押しっぱなしにすると着地後に再度ジャンプが行われてしまう。
4. 動きがもっさりしている。

といった問題点が挙げられます。

特に4の「**動きがもっさりしている**」という点。
現状の動きは概ね物理法則に沿っているはずなのですが、それではメリハリの無い動きになってしまうようです。
現実の落下運動とは異なりますが、もっと極端に速度変化を付けてみたいところです。

### 改善方法

先ほど挙げた問題点を改善してみます。

まず1と2について。

> 1.一瞬だけキーを押した時の上昇量が低すぎる。
> 2.途中でジャンプを止めた際の動きが不自然。急に下に落ちる。

途中でキーを離した際に急に落下するようになっているので、緩やかに(かつ早めに)落下するようにします。

``` c#
            // 上昇時
            case Status.UP:
                timer += Time.deltaTime;

                if (jumpKey || jumpLowerLimit > timer)
                {
                    newvec.y = firstSpeed;
                    newvec.y -= (gravity * Mathf.Pow(timer, 2));
                }

                else
                {
                    timer += Time.deltaTime; // 落下を早める
                    newvec.y = firstSpeed;
                    newvec.y -= (gravity * Mathf.Pow(timer, 2));
                }

                if (0f > newvec.y)
                {
                    playerStatus = Status.DOWN;
                    newvec.y = 0f;
                    timer = 0.1f;
                }
                break;
```

キーを離したら急に落下に移行するのではなく、縦方向の移動速度がマイナスになった時点で落下に移行するようにしました。
また途中でキーを離した場合は上昇速度が減るのが早くなります。

---

次に3について。
> 3.キーを押しっぱなしにすると着地後に再度ジャンプが行われてしまう。

着地時にキーを押しっぱなしだった場合、離すまでキー操作をロックする事でジャンプを防ぎます。
これはキーロック用の変数を用意する事で対策します。

---

次に4について。
> 4.動きがもっさりしている。

``` c#
newvec.y = firstSpeed;
newvec.y -= (gravity * timer);
```

最初のコードでは物理法則に沿った動きをさせていました。

``` c#
newvec.y = firstSpeed;
newvec.y -= (gravity * Mathf.Pow(timer, 2));
```
色んな改善方法がありそうですが、2次関数を用いて速度変化を強めます。

### 改善後の動き

修正後のコードがこちらです。

``` c#
public class PlayerJump : MonoBehaviour
{
    Rigidbody2D rigidbody2d;
    Status playerStatus = Status.GROUND; // プレイヤーの状態

    float firstSpeed = 16.0f; // 初速
    const float gravity = 120.0f; // 重力
    const float jumpLowerLimit = 0.03f; // ジャンプ時間の下限

    float timer = 0f; // 経過時間
    bool jumpKey = false; // ジャンプキー
    bool keyLook = false; // キー入力を受け付けない

    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        // キー入力取得
        if (Input.GetKey(KeyCode.Space))
        {
            jumpKey = !keyLook;
        }
        else
        {
            jumpKey = false;
            keyLook = false;
        }
    }

    void FixedUpdate()
    {
        Vector2 newvec = Vector2.zero;

        switch (playerStatus)
        {
            // 接地時
            case Status.GROUND:
                if (jumpKey)
                {
                    playerStatus = Status.UP;
                }
                break;

            // 上昇時
            case Status.UP:
                timer += Time.deltaTime;

                if (jumpKey || jumpLowerLimit > timer)
                {
                    newvec.y = firstSpeed;
                    newvec.y -= (gravity * Mathf.Pow(timer, 2));
                }

                else
                {
                    timer += Time.deltaTime; // 落下を早める
                    newvec.y = firstSpeed;
                    newvec.y -= (gravity * Mathf.Pow(timer, 2));
                }

                if (0f > newvec.y)
                {
                    playerStatus = Status.DOWN;
                    newvec.y = 0f;
                    timer = 0.1f;
                }
                break;

            // 落下時
            case Status.DOWN:
                timer += Time.deltaTime;

                newvec.y = 0f;
                newvec.y = -(gravity * Mathf.Pow(timer, 2));
                break;

            default:
                break;
        }

        rigidbody2d.velocity = newvec;
    }

    void OnCollisionStay2D(Collision2D collision)
    {
        if (playerStatus == Status.DOWN &&
            collision.gameObject.name.Contains("Ground"))
        {
            playerStatus = Status.GROUND;
            timer = 0f;
            keyLook = true; // キー操作をロックする
        }
    }
}
```

これで動かした場合、こうなります。
![jump3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924601/d3741c76-8f54-0d9f-310c-db78458787b8.gif)
先ほどと比べると、メリハリの付いた動きになりました。

![jump2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924601/d13d9ea5-8099-951f-fc46-7901d77efab3.gif)
途中でキーを離したりしても、自然な動きで落ちていきます。

# まとめ

キーを押した長さに応じてジャンプの高さを変えるコードを実装し、その改善を行いました。
今回は動きの分かりやすさ・操作のしやすさを重視してメリハリの付いた速度付けを意識しています。

動きの付け方ですが、どれが正解という物でもありません。
リアルさを重視したいのであれば物理法則に沿った物を実装するのもアリです。もっと極端な動きを付ければ、インパクトのあるゲーム画面が生まれる事でしょう。
どういう動きを付ければ気持ち良い操作感が生まれるか、試行錯誤するのもまた楽しみの一つですね。

以上です。
明日は@ackylaBさんの記事です。