こんにちは。情熱開発部プログラマの樋宮です。
気づけばもう10月。
街はハロウィンの装飾でにぎやかですが、僕の頭の中は今日も「アニメーションどう制御しよう?」でいっぱいです。
普段はAnimatorに任せっきりの動きを、もう少し自分で触ってみたい——
そんなときに使えるのが Playable API(PlayableGraph)。
ということで今回はPlayable APIを使ってAnimationとUIを連携させてみようと思います。
※本ブログではUnity 60000.0.40f1を使用しています
目次
そもそもPlayable APIって何?
UnityでAnimationを制御するとなると大抵の場合、AnimatorあるいはTimelineを利用することがほとんどかと思います。
実際、ステートでの管理やトランジションの設定などほとんどのことはこなせてしまいます。
でも、「Stateの数が膨大になってきて管理が複雑になってきた」「AnimationEventでScriptと同期していたけどもっと細かく制御したい」と思ったことはないでしょうか?
そこで登場するのがPlayable API(PlayableGraph)です。
簡単に言うとPlayable APIとは「スクリプト上でAnimation再生の裏側の処理を自分で組み立てられる仕組み」です。
Animation Clipを再生したり、複数のClipのブレンドを行ったり、
さらには音や関連するUIやシェーダーのスクリプトなどと直接連携を図ることが可能です!
簡単な仕組みの解説
Playable APIには大きく分けて3人の登場人物がいます。
これらのどれか一つでもかけているとうまく動かないため、実装の際は気をつけましょう。
・Playable Graph:再生全体を管理する
・Playable:再生する個々の要素(Animation、Sound、etc…)
・Playable Output:再生を出力する先(Animator、AudioSource、etc…)
これらを組み合わせることで細かく制御していくわけです。
今回はAnimation⇒Animatorで出力⇒UI更新というフローを作成していきます。
簡単なサンプルを実装してみる
では実際に簡単なサンプル実装を行っていきます
目標は2つです。
①まずPlayableAPIを使用してAnimation再生を行えるようにする。
② ①で作成したものに追記する形で再生進行度をUIのSliderに反映してみる。
まずは①を達成するためのAnimation再生用コードを記述していきましょう。
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using UnityEngine.UI;
public class PlayableUISample : MonoBehaviour
{
[SerializeField] private Animator animator;
[SerializeField] private AnimationClip clip;
private PlayableGraph graph;
void Start()
{
graph = PlayableGraph.Create("TestGraph");
graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
// ① AnimationClipPlayable(通常のアニメーション出力)
var clipPlayable = AnimationClipPlayable.Create(graph, clip);
var animOutput = AnimationPlayableOutput.Create(graph, "AnimOutput", animator);
animOutput.SetSourcePlayable(clipPlayable);
graph.Play();
}
void OnDestroy()
{
// 明示的に破棄をする
graph.Destroy();
graph = default;
}
}
コードはこちらで完了。
一つずつ確認していきましょう
graph = PlayableGraph.Create("TestGraph");
graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
ここでPlayableGraphを作成しています、Create()の引数には名前を設定して渡すことが可能です。
SetTimeUpdateMode()では何をしているかというと、実際のGraphの更新方法を決定できます。
GameTimeでは通常通りの更新方法でTime.deltaTimeを使用して更新されます。
他にはManualなどがありManualの場合はgraph. Evaluate(float deltaTime = 0)を明示的に呼び出さない限り更新されません。
初期値はGameTimeになっているので今回の場合は明示的に呼び出す必要はないのですが、せっかくの紹介なので記述しておきます。
これで3人いる登場人物の内1人が登場したわけです。
var clipPlayable = AnimationClipPlayable.Create(graph, clip);
つづいてこちら。
ここでは2人目の登場人物であるPlayableを作成しています。
今回はAnimationの再生になるので作成するのはAnimationClipになります。
そのため使用する関数はAnimationClipPlayable()になり、引数に先ほど作成したPlayableGraphとAnimationClipを渡しておきます。
ここで使用する関数は生成したいPlayableによって変わるので注意してください。
var animOutput = AnimationPlayableOutput.Create(graph, "AnimOutput", animator);
animOutput.SetSourcePlayable(clipPlayable);
そしてOutputの生成と設定です。
AnimationのOutputになるためここで使用する関数はAnimationPlayableOutput()になります。
こちらもPlayableの生成と同じく対象によって関数が変わってきます。
引数にはGraph、名前、Animatorが必要です。
名前は自由に決定しましょう。
これで再生に必要な3人がそろったことになります。
が、忘れてはいけないのがこちら。
void OnDestroy()
{
// 明示的に破棄をする
graph.Destroy();
graph = default;
}
PlayableGraphはきちんと明示的に破棄をしてあげる必要があります。
破棄をしない場合ずっとメモリに残り続けてしまうため、寿命管理には注意する必要があります。
今回は特に外からgraphを変更したりすることはないのでOnDestroy()で破棄をしてあげます。
Scene側の準備
ここまででコード側の準備は整いましたね。
では実際にアニメーションさせるためのものをScene側で用意していきましょう。
今回は凝ったアニメーションを動かすわけではないので、CubeがY軸基準で回転するだけの簡単なAnimationClipを用意して
Scene上のCubeにAnimatorをアタッチしておく程度ですね。
先程作成したスクリプトも忘れず付けて各種SerializeFieldにデータを入れて完了です。

今回はPlayableから再生するのでAnimatorControllerは必要ありません。
では準備も完了したので実際にPlayを押して確認してみましょう。

無事動かすことに成功しましたね。
これで目標の①は達成です!
UIと連携させる
では続いてUIとの連携ですね。
先ほどのクラスに追記するコードと新規のコードとがあるので順番に見ていきます。
まずはPlayableUISampleから。ハイライトされている箇所が追記に当たります。
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using UnityEngine.UI;
public class PlayableUISample : MonoBehaviour
{
[SerializeField] private Animator animator;
[SerializeField] private AnimationClip clip;
[SerializeField] private Slider slider;
private PlayableGraph graph;
void Start()
{
graph = PlayableGraph.Create("TestGraph");
graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
// ① AnimationClipPlayable(通常のアニメーション出力)
var clipPlayable = AnimationClipPlayable.Create(graph, clip);
var animOutput = AnimationPlayableOutput.Create(graph, "AnimOutput", animator);
animOutput.SetSourcePlayable(clipPlayable);
animOutput.SetTarget(animator);
// ② ScriptPlayable(Slider更新用)
var sliderPlayable = ScriptPlayable<UISliderPlayable>.Create(graph);
var sliderOutput = ScriptPlayableOutput.Create(graph, "SliderOutput");
sliderOutput.SetSourcePlayable(sliderPlayable);
sliderPlayable.GetBehaviour().slider = slider;
sliderPlayable.GetBehaviour().clipLength = clip.length;
sliderPlayable.SetDuration(clip.length);
graph.Play();
}
void OnDestroy()
{
// 明示的に破棄をする
graph.Destroy();
graph = default;
}
}
続いてすでに出ていますが新規のUISliderPlayableクラス。
using UnityEngine.Playables;
using UnityEngine.UI;
public class UISliderPlayable : PlayableBehaviour
{
public Slider slider;
public double clipLength;
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
if (slider == null || clipLength <= 0)
return;
double time = playable.GetTime();
float normalized = (float)((time % clipLength) / clipLength);
slider.value = normalized;
}
}
追記分から順番に見ていきます。
animOutput.SetTarget(animator);
Outputの生成時に引数で渡していますがここでも設定しています。
単にAnimationClipを再生する分には問題ないのですが、他Outputなどと連携する際は明示的に設定する必要があります。
var sliderPlayable = ScriptPlayable<UISliderPlayable>.Create(graph);
var sliderOutput = ScriptPlayableOutput.Create(graph, "SliderOutput");
sliderOutput.SetSourcePlayable(sliderPlayable);
sliderPlayable.GetBehaviour().slider = slider;
sliderPlayable.GetBehaviour().clipLength = clip.length;
sliderPlayable.SetDuration(clip.length);
こちらがUI用の設定箇所になります。
graphに関しては同じものを使うので残りのPlayableとOutputを作成し設定する必要があります。
UIを動かすのはコードになるため作成するのはScriptPlayableになっているのがわかるかと思います。
下3行については各種パラメータを渡している形になります。
UISliderPlayableクラスについてはUIのための計算は割愛して重要な箇所を見ていきます。
UISliderPlayable : PlayableBehaviour
単にAnimationClipを再生するだけではないような、コードやサウンドなど独自の処理を差し込みたい場合PlayableBehaviourを継承することで専用の関数を扱えるようになります。
そのうち3つを今回は紹介します。
public override void OnBehaviourPlay(Playable playable, FrameData info)
{
// PlayableGraph実行時1度だけ発火される
}
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
// PlayableGraph実行中は毎フレーム発火される
}
public override void OnBehaviourPause(Playable playable, FrameData info)
{
// PlayableGraph終了時に1度だけ発火される
}
今回は毎フレームSliderを更新したいのでProcessFrame()を使用しています。
では、新しく追加されたSerializeFieldへのSliderのアサインを忘れずに行って実行してみます。

AnimationClipの開始から終わりまでに同期してSliderが動いているのが確認できましたね!
PlayableGraph Visualizerについて
最後にデバッグなどで利用できるPlayableGraph Visualizerについて紹介します。
こちらはコード上で記載した各種Playableの要素についてそれぞれがどのように接続されているかを確認できるものです。
VisualizerについてはGitHubで公開されており、PackageManagerから導入が可能です。
GitHubのURLはこちら
導入後はWindow⇒Analysis⇒PlayableGraph Visualyzerを選択することで表示が可能です。
Sceneを実行中でないと確認ができない点に注意してください。
では早速先ほど作成したもので見てみましょう。

作成したTestGraphの中でAnimationClipがAnimationOutPutに、UISliderがScriptOutputに正しく接続されているのがわかりますね。
もしうまく動作しない場合は接続が正しくない場合もあるのでこちらを活用してみてください。
ちなみに・・・・
試しにテスト用のTimelineを作成してViewerで見てみます。
すると….

TimelineもPlayableDirectorの名の通り中身はPlayableAPIで動いているのがわかりますね。
今回紹介しなかったクラスも表示されていますが、AnimationClipをAnimationOutputに接続している部分は変わりません。
表示されているAnimationMixerはAnimationClipのブレンドなどに使用されるものですね。
最後に
今回はPlayableAPIに焦点をあてて簡単なサンプル実装をして中身をのぞいてみました。
PlayableAPIの強みとしてはやはり、再生時間との同期にあると思います。
同じGraphを共有することで、どこまで再生されたか完了したのかなどが正確に取得できるため今回のようにUIなどとの連携が簡単に行えます。
今回はUIで行いましたが、シェーダーやカメラなどと各種アニメーションと連携させることで様々な演出に活かせそうです。
ぜひ活用してみてください!
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。