前編はこちら。
前編はスプライトアニメーションの基本、AnimationClip、AnimatorController でのアニメーション管理についてでした。後編は SimpleAnimation、Playables API についてです。
SimpleAnimationについて
SimpleAnimationとは、レガシー扱いとなった Animation コンポーネントを後述する Playables API によって再現したものです。Animation コンポーネントについてはこちら。
前編で紹介した AnimatorController による管理は遷移条件などが視覚化されておりとっつきやすいのですが、一方である程度の規模になると整理を意識的にしないとグラフがごちゃごちゃするために分かりづらくなるという欠点があります。
また、スプライトアニメーションに関しては AnimatorController でデフォルトで発生するアニメーションの補間の意味がなかったり、機能が多すぎる印象も受けるかもしれません。
一方 SimpleAnimation の場合、アニメーションの再生は簡単な登録をした後に SimpleAnimation.Play ("再生したいアニメーション名") のように呼び出すだけでよく、名前の通りとてもシンプルです。
SimpleAnimationでのアニメーション再生まで
SimpleAnimation をインポート
SimpleAnimation は Animator コンポーネントなどと違い、 Unity で最初から使える機能ではありません。また、パッケージでもないので Package Manager からインストールすることもできません。
使用するには GitHub で公開されているアセットをダウンロード、インポートします。次のページの使用しているバージョンに応じた zip ファイルをダウンロードし、プロジェクトに追加してください。
https://github.com/Unity-Technologies/SimpleAnimation/releases
インポートができたら、アニメーションさせたいオブジェクトに SimpleAnimation コンポーネントを追加します。 Animator コンポーネントも内部的に使用されるため、自動的に追加されます。
AnimationClip の用意と登録
本来であればこの時点で使用する AnimationClip を用意する必要があります。今回は前編で作成したものを使います。また、使用する AnimationClip はステート名とともに SimpleAnimation コンポーネントに登録しておきます。
アニメーションの呼び出し
あとはアニメーションを呼び出すだけです。今回の場合、 SimpleAnimation.Play("Run") で移動アニメーションを再生できます。また、アニメーションを滑らかに遷移させるのであれば SimpleAnimation.CrossFade でクロスフェードさせることもできます。
ただし、今回は2Dスプライトアニメーションであり遷移時に補間は必要ないというか効かないので、基本的には SimpleAnimation.Play を必要なタイミングで呼び出すことで遷移していくことになります。
再生周りの主な関数は次の通りです。
関数名 | 内容 |
---|---|
Play | 再生するステートを指定。呼び出したらすぐに再生される |
PlayQueued | 現在再生中のステートが1周したら再生されるキューにアニメーションを追加 |
CrossFade | 現在再生中のアニメーションを別のアニメーションに徐々に入れ替える |
Blend | 現在のアニメーションに指定したアニメーションをブレンドして再生する |
AddState | Playなどの再生時に指定するステートを追加する。クリップとステート名のセットで指定 |
GetState | 特定のステートを取得する。ステートからは詳細な情報を取得したり、 Weight などを変更できる |
Stop | 引数なしで再生中のもの全てを、ステートを指定すると特定のステートを停止する |
CrossFade と Blend の違いですが、 CrossFade はアニメーション A と B を入れ替えて再生するためのものであるのに対し、 Blend はアニメーション A を再生しているところに B を混ぜた動きをさせたい場合などに使います(前述の通り今回は2Dであるため CrossFade は使いませんが)。
再現してみる
ステート管理と基本的な再生
以上が基本的な情報です。では次に、 AnimatorController で実現していたアニメーションを再現してみます。
前提としてキャラクターの操作には前回作った UnityChanController を使うのですが、アニメーションの再生管理には Simple Finite State Machine をお借りしました。スプライトアニメーション自体には関係ないのでこちらは軽く紹介するのですが、次のように初期化するだけで最初に定義した Enum を元にステートマシンを利用できるアセットです。
// アニメーションで想定している状態を表すEnum
private enum AnimState
{
Stand, Jump, Down, Grounded, Run
}
// 上記Enumを元にステートマシンを作成
private StateMachine<AnimState> _animationFsm;
void Awake()
{
// ステートマシンの初期化
_animationFsm = StateMachine<AnimState>.Initialize(this);
}
void Start()
{
// 最初はStand 状態にしておく
_animationFsm.ChangeState(AnimState.Stand);
}
_animationFsm.ChangeState
を呼んだ時点でステートマシンの状態が更新され、初期化に用いた Enum の名前がついたコールバックが自動的に呼ばれるようになります。
例えば、上記の ChangeState(AnimState.Stand) 直後には Stand_Enter が一度だけ呼ばれ、以降ステートが更新されない限りは Update の度に Stand_Update が呼ばれる、 Run に ChangeState した直後には Run_Enter が呼ばれる、といった具合です。詳しくは上記のページを確認してください。
あとはそのコールバックの中にアニメーションの再生内容を書きます。例えば待機モーション周りは次のような感じです。
// 待機ステートになった瞬間待機アニメーションを再生
void Stand_Enter()
{
_simpleAnimation.Play(StandStateName);
}
void Stand_Update()
{
// ジャンプボタン押下でジャンプステートに変更
if (_controller.JumpTriggered)
{
_animationFsm.ChangeState(AnimState.Jump);
return;
}
// 左右入力に従って走りステートに移行
if (_controller.HorAbsInput != 0)
{
_animationFsm.ChangeState(AnimState.Run);
return;
}
}
基本的にはこのように各ステートへ移行した瞬間対応したアニメーションを再生、ステート中に条件を満たしたら対応するステートに移行、という流れでアニメーションを管理する方針としました。
これで、待機や移動といったシンプルなループアニメーションには対応できました。 AnimatorController でいうとこの部分です。
自動的に遷移するアニメーション
次にジャンプ内、アニメーションが一周したら自動的に遷移する部分と、条件を満たすまではループする部分が混在する部分を再現します。AnimatorController でいうとこの部分です。
まずはこう書けたらよかった、という書き方です。
void Jump_Enter()
{
// ジャンプ開始アニメーション再生
_simpleAnimation.Play(JumpStartStateName);
// 上のアニメーションが終わったら上昇中のアニメーション再生
_simpleAnimation.PlayQueued(JumpStateName);
}
void Jump_Update()
{
// 縦方向の速度が0を下回ったら下降ステートへ
if (_controller.VerSpeed >= 0) return;
_animationFsm.ChangeState(AnimState.Down);
}
コメントにある通り、
- ジャンプ開始
- そのモーションが一周したら上昇中のループ再生
- 下降条件を満たしたら下降状態へ遷移
という流れが想定されています。ただし、SimpleAnimation にはバグがあるらしく現状この書き方はできませんでした。具体的には PlayQueued で入力したアニメーションのキューが消されないのか、使っているアニメーションが極端に短いものが含まれるのが悪いのか、とにかく意図しないループをしてしまっていました。
時間を取れたら検証とともに修正したいのですが、とりあえず動く書き方をします。
void Jump_Enter()
{
_simpleAnimation.Play(JumpStartStateName);
animationTimer = 0f;
}
void Jump_Update()
{
// ジャンプ開始アニメーションの長さを取り、
// その長さだけ待機したら上昇アニメーション再生
animationTimer += Time.deltaTime;
var length = _simpleAnimation.GetState(JumpStartStateName).length;
if (animationTimer >= length)
_simpleAnimation.Play(JumpStateName);
if (_controller.VerSpeed >= 0) return;
_animationFsm.ChangeState(AnimState.Down);
}
GetState().length
でクリップの長さを取れるのでその時間だけ待機して次のアニメーションを再生しました。これで PlayQueued は再現できます。
射撃アニメーションとの切り替え
射撃アニメーションとの切り替えは AnimatorController ではレイヤーを用いていました。しかし SimpleAnimation には Wiki に SimpleAnimation Component doesn’t support Layers
という項目がある通り、レイヤーが存在していません。
https://github.com/Unity-Technologies/SimpleAnimation/wiki
しかし Blend を使えば同じようなことができます。関連する箇所を抜粋してみます。
private float _shootTimer = -1f;
private float _shootTime = 0.5f;
// 射撃アニメーション用の Weight
private float ShootWeight => _shootTimer >= 0f && _shootTimer <= _shootTime ? 1f : 0f;
~~ 中略 ~~
void Update()
{
// 射撃レイヤー管理用のタイマーを動かす
if (_controller.FireTriggered)
_shootTimer = 0f;
if (!(_shootTimer >= 0f)) return;
_shootTimer += Time.deltaTime;
if (_shootTimer >= _shootTime)
_shootTimer = -1f;
}
~~ 中略 ~~
void Run_Enter()
{
// 2つのアニメーションを再生し、 Weight に応じて再生状態を変える
_simpleAnimation.Play(RunStateName);
_simpleAnimation.GetState(RunStateName).weight = 1 - ShootWeight;
_simpleAnimation.Blend(RunShootStateName, ShootWeight, 0f);
}
void Run_Update()
{
_simpleAnimation.GetState(RunStateName).weight = 1 - ShootWeight;
_simpleAnimation.GetState(RunShootStateName).weight = ShootWeight;
Play と Blend で2つのアニメーションを再生しておき、 GetState でそれぞれの Weight を変更しています。
これで SimpleAnimation での再現はできます。いくつかバグがあり、今回の用途では若干ハマりどころもありましたが AnimatorController と比べてシンプルに再生でき総じて良い感じだと思います。
Playables API について
次は Playables API についてです。 Playables API はUnity2017.1 から正式リリースされている機能で、これまでに見てきた Animator Controller や SimpleAnimation の内部で使われています。
AnimatorControllerの内部を理解する
まずは内部的にどのようなことが起こっているかを確認してみましょう。 Package Manager を開き、 Advanced から Show preview packages を選択した上で PlayableGraph Visualizer をインストールしてください。
インストールが完了したら Window > PlayableGraph Visualizer からウィンドウを開きます。すると、シーン中に Animator がある場合、自動的にその Animator に設定されている AnimatorController のツリー構造を読み取った図を表示してくれます。SimpleAnimation や Playables API を使う場合、ツリー構造はランタイムで構築されるため、シーン再生中のみ表示されます。
次の画像は最初に作成した AnimatorController のツリー構造です。
一見ごちゃっとしていて理解しづらそうですが、構成要素はほぼ共通しているので複雑ではありません。理解を深めるため、まずはできる限りシンプルなグラフを構築してみます。
ノードが2つだけになりました。右のものがアニメーションの情報を左にある PlayableOutput に渡す AnimationClipPlayable で、PlayableOutput はアニメーションの情報を Animator に出力して実際に動かすためのものです。このように、情報を入力する Playable とそれを受け取る PlayableOutput はアニメーションには必須です。
また、このウィンドウに表示されているアニメーションを再生するためのグラフ構造を PlayableGraph と呼び、このウィンドウにおいてグラフは右から左に向けて評価されていることも覚えておきましょう。
この PlayableGraph を作っているスクリプトは次の通りです。AnimationClip を再生する Playable とそれを受け取る PlayableOutput を繋ぎ込み、 PlayableGraph に再生を指示する簡単なシンプルなものです。
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;
[RequireComponent(typeof(Animator))]
public class SuperSimpleSample : MonoBehaviour {
private PlayableGraph _playableGraph;
[SerializeField] private AnimationClip animationClip;
void Awake()
{
_playableGraph = PlayableGraph.Create ();
}
void Start()
{
var clipPlayable = AnimationClipPlayable.Create (_playableGraph, animationClip);
var output = AnimationPlayableOutput.Create (_playableGraph, "output", GetComponent<Animator>());
output.SetSourcePlayable (clipPlayable);
_playableGraph.Play ();
}
void OnDestroy()
{
_playableGraph.Destroy ();
}
}
次に、 AnimationMixerPlayable を使い複数のアニメーションを切り替えられるようにします。これは複数の AnimationClip の入力を受け取り、設定された Weight に応じて後ろに流す内容を変えたりブレンドする Playable です。
AnimationMixer ノードが2つの AnimationClip と Output の間に入り、 Weight を変更するとアニメーションが切り替わるようにしました。ノード間の曲線が白に近いほど Weight が1に近く優先されます。
このサンプルのスクリプトは次の通りです。先ほどのものを修正していますが、主に変更した辺りにコメントを入れています。
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;
[RequireComponent(typeof(Animator))]
public class SuperSimpleSample : MonoBehaviour {
private PlayableGraph _playableGraph;
private AnimationMixerPlayable _mixerPlayable;
[SerializeField] private AnimationClip animationClip1, animationClip2;
public float Weight { get; set; }
void Awake()
{
_playableGraph = PlayableGraph.Create ();
}
void Start()
{
var clipPlayable1 = AnimationClipPlayable.Create (_playableGraph, animationClip1);
var clipPlayable2 = AnimationClipPlayable.Create (_playableGraph, animationClip2);
// ミキサーを作成
// ミキサーには2つのクリップを接続
_mixerPlayable = AnimationMixerPlayable.Create (_playableGraph, 2, true);
_mixerPlayable.ConnectInput (0, clipPlayable1, 0);
_mixerPlayable.ConnectInput (1, clipPlayable2, 0);
var output = AnimationPlayableOutput.Create (_playableGraph, "output", GetComponent<Animator>());
output.SetSourcePlayable (_mixerPlayable);
_playableGraph.Play ();
}
void Update()
{
// Weight に応じてミキサーの設定を変える
_mixerPlayable.SetInputWeight (0, Weight);
_mixerPlayable.SetInputWeight (1, 1 - Weight);
}
void OnDestroy()
{
_playableGraph.Destroy ();
}
}
AnimationClipPlayable と AnimationMixerPlayable を知ると先ほどの画像はだいたい理解できます。まず右上のあたりに注目してみます。 PlayableGraph Visualizer では Playable を選択するとその詳細が確認できます。右上の AnimationClip には待機モーション(UniStand)が Clip として設定されており、現在はそれが再生されていることがわかります。
その2つ下にある AnimationClip には Clip が設定されていませんが、横キーなどを入力してアニメーションが切り替わるタイミングになると動的に中身が入れ替わり画像左にある AnimationMixer からの線はそちらにつながります。
右下も同様の構造ですが、そちらの右上には待機状態での射撃モーション(UniStandShoot)が設定されています。それらのさらに左を見ると AnimationLayerMixer があることからも右上のあたりは通常アニメーション用のレイヤー、右下は射撃用のレイヤーを表していることがわかります。ちなみに AnimationLayerMixer は名前の通り AnimationMixer のレイヤー版です。
そうしたミキサーを経由して AnimatorController 特有の AnimatorControllerPlayable を通り PlayableOutput に渡されるのが今回作成した AnimatorController の全容です。
なお、ここまで全く触れていない AnimationPose ですが…これはよくわかりませんでした。ミキサーの対象になっていることから処理には影響していると思われるのですが Forum では存在について回答がついておらず、Visualizer のソースコードを読んでもそれらしきものについての記述はパッと見つかりませんでした。説明がどこかでなされるか、こちらから関与しようがないものなら表示から除外できるようにするなどしてほしいですが、理解しなくても問題ない類のものと思われるのでとりあえず気にしないことにしておきます。
ちなみに、レイヤーが存在しない SimpleAnimation の PlayableGraph はもっとわかりやすいです。
動的に入れ替えるのではなく、初めから使用する AnimationClipPlayable は全て作成した上でミキサーに接続しておき、Play のタイミングで再生中のものの停止と新規再生、 Weight の設定を行っているようです。さすがシンプル。
独自のアニメーション再生コンポーネントを作成する
だいたいの仕組みが理解できたところで独自のアニメーション管理の仕組みを作ってみます。 SimpleAnimation を使ってみて思ったことは次のようなことでした。
- CrossFade などは必要ない
- PlayQueued は動作してほしい
- 通常アニメーションと射撃用のレイヤーは簡単に切り替えたい
基本的な機能は抑えつつ、これらの点をどうにかしてみます。最終的な実装の結果はこちらにあります。
基本的な再生機能
再生機能の方針をまず決めます。つまり PlayableGraph で AnimationClipPlayable を SimpleAnimation のように最初から並べておくか、AnimatorController のように Clip を空けておいて動的に入力するかという話です。
今回は方針は使用感は SimpleAnimation に寄せつつ、内部的には AnimatorController に近いテラシュールブログさまの記事の実装を参考にします。
- AnimationMixer でアニメーションの切り替えを行う
- AnimationClipPlayable は2つ用意しておき CrossFade の際に2つ目の Playable を使う
- Play や CrossFade の度に既存の AnimationClipPlayable は作り直し、常に2つの状態を保つ
今回はスプライトアニメーションで使う前提なので CrossFade が必要なく2つ Playable を用意する必要は本来ないのですが、射撃アニメーション郡と切り替えることを想定してやはり2つ用意します。
public void Play(int index)
{
// 再生中のPlayableを削除
DisconnectPlayables();
// 通常と射撃用のPlayableを作成する
_basePlayable = AnimationClipPlayable.Create(_playableGraph, clipInfos[index].baseClip);
_overridePlayable = AnimationClipPlayable.Create(_playableGraph, clipInfos[index].overrideClip);
// ミキサーにそれぞれ接続、再生する
_animationMixer.ConnectInput(0, _overridePlayable, 0);
_animationMixer.SetInputWeight(0, 0);
_animationMixer.ConnectInput(1, _basePlayable, 0);
_animationMixer.SetInputWeight(1, 1);
}
ConnectInput と SetInputWeight でそれぞれ接続と Weight の設定を行っています。
レイヤーの切り替え機能
SimpleAnimation で2つのアニメーションを再生しながらレイヤー切り替えのようなことをしようとした場合、利用者側がレイヤーの Weight を見てやる必要がありました。
// 再生開始時にレイヤーを設定する場合
_simpleAnimation.Play(RunStateName);
_simpleAnimation.GetState(RunStateName).weight = 1 - ShootWeight;
_simpleAnimation.Blend(RunShootStateName, ShootWeight, 0f);
// 再生中にレイヤーを切り替える場合
_simpleAnimation.GetState(RunStateName).weight = 1 - ShootWeight;
_simpleAnimation.GetState(RunShootStateName).weight = ShootWeight;
せっかく自前でやるのでもう少し楽にしたいと思います。 UseOverrideClips
というプロパティを用意し、これを有効にすると射撃用のアニメーション郡に切り替わるようにします。実装は以下の通りです。
public bool UseOverrideClips
{
private get => _useOverrideClips;
set
{
_useOverrideClips = value;
// 現在の値を元にミキサーに接続している
// 射撃用 Playable の Weight を変更
var overrideWeight = _useOverrideClips ? 1 : 0;
_animationMixer.SetInputWeight(0, overrideWeight);
}
}
private bool _useOverrideClips;
こちらは現在再生中の Playable の設定を変更するものなので、再生開始時点にも設定が適用されるよう Play も少し変更します。
public void Play(int index)
{
~~ 中略 ~~
// UseOverrideClips の値を元に
// 射撃用 Playable の Weight を設定
_animationMixer.ConnectInput(0, _overridePlayable, 0);
var overrideWeight = _useOverrideClips ? 1 : 0;
_animationMixer.SetInputWeight(0, overrideWeight);
_animationMixer.ConnectInput(1, _basePlayable, 0);
_animationMixer.SetInputWeight(1, 1);
}
これで利用者側は Weight を気にせずレイヤーを切り替えられるようになりました。
// 再生と同時に射撃レイヤーに切り替え
_sample2DAnimation.Play(RunStateName);
_sample2DAnimation.UseOverrideClips = true;
PlayQueuedの実装
SimpleAnimation では上手く動作しなかった PlayQueued を実装します。改めてですが、PlayQueued は既に再生されているアニメーションがある場合はそちらが一周するまで待ってからキューに入っているアニメーションを再生するものでした。
したがってキューへの登録と、キューの中身とアニメーションの状態を見て次のアニメーションを再生する処理の2つを実装できていれば良さそうです。
まずキューへの登録ですがこれは LinkedList で作ったキューにインデックスを登録するだけでよく、とてもお手軽です。
private readonly LinkedList<int> _queuedIndexList = new LinkedList<int>();
public void PlayQueued(int index)
{
_queuedIndexList.AddLast(index);
}
次にアニメーションを再生する部分です。まずは実装を。
void LateUpdate()
{
// キューの中身がない場合は処理しない
if (_queuedIndexList.First == null) return;
// 長さが違うことがあり得るので UseOverrideClips
// に応じて待機時間を変える
var watchTarget = _useOverrideClips ?
_overridePlayable : _basePlayable;
// 現在のアニメーションが一周するか
// 再生中のものがない場合はキューの先頭を再生する
var isDone = watchTarget.GetTime() >= watchTarget.GetAnimationClip().length;
if (watchTarget.IsNull() || isDone)
{
Play(_queuedIndexList.First.Value);
_queuedIndexList.RemoveFirst();
}
}
注意点は3つあります。まず、コメントにもある通り通常のアニメーションと射撃レイヤーのアニメーションは長さが異なる場合があるため、現在の UseOverrideClips に応じて待機時間を変える必要があります。
次に Playables API で AnimationClipPlayable の長さをとる場合は GetDuration()
ではなく GetAnimationClip().length
とする必要がある点です。GetDuration でも長さを取れそうに思えますが、AnimationClip は標準でループする設定になっているため、 GetDuration は Inf を返します。
最後にこのアニメーションの監視と再生は LateUpdate で行う必要があることです。 Update では Unity のライフサイクルの関係で正確にアニメーションの終了を取れず、微妙にループしてから切り替わってしまうことがあります。
呼び出し側は SimpleAnimation とだいたい同じ内容になるので割愛します。以上で Playables API に関しては終了です。
まとめと所感
AnimatorController、SimpleAnimation、Playables API について見てきました。それぞれ良し悪しがあるように思いました。
良いところ | 気になるところ | |
---|---|---|
AnimatorController | ・遷移先などが視覚的に把握しやすい ・レイヤー周りの設定がお手軽 |
・遷移が複雑な場合は辛い ・2Dには不要な機能が多め |
SimpleAnimation | ・旧Animationのようなお手軽さ ・比較的軽い |
・若干バグあり? ・レイヤーがない |
Playables API | ・具体的に欲しい機能があれば対応できる | ・自分で組むのは面倒(かも) |
個人的には注意点はあるものの今回の用途では SimpleAnimation を普段使いするのが良さそうかなと思いました。そちらの気になる点は無視できる範囲ですし、Playables API で自前で作る場合は保守の必要があったり、今回の用途の場合は劇的に改善したというには及ばないためです。もちろんもっと特別なことをしたい場合は選択肢に入ると思います。
この記事がどの方法を採用するかの判断材料になれば幸いです。
ここで少し宣伝をさせてください!技術書展8の1日目(2/29) に Unity の Sprite に関する同人誌を出します。こちらの記事の内容も書籍用に手を加えた上で収録する予定です。是非チェックしてください!
https://techbookfest.org/event/tbf08/circle/5746705018388480