[unity2019.LTS]2D のアニメーション(animator) 基礎

アニメーションというと、キャラクターが華麗に走ったり飛んだりするサンプルはあるものの、もっと簡単な…例えばメニューが「開く」「閉じる」といったサンプルをあまり見かけませんでした。

プログラムだけで済ませている方も多いかもしれませんが、unity のアニメーションツールはかなり表現力の高いアニメーションを「簡単に」作れそうです。
デザイナーに任せなくても、プログラマでも楽しんで作れる、そんなイメージを受けました。
(動画のアニメーションは作成に1時間もかかりませんでした)

なお、unity のアニメーションに触れるのは初めてなので、基本的な使い方が間違っていたらゴメンナサイ。

GUIでアニメーション作成まではよかったのですが、それをスクリプトから呼び出す部分は取っつきづらかった。
メニューのオンオフ程度なので複雑なことはしない、と決めても若干コーディングで悩む感じ。
まさか animator から、登録した Animation の全リストさえ(実行時に)取得することができないとは…。

動画の作成環境

2019.4.6f1 で確認しました。animator 自体は unity5 のあたりから左程変わってないようなので、それ以前でも使えるかもしれません。

使い方

実行し、Z を押すとフレームイン、X を押すとフレームアウトします。
Test.cs が実行部分です。

2つのアニメーションを組み合わせる

動画を見てもらうとわかるのですが、今回のアニメーションは

文字表示エリア(フレーム)
Hello world

この2つが続けて表示されます。
この2つを合わせて1アニメーションにする事もできますが、ゲームで使う場合、フレームだけ共通で他の場所に使うこともあるでしょう。そうした実践も踏まえて今回は2つに分けました。

フレーム(Frame):Image

フレームに Animator をアタッチ、Animator Controller と、Animation を2つ作成しました。
オレンジの Frame_Init は「表示を消すだけ」のアニメーションです。
これによって起動時にフレームの表示が消えます。

Frame_OpenClose は画面の通りで、注意点はオレンジの①② speed
アニメーション順再生 / 逆再生、と Animation 2 つで管理するのが一般的のようですが、それだと途中でアニメーションを修正した時 2 つ直す必要があり、面倒なので 1 つで済ませるための準備です。

文字(Helloworld):TextMeshProUGUI

文字(Helloworld)もほぼ構成は同じです。こちらの方がカラーのα値アニメーションもあるので、キーフレームがちょっと多めです。

Animation の Curves で増減を曲線的に変えられるのは本当に便利ですね!
自前スクリプトで同じことをしようとすると、数多のツール・ライブラリを自作する…想像するだけで気が遠くなります。

アニメーションの再生

単純な再生

Animator animator = GetComponent<Animator>();
// Animation Name, Layer, 正規化フレーム
animator.Play("Frame_OpenClose", 0, 0);

レイヤーは Base Layer しかないので、0 固定です。
正規化フレームは 0 が開始位置、1 が終了位置になります。
例えば 0.5f にした場合、アニメーションは 50% の地点から始まります。

逆再生

Animator animator = GetComponent<Animator>();

//順再生
animator.SetFloat("speed", 1);
animator.Play("Frame_OpenClose", 0, 0);

//逆再生
animator.SetFloat("speed", -1);
animator.Play("Frame_OpenClose", 0, 1);

オレンジの①② speed を覚えているでしょうか。あの設定はここで活用されます。
1 なら順再生、-1なら逆再生です。毎度設定しなおさないと、前の状態が記憶されているので注意してください。
なお、animator.Play() の第 3 引数も注目です。逆再生の場合、終了位置からアニメーションを再生する必要があるので、1 を設定する必要があります。

直接 Speed をプログラムで触れる方法があればこんな回りくどいことにはならないんですが…その方法を探すことはできませんでした😢

animator.speed というメンバは 0 より小さい、マイナス値を設定することができません。

アニメーション再生待ち

こちらもかなり面倒です。IsPlaying みたいな確認手段がありませんので、次のようなコードで代替します。

while (true)
{
    // 1フレーム目はアニメーション処理が始まってない(しかもその情報すら取れない)
    yield return null;
    
    // 再生中は 0 ~ 1 の範囲内のはず
    float time = animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
    if (time < 0 || time > 1)
    {
        break;
    }
}

特筆すべき問題は yield return null の位置で、これを break の後に置くだけで上手く動作しなくなってしまいます。
animator は Play した段階では normalizedTime (とか、他の構造体メンバ)を明示的にクリアせず、以前再生した値がそのまま残っているため、yield return null で1回空ループしないと、まともな値が取得できないのです…。

これはさすがに設計ミスといってもおかしくない所作のように思えますが、わたしの使い方に問題のある可能性も否定はできないので、モヤモヤしつつ、前述の位置に yield return null を置くことで回避しました。

また、normalizedTime はアニメーション再生が終わっても内部でじゃかじゃか数字を増減しているようでした。明示的に Stop() を呼ばないといけないのか。え、Stop() もない? そんなバカな。

Animator ヘルパ関数

以上の不可思議? な挙動をなんとなく吸収して、使いやすくしたヘルパクラスを置いておきます。
「作成環境」でも同じものを使っています。

CoroutineAccessor は内部で使っています。詳しくはこちらの記事をご覧ください。

使い方

AnimatorEx animEx = new AnimatorEx(animator);

// layer は 0 固定です。可変にしたい場合 AnimatorEx を修正する必要あり
animEx.Play("AnimationName");

// アニメーション再生中は true
while (animEx.CheckIsPlaying())
{
    yield return null;
}

// 逆再生。Animator に speed パラメータを追加し、Animation の Multiplier に登録しておくこと
animEx.PlayRev("AnimationName");

// 現在再生中のフレームから開始して逆再生(既に逆再生が終わってたら何もしない)
animEx.ContinuePlayRev("AnimationName");

参考記事