(この記事の使用環境: Unity5.4.2f2 Personal、Windows10


今回はキャラクター動作アニメーション中の位置の微調整について。
全画面キャプチャ 20170119 161442

UnityではたくさんのAssetでキャラクターを動作させるためのAnimationClipが提供されていますが、それをただアタッチするだけでは少々不自然な動作になることがあります。

こんな感じ(不自然な動作の例。ちょっと分かりにくいかもしれませんが・・・)。よじ登るアニメーションを付けてみたのですが、そのままでは物につかまるモーションで物に接しておらず、乗り上げるモーションでも物の上に乗れていませんでした。


で、これが調整後。ちゃんと物に掴まって、よじ登って乗り上げてい(るように見え)ます。


ではさっそく解説。

まずはAnimatorControllerの遷移図。Float型の変数「ClimbForwardOffset」(位置調整用オフセット)を作っています。この変数の値を使って位置調整を行います。
全画面キャプチャ 20170119 153552

遷移図の「Climb」のステートに、StateMachineBehaviourスクリプト「Climb」を作ってアタッチしています。
全画面キャプチャ 20170119 153950

で、これが「Climb」スクリプトの中身。
このスクリプトは「StateMachineBehaviour」を継承しており、あらかじめ定義された関数OnStateEnter、OnStateUpdate、OnStateExit等を使うことができます。今回はこれらの関数のなかでAnimatorの再生速度や位置調整用オフセットの値の設定を行っています。

using UnityEngine;
using System.Collections;

// --- よじ登りアニメーションにアタッチするStateMachineBehaviour "Climb" ----------------------
public class Climb : StateMachineBehaviour
{
    float offset = 0f;              // 移動オフセット(PlayerController.cs側でこの値を受け取って移動処理に使う)
    float deltaForward = 0.002f;     // 移動オフセットの漸増値


    // --- よじ登りアニメーション開始時に行う処理 ----------------------
    override public void OnStateEnter (Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.speed = 2.0f;
        offset = 0.012f;
        animator.SetFloat ("ClimbForwardOffset", offset);   // つかまる位置の調整
    }


    // --- よじ登りアニメーション中に行う処理 ----------------------
    override public void OnStateUpdate (Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if (stateInfo.normalizedTime > 0.25f && stateInfo.normalizedTime < 0.55f)   // ぶら下がりアニメーションは短縮する
        {
            animator.speed = 100f;
        }
        else if (stateInfo.normalizedTime > 0.55f)          // 対象物に乗り上げる際は、少し前進する
        {
            animator.speed = 2.0f;
            offset += deltaForward * Time.deltaTime;
            animator.SetFloat ("ClimbForwardOffset", offset);
        }
    }


    // --- よじ登りアニメーション終了時に行う処理 ----------------------
    override public void OnStateExit (Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.speed = 1.0f;
        animator.SetBool ("Climb", false);
        offset = 0f;
        animator.SetFloat ("ClimbForwardOffset", offset);
    }
    

    // OnStateMove is called right after Animator.OnAnimatorMove(). Code that processes and affects root motion should be implemented here
    //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    //
    //}

    // OnStateIK is called right after Animator.OnAnimatorIK(). Code that sets up animation IK (inverse kinematics) should be implemented here.
    //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    //
    //}
}


スクリプト中で何回か使っている「stateInfo.normalizedTime」ではこのアニメーションの正規化された時間を取得しています。ようは開始時を0、終了時を1としたものです。

今回使っているよじ登りアニメーションでは、対象物に掴まってからしばらくぶら下がっているモーションがあるのですが、そこは短縮してしまいたいので、OnStateUpdate関数の中で、該当モーションの正規化時間0.25~0.55の間は再生速度を100倍にし、時間短縮しています。

また、ぶら下がった後は対象物に乗り上げたいので、正規化時間0.55以降は位置調整用オフセットの値を徐々に増やすようにしています。

よじ登りアニメーションの終了時に呼び出されるOnStateExit関数で、アニメーション再生速度やオフセットの値等をリセットしています。


で、次にプレイヤーを動かすスクリプトです。該当箇所のみ。こんな感じ。

    // --- 初期化 ------------------------------------------------------------------------
    private void Start ()
    {
        anim = GetComponentInChildren<Animator> ();
        rb = GetComponent<Rigidbody> ();
    }


    // --- 更新処理 -----------------------------------------------------------------------
    void Update ()
    {
        switch (_state)
        {
            case eState.Default:
                DefaultMove ();
                break;

            case eState.Climb:
                Climb ();
                break;
        }
    }


    // --- 通常の移動処理(よじ登り以外)------------------------------------------------------------------
    void DefaultMove ()
    {
        // 説明省略
    }


    // --- よじ登り時の処理 ------------------------------------------------------------------
    void Climb ()
    {
        // AnimatorのStateMachineBehaviour("Climb")で変更する値を使って位置調整を行う
        transform.Translate (transform.forward * anim.GetFloat ("ClimbForwardOffset"), Space.World);

        // --- よじ登りアニメーションが終わった時の処理 ----------------------------
        if (anim.GetBool ("Climb") == false)  
        {
            _state = eState.Default;
            rb.isKinematic = false;

            Ray ray = new Ray (transform.position + transform.up * 1f, -transform.up);
            RaycastHit hit;
            if (Physics.Raycast (ray, out hit))
            {
                transform.position = new Vector3 (transform.position.x, hit.point.y, transform.position.z); // 接地位置あわせ
            }
        }
    }


AnimatorのGetFloat()を使って、位置調整用オフセットの値を取得し、transform.Translate()で位置調整しています。
 
なお、今回使っているアニメーションは ↓ このAssetのものです。
以上です。