mecanimのアニメーションイベントで、ジャンプしてから着地するまでを自然に表現する


## やりたいこと
- キーを押したらキャラクターがジャンプするアニメーションに遷移する
- ジャンプ処理は `Rigidbody.AddForce` を使って物理エンジンに任せる
- 地面に着地するときにアニメーションも丁度地面に着地する

## 現実
- アニメーションの再生速度とジャンプ力が合っておらず、空中で既に着地モーションに入っている
- Apply Root Motionにすれば自然な跳躍になるけど、ジャンプ力の調整をアニメーションファイル側でやることになるので融通が効かない

こんな感じで悲しい気持ちになった人結構いるんじゃないかと思ってます。
一般的なジャンプってこういう風に分割できるのかなと考えます

1. 膝を曲げて足が地面から離れるまで : 再生時間が固定
2. 地面を離れている : 再生時間がどのくらいあるかわからない
3. 足が地面に着いて、着地完了するまで : 再生時間が固定

2の状態がどのくらい続くのか状況によって変わるので、この部分と実際のアニメーションの再生を同期させれば解決するっぽい。

なので、それぞれ三つのフレームにイベントを仕込んでスクリプトを書いてしまいます。
アニメーションイベントについては下記のエントリが死ぬほど参考になります。

【Unity】 Unity4.3のMecanimのアニメーションイベントの使い方 http://posposi.blog.fc2.com/blog-entry-252.html

今回の場合は、ジャンプアニメーションの三つのフレームにイベントを仕込みます。
- 膝を曲げて、ジャンプする瞬間 : `OnJumpStart`
- ジャンプの昇り部分の頂上部分 : `OnJumpTopPoint`
- 地面につま先が着いた瞬間 : `OnJumpEnd` 

```csharp
using UnityEngine;
using System.Collections;

public class JumpMotionController : MonoBehaviour {

    private Animator animator;
    /// <summary>
    /// アニメーションのデフォルトの再生速度
    /// </summary>
    private float defaultSpeed;
    /// <summary>
    /// 着地判定を調べる回数
    /// </summary>
    private readonly int landingCheckLimit = 100;
    /// <summary>
    /// 着地判定チェックを行う時間間隔
    /// </summary>
    private readonly float waitTime = 0.05F;
    /// <summary>
    /// 着地モーションへの移項を許可する距離
    /// </summary>
    private readonly float landingDistance = 1F;
    
    public float jump;
    void Start()
    {
        animator = GetComponent<Animator>();
    }

    /// <summary>
    /// ジャンプモーションで、足が離れる瞬間に呼び出されるメソッド
    /// </summary>
    void OnJumpStart()
    {
        defaultSpeed = animator.speed;
        // キャラクターをジャンプさせる
        rigidbody.AddForce(Vector3.up * jump, ForceMode.VelocityChange);
    }

    /// <summary>
    /// ジャンプモーションで、頂点のフレームで呼び出されるメソッド
    /// </summary>
    void OnJumpTopPoint()
    {
        // アニメーションを停止して、着地判定のチェックを行う
        animator.speed = 0;
        StartCoroutine(CheckLanding());
    }

    /// <summary>
    /// ジャンプモーションで、足が地上に着いたときに呼ばれるメソッド
    /// </summary>
    void OnJumpEnd()
    {
        // なんかいい感じの処理
    }

    /// <summary>
    /// 足下との距離を計算して、一定距離まで近づいたらアニメーションを再会させる
    /// </summary>
    /// <returns>The landing.</returns>
    IEnumerator CheckLanding()
    {
        // 規定回数チェックして成功しない場合も着地モーションに移行する
        for(int count = 0;count < landingCheckLimit; count++)
        {
            var raycast = new RaycastHit();
            var raycastSuccess = Physics.Raycast(transform.position, Vector3.down, out raycast);
            // レイを飛ばして、成功且つ一定距離内であった場合、着地モーションへ移項させる
            if(raycastSuccess && raycast.distance < landingDistance) break;
            yield return new WaitForSeconds(waitTime);
        }
        animator.speed = defaultSpeed;
    }
}
```

ポイントは二つ

- ジャンプの頂上 `OnJumpTopPoint` に来たとき、アニメーションの `speed` を0にして、一時停止状態にする
- 自分の真下にある地面との距離を常にチェックする。距離が一定範囲内であれば `speed` を戻しアニメーション再生を再開する

`CheckLanding` メソッドは、キャラクターの位置から真下にレイを飛ばして自分と地面との距離を算出し、一定距離内であればアニメーションの再開、そうでなければ `waitTime` 秒後に再度チェックします。

ジャンプアニメーションの頂上に来たときに `CheckLanding` をコルーチンとして呼び出しておけば、近くに床を検知したときに自然と着地するかのようなモーションに移項できます。

### 欠点
- RayCastは軽い処理というわけではないので、あんまり連続で呼び出すと辛い

waitTimeを設定しているのはこのため。ほどよくバランス調整した方が良さそう。

- 実際使うときは、真下に地面以外のオブジェクトがあるのも考えられる

クリボー的な物を踏んで着地はなんか違う気がするので、レイヤー分けをきちんとしておき、`Physics.Raycast` で引数にレイヤー番号を渡すように修正すればよさそう。


これでもまだまだ自然なジャンプとは言い難いけどある程度マシにはなる気がする。
ちなみにUnity5だとアニメーションのstate一つ一つにスクリプトをアタッチできるようになるので、またやり方変わると思います。

俺はこうやってるよ！というもっと良い手法があればどなたか教えていただきたいです。