この記事は Unity Advent Calender 2019 17日目の記事です。
Unity には様々なアニメーション再生の方法が用意されていて、大きく分けて2Dでのみ使えるものと3Dのみで使えるもの、どちらでも使えるものがあります。今回は2Dで使えるものを見ていきますが、2Dの中にもパラパラ漫画のように次々と Sprite を切り替えるスプライトアニメーションと、2D Animation によるボーンベースのアニメーションなどがあります。
この記事では同じ条件で遷移するスプライトアニメーションを複数の再生方法で再現し、それぞれの方法の比較も行います。再生に用いるのは AnimatorController、SimpleAnimation、PlayableGraph です。後互換性のために残されているものの非推奨となっている Animation コンポーネントや、今回はインゲームで操作する場合を想定したいので内容的に向いていないと思われる Timeline は対象外としています。
SimpleAnimation、Playables API についての後編はこちらです。
また、作成したプロジェクトはこちらです。
https://github.com/gok11/2DAnimationSample2019
再生するアニメーション
今回再生するアニメーションは次のようなものです。弾が出ていないのでわかりにくいですが、時折射撃モーションが挟まっています。
今回のアニメーションの要件は次の通りです。
- 待機、走り、ジャンプモーションがある
- それぞれのモーションには対応する射撃モーションがあり、攻撃ボタンを押されてから少しの間は射撃用のアニメーション群が優先される
- 待機、走りは相互に遷移可能。それぞれからジャンプへは遷移可能
- ジャンプは 上昇開始→上昇中→下降開始→下降中→着地 という流れで遷移し、着地後までは他のアニメーションへの遷移を行わない
射撃レイヤーによる上書きという要素がある分、2Dの中ではそれなりに複雑な部類になると思います。
本当はジャンプモーションについては上昇→落下だけでなく、落とし穴に落ちた場合などいきなり落下した時はジャンプ後半の落下以降だけが必要など他に考えるべきパターンがあると思いますが、今回はサンプルと割り切って前述以外の遷移は考えないことにしました。
使用するキャラクターコントローラー
キャラクターコントローラーはキャラクター操作と同時にアニメーションに関連するフラグのプロパティを更新します。フラグに関連する箇所を抜粋してみます。
using UnityEngine;
public class UnityChanController : MonoBehaviour
{
~~ 中略 ~~
public float HorAbsInput => Mathf.Abs(_horInput);
public float VerSpeed => _rigidbody2D.velocity.y;
public bool JumpTriggered { get; private set; }
public bool GroundedTriggered { get; private set; }
public bool FireTriggered { get; private set; }
~~ 中略 ~~
void Update()
{
_horInput = Input.GetAxisRaw("Horizontal");
JumpTriggered = _isGrounded && Input.GetButtonDown("Jump");
FireTriggered = Input.GetButtonDown("Fire1");
~~ 中略 ~~
void OnCollisionEnter2D()
{
_isGrounded = true;
GroundedTriggered = true;
}
// アニメーション終了後にフラグリセットする用
public void ResetTriggers()
{
GroundedTriggered = false;
JumpTriggered = false;
FireTriggered = false;
}
}
OnCollisionEnter よりレイなどを飛ばした方が着地判定安定しそうなど、ツッコミどころはありますがサンプルなのでこちらも勘弁してください。
とにかく、アニメーションに関係しそうなパラメータは公開しているので、実際にアニメーション管理するコンポーネントはそちらを読み取ってよしなにアニメーションを更新し、読み取ったあとは ResetTriggers を呼び出す方針にしています。
AnimationClipについて
今回の記事ではアニメーションを再生する側について主にまとめますが、 AnimationClip は再生される側の素材にあたります。今回紹介する全ての再生方法で用いられるため、 AnimationClip についても基本的なことをまとめておきたいと思います。
AnimationClip はアセットの一種で、Projectブラウザで 右クリック > Create > Animation から作成することができます。
また、 メニューの Window > Animation > Animation から Animation ウィンドウを開くことができます。 Animation ウィンドウでは Project ブラウザで選択している AnimationClip を編集できます。
アニメーションの内容はこのウィンドウ内に並んでいるキーフレームという菱形に記録されます。この1つ1つに対してある時点でオブジェクトがどのような状態になっていて欲しいかという情報がフレーム単位で設定されています。
キーフレームで嬉しいのは補間が効く点です。オブジェクトの位置などの情報もキーフレームとして記録できますが、あるオブジェクトのX座標を更新したいとして、0フレーム目に 0、100フレーム目に 5 という位置を指定するためのキーを打ったとします。1~99フレームの間に全てのフレームで位置を設定しないと滑らかに動かない、ということはなく補間の設定にもよりますが50フレーム目では2.5などの値へと自動的に補間してくれます。
しかし今回使用するのは画像のパラパラアニメーションであるため、このような自動的な補間は効きません。そのため、あるフレームでどうなっていて欲しいかというキーを1つ1つ指定してやる必要があります。
幸い Sprite のキーの設定は一括で行うことができます。アニメーションさせたいテクスチャの子にあるスプライトを Project ブラウザでまとめて選択し、 Animation ウィンドウにD&Dするだけで1フレーム間隔で並べてくれます。
なお、 スプライトアニメーションにおいてはこの補間が効かないというのが見た目に何かと影響するので覚えておいた方が良いです。
アニメーション速度の変更
以前は AnimationClip の再生速度は Samples
という値から変更できました。それがいつからか Animation ウィンドウから項目が消え、編集できなくなっています。また、 AnimationClip を選択すると FPS という項目がありこれがアニメーションの再生速度に直結しているのですが編集はできません。
しかしこれはインスペクターを Debug モードにすることで編集できます。Sample Rate は1秒間に何フレーム再生するか、という値なので低く設定するほどアニメーション速度はゆっくりになります。
なお、この方法には2つ注意点があります。
まず、アニメーション速度は各再生側でも指定できます。 Samples の項目が消えたことから、中の人的には再生側で調整してほしいのかもしれません。また、この方法はまだクリップが空の間に行うことをオススメします。既にキーフレームが存在するクリップを編集すると、キーフレームの情報が一部欠落して正しいアニメーションが再生できなくなることがあったためです。
AnimatorControllerによるアニメーション
AnimatorController でのアニメーション管理は比較的古くからある方法です。AnimatorController にはデフォルトで遷移時にモーション同士をブレンドして違和感をなくしてくれたり、遷移時にアニメーションだけでなくスクリプトを実行させられるなどの便利な機能があります。
その仕組み上、アニメーションの遷移周りを GUI から確認できるので(大規模なアニメーションでなければ)比較的理解しやすく入門しやすいと思います。
なお AnimatorController はアセットの種類のことで、これを Animator コンポーネントに設定することで Animator がその設定に沿ったアニメーション管理を行う、という関係になっています。
Spriteでのお手軽な初期設定方法
スプライトアニメーション用の AnimatorController の下準備は次のように行います。
- SpriteRenderer を持つゲームオブジェクトを用意
- Animator コンポーネントをアタッチ
- AnimatorController を作成し、Animator コンポーネントに設定
- AnimatorController 内にスプライトアニメーションを行う AnimationClip を配置する
これらの手順は実は Project ウィンドウで Sprite を複数選択し、ヒエラルキーかシーンビューにD&Dして作成される AnimationClip の名前を決めるだけで完了できます。
ステートを設定する
AnimatorController では GUI からステートマシンを構築する必要があります。今回の場合、移動状態の時に再生する移動アニメーションはどれか、ジャンプ状態にはどのような条件で遷移するか、といった情報を予め用意しておく必要がある、ということになります。
まずはステートを作成します。先ほど作成したゲームオブジェクトを選択し、 Animator コンポーネントにある AnimatorController をダブルクリックすると、 Animator ウィンドウが開きます。 Window > Animator から開くこともできます。
画像にあるオレンジ色の矩形が最初に再生されるデフォルトアニメーションです。灰色の矩形がアニメーションの遷移先候補です。次は灰色の矩形を増やしてみます。
先ほど作成した Animator コンポーネントがアタッチされたオブジェクトを選択した状態で Animation ウィンドウを開き、デフォルトアニメーション名が表示されている箇所をクリックすると Create New Clip... という項目が選択肢に現れます。
こちらを選択すると新しい AnimationClip を作成でき、以降はドロップダウンメニューからも選択できます。そして、そのクリップは自動的に Animator 一覧に灰色の矩形として追加されます。追加したあとは先ほど AnimationClip の項で書いていた手順の通り再生したい Sprite 一覧を登録してやります。
また、既に存在する AnimationClip を Animator ウィンドウに D&D でも同様に追加できます。
最初に再生されるアニメーションを変更したい場合は、該当のステートを右クリックして Set as Default State
を選択します。
ジャンプのアニメーション登録
ジャンプなどの遷移が複雑なアニメーションはステートの登録にも注意が必要な場合があります。今回の場合、ジャンプアニメーションは次のような5種類に分けることができます。
このうち「上昇中」はY軸の速度が0以下に変わるまでは、「下降中」は地面に着地するまではループ再生したいのでその部分のアニメーションは分離していなければいけません。そのことに考慮するとステートも次のように分割されます。
遷移を設定する
ステートを全て登録できたら次はステート間で遷移できるようにします。ステートを右クリックして Make Transition を選択し、他のステートをクリックすると遷移先候補として設定できます。
一通り遷移を設定すると次のようになります。
相互に矢印が伸びているのが待機と走りステートで、一方通行なのがジャンプです。今回の場合、ジャンプは途中から再生されることはないという前提でステートが設定されています。最初の方でも書きましたが、実際のゲームでは落とし穴等のいきなり下降から始まる場合もあると思うのでそれは要設定、となります。
ちなみに、左上に Any State というステートがありますが死亡アニメーションなど、どのステートからでも遷移する可能性があるものはこちらからの遷移を設定してやります。
複数のステートをまとめる
AnimatorController にはサブステートマシンというステートをまとめる機能があります。 Animator ウィンドウで右クリックし、 Create Sub-State Machine を選択するとサブステートマシンを作成できます。
今回の場合、ジャンプ周りは数が多いですが遷移が一方通行であるためまとめられます。それぞれのステートを Ctrl(Cmd)を押しながら複数選択し、作成したサブステートマシンに D&D します。
そうすると対象のステートは全てサブステートマシンに含まれ自動的に以前の遷移の状態を考慮した遷移を構築してくれます。また、サブステートマシンをダブルクリックすることで内部に含まれているステートの状態を確認・編集することができます。
遷移条件を設定する
現時点で遷移先は指定できているのですが、遷移条件は初期設定なのでこのままでは「アニメーションが1周したら自動的にランダムな候補に遷移する」となります。次は遷移条件を設定して入力に応じた遷移を行えるようにします。
まずは条件の元となるパラメータを用意します。 Animator ウィンドウ左上の Parameters タブを選択し、 + ボタンからパラメータの型を選択します。
作成したパラメータは名前をダブルクリックすることで編集できます。名前はスクリプトから操作する際に重要なので、タイポには気をつけましょう。今回は次のようなパラメータを用意しました。
Float で現在の横方向の入力と縦方向の速度、Bool でジャンプボタンが押されたかと着地したかを表すものを作りました。パラメータには Trigger という遷移条件があり、ジャンプボタンが押された瞬間などはそちらも使用できるのですが、取り回しの関係で今回は Bool を使っています。これらの値は後ほどスクリプトから入力されます。
次に各パラメータを遷移の条件として設定します。遷移用の線をクリックするとインスペクターに遷移に関する情報が表示されます。
遷移条件に関して重要な項目は、 Has Exit Time
と Conditions
です。 Has Exit Time は Settings にある Exit Time
分だけアニメーションを再生したら遷移する、という条件です。 Conditions は先ほど作成したパラメータを用いて遷移時の条件を設定できます。
ジャンプ開始→上昇中など、一連のアニメーションとして再生したい場合は Conditions は空にして Has Exit Time を有効にしたままにしておきます。 Exit Time は秒数ではなく、クリップ1周を 1 とした割合です。アニメーションを1周再生させたい場合は 1 としておきます。
Conditions は先ほどのパラメータを用いますが、その型に応じて遷移条件を設定できます。例えば待機状態から移動状態に移る時は HorAbsInput という値を使用し、これには横軸の入力に応じて 0 か 1 が入ります。少しでも入力されたら移動アニメーションを再生したいので、こちらは Greater 0 を条件とします。
逆に歩きから待機状態に戻る時は入力がされなくなったら戻すので、 Less 0.05 とします。
同様に、 Bool 型の時は True/False の時は遷移する、という条件がつけられます。これらの条件をそれぞれの遷移条件として設定します。
スクリプトからアニメーションさせる
条件が設定できたらキャラクターの動きに合わせてアニメーションできるようにスクリプトを書きます。
まずはスクリプト全文はこんな感じです。
using UnityEngine;
public class AnimatorControllerAnimator : MonoBehaviour
{
private Animator _animator;
private UnityChanController _controller;
private readonly int _groundedTrigger = Animator.StringToHash("GroundedTrigger");
private readonly int _jumpTrigger = Animator.StringToHash("JumpTrigger");
private readonly int _horAbsInput = Animator.StringToHash("HorAbsInput");
private readonly int _verSpeed = Animator.StringToHash("VerSpeed");
void Awake()
{
_animator = GetComponent<Animator>();
_controller = GetComponent<UnityChanController>();
}
void Update()
{
_animator.SetBool(_groundedTrigger, _controller.GroundedTriggered);
_animator.SetBool(_jumpTrigger, _controller.JumpTriggered);
_animator.SetFloat(_horAbsInput, _controller.HorAbsInput);
_animator.SetFloat(_verSpeed, _controller.VerSpeed);
_controller.ResetTriggers();
}
}
_animator.SetBool
などで Animator に対して UnityChanController が持っているアニメーション関係のフラグを渡しています。それぞれの第一引数ではパラメータの名前、例えば "HorAbsInput"
などを指定することもできるのですが、 Animator.StringToHash
で予め数値に変換してから指定した方が安いのでそうしています。
即座に遷移させる
この時点で銃を撃つ差分以外のアニメーションは一通り操作に応じて再生されるのですが、入力した瞬間にサッと遷移してくれないことがあります。
この原因の1つは Has Exit Time です。 Conditions で遷移条件を有効にした場合でも、これが有効な場合は Exit Time
分だけアニメーションを再生したら遷移するという条件も同時に満たさなくてはいけなくなります。要するにサッと遷移させたい場合はこれを無効にしておく必要があります。
また、Settings には Transition Duration という項目があります。 Has Exit Time は遷移するための条件でしたが、こちらは遷移をどのくらいかけて行うか、という設定です。0.25 であれば遷移前のアニメーションの 25% ほどの長さをかけて遷移前と遷移後のアニメーションをブレンドしながら徐々に入れ替えていくのですが、スプライトアニメーションにおいてはそういった補間は効かないのでこれは 0 にしておくべきです。
射撃レイヤーを用意する
最後に射撃モーションに対応します。
Animator にはレイヤーという概念があります。これは例えば、3Dモデルでアニメーションをさせる時に上半身だけ独立してアニメーションさせたりする場合に使えます。2Dでも今回のようにアニメーションを一定時間上書きするような表現を行う場合に使えます。
まず Animator ウィンドウ左上の Layers タブを選んで、 + ボタンからレイヤーを追加します。名前は Shoot Layer としました。
追加したレイヤーの歯車ボタンを押して、出てきたパネルの Sync ボタンを押します。これは他のレイヤーのステート構造をそのままコピーしてそのレイヤーに再現するボタンです。
また、同パネルにある Weight は重要なパラメータです。Weight は優先度とも言い換えることができると思いますが、今回の場合 Shoot Layer の Weight が Base Layer の Weight と同等である 1 の時だけ Base Layer の現在のアニメーション、再生時間を引き継いでアニメーションを再生することができます。
これで射撃モーション用の AnimationClip を用意し、 Shoot Layer の各ステートに割り当ればアニメーション上書きの準備は整います。あとはスクリプトから Weight の調節をすれば完了です。スクリプトの変更周りを抜粋します。
using UnityEngine;
public class AnimatorControllerAnimator : MonoBehaviour
{
~~ 中略 ~~
private float _shootAnimationTime = 0.5f;
private float _shootTimer;
void Awake()
{
_animator = GetComponent<Animator>();
_controller = GetComponent<UnityChanController>();
_shootTimer = _shootAnimationTime;
}
void Update()
{
~~ 中略 ~~
if (_controller.FireTriggered) _shootTimer = 0;
if (_shootTimer < _shootAnimationTime)
{
_shootTimer += Time.deltaTime;
var weight = _shootTimer >= _shootAnimationTime ? 0 : 1;
_animator.SetLayerWeight(1, weight);
}
_controller.ResetTriggers();
}
}
_animator.SetLayerWeight
で Weight を設定しています。
SimpleAnimation、Playables API についての後編はこちらです。