【Unity(C#)】Animationとパラメータを連動させる方法


デモ

タイトルの内容について書きますが、百聞は一見に如かずということでご覧ください。

膝の角度に合わせて数値及び、青い角度計が変化しています。

これは膝の角度を読み取っているわけではないです。

アニメーション(スクワット)の再生時間に応じてパラメータを変化させています。


2019/06/25 追記
アニメーションはこちらから拾ってきました→Mixamo

GetCurrentAnimatorStateInfo

Animatorを作ると複数のステートを作成できます。

ステートはアニメーションを遷移させるときに便利です。

今回は再生中のアニメーションを調べる必要があるので、
GetCurrentAnimatorStateInfo(0)を使って調べます。


   [SerializeField]
   Animator squatAnimeController;

   void Update()
    {
       AnimatorStateInfo animeStateInfo = squatAnimeController.GetCurrentAnimatorStateInfo(0);
    }

GetCurrentAnimatorStateInfo(0)を使うことで、再生中のアニメーションを取得できます。
引数はアニメーションのLayerですが、複雑なアニメーションを作成してLayerを使用した場合を除いて、
基本的にデフォルトの0で大丈夫です。

AnimatorStateInfo.normalizedTime

normalizedTimeを使用すればアニメーションの再生時間を0~1の値に正規化することができます。

正規化された時間を利用して他のパラメータの値を操作すれば
デモのようにアニメーションに合わせて数値を変化させることができます。

Mathf.Lerp

先程の正規化された時間とMathf.Lerpを利用して数値を連動して変化させます。
Mathf.Lerpの引数は3つ用意されており、Lerp(float a, float b, float t)となっています。

それぞれの引数は
a:開始値
b:終了値
t:補完値(0~1)
となっています。aの値からbの値に向けてtで補完します。
と言われてもいまいちわからないと思います。(私は使ってみるまでさっぱりでした)

なので一例を示します。

まずはコードです。


   [SerializeField]
   Animator squatAnimeController;

   [SerializeField]
   Text degreesText;

   void Update()
    {
       AnimatorStateInfo animeStateInfo = squatAnimeController.GetCurrentAnimatorStateInfo(0);

       float degrees = Mathf.Lerp(180, 0, animeStateInfo.normalizedTime); 
       degreesText.text = ((int)degrees).ToString() + "°";
    }

このように、アニメーションの再生時間(正規化されているので0~1)に合わせて
角度が180~0°で変化しているのがわかるかと思います。(GIFが汚いのはご容赦ください)

これが先程のaの値からbの値に向けてtで補完するという意味です。

a:開始値(180)
b:終了値(0)
t:補完値(正規化したアニメーションの再生時間0~1)

Mathf.Repeat

先程のGIF画像ではステートを二つ用意して、同じアニメーションを交互に遷移させていました。
その理由としてはアニメーションのループ設定がうまくいかなかったからです。

ループ設定してしまうと、アニメーションの正規化がうまくできないようです。
0~1収まるはずの正規化した値が1を超えて無限に増えていきます。(ループに終了時間という概念が存在しないから?)

もし、なんらかの事情でアニメーションのループ設定を外せない場合は、
Mathf.Repeatで正規化した値が1を超えないようにします。

Mathf.Repeat(float t, float length)
指定値(length)を超えると0に戻り、再度 指定値まで循環します。

   [SerializeField]
   Animator squatAnimeController;

   [SerializeField]
   Text degreesText;

   void Update()
    {
       AnimatorStateInfo animeStateInfo = squatAnimeController.GetCurrentAnimatorStateInfo(0);

       float degrees = Mathf.Lerp(180, 0, Mathf.Repeat(animeStateInfo.normalizedTime, 1)); 
       degreesText.text = ((int)degrees).ToString() + "°";
    }

これでループ設定を外せないとしても安心です。

fillAmount

角度計のアニメーションはこちらのサイトを参考にしました。

アニメーションに応じて、FillAmountの値を変化させています。

最終的なコード

当方、マジックナンバーおじさんですが、ご容赦ください。

作成した画像(色付き)にアタッチ
using UnityEngine.UI;
using UnityEngine;

public class AngleVisualController : MonoBehaviour
{
    [SerializeField]
    Animator squatAnimeController;

    AnimatorStateInfo animeStateInfo;

    [SerializeField]
    Text degreesText;

    Image circle_Image;

    void Start()
    {
        circle_Image = this.gameObject.GetComponent<Image>();
    }

    void Update()
    {
        animeStateInfo = squatAnimeController.GetCurrentAnimatorStateInfo(0);

        float squatVertical = 25 / 60.0f;
        float degrees = 0;

        if (animeStateInfo.normalizedTime < squatVertical)
        {
            circle_Image.fillAmount = Mathf.Lerp(0.48f, 0.25f, animeStateInfo.normalizedTime * (60.0f / 25));

            degrees = Mathf.Lerp(170, 90, animeStateInfo.normalizedTime * (60.0f / 25));
            degreesText.text = ((int)degrees).ToString() + "°";
        }
        else
        {
            circle_Image.fillAmount = Mathf.Lerp(0.25f, 0.48f, (animeStateInfo.normalizedTime-(25.0f/60)) * (60.0f / 25));

            degrees = Mathf.Lerp(90, 170, (animeStateInfo.normalizedTime - (25.0f / 60)) * (60.0f / 25));
            degreesText.text = ((int)degrees).ToString() + "°";
        }
    }
}

なぜこんなわけのわからない計算をLerpの中に仕込ませているかというと、
・スクワットのアニメーションの折り返し地点(正規化した値でいうと0.5)がしゃがんだ状態ではない
・角度の値が、直立(170)→しゃがむ(90)→直立(170)を取る
・アニメーションの再生フレームを取得できない
からです。

なので、もっとシンプルに使うには
・しゃがむ、立ち上がる の二つのアニメーションを用意する
なのかな~と思います。そうすれば、変な計算しなくても正規化した値をそのまま使えるかと思います。

本当は、アニメーションの再生フレームを取得して
0~25フレームはこの処理をして、26~60フレームは別の処理を...
みたいなことができれば便利なのですが、見つかりませんでした。(ありましたら教えてください)