🌊

Unity Playable API を使いこなす

2024/11/04に公開

本記事について

Playable API は AnimatorController に依存せずにアニメーションを再生できる素晴らしい機能です。APIは洗練されており、ユーザー側でカスタムPlayableを作成することができます。
アニメーション、オーディオ、ビデオ、その他再生したいものをPlayable Graphというグラフ形式で制御・同期することができます。

なんといってもAnimatorControllerの状態爆発を避けることができるという点が魅力的です。スクリプトベースで直接アニメーション状態を管理しつつ、ゲームロジックや当たり判定やモーション遷移などを制御できます。複雑な遷移やキャンセル挙動もスクリプトなら(比較的)スッキリと実装できるからです。簡単なゲームならば複雑なアニメ遷移は仕様として存在しないのですが、アクションゲームやプラットフォーマー等、特定のジャンルではびっくりするほどアニメ遷移が複雑になります。アニメーションが触り心地に直結する類のゲームならばPlayable APIは救世主となるでしょう。

そんな素晴らしいPlayable APIですが、少々癖があり初学者には厳しいものがあります。直感に反し、マニュアルを隅から隅まで読まないと踏む抜く罠がいくつかあります。

本記事ではそれなりに安全かつ効果的にPlayable APIを実装する方法について、私なりにまとめます。Playable APIがリリースされた頃はUnity公式やインターネットによる集合知から有意義な情報を得られたのですが、近年アップデートされておりません。2024年現在においてもPlayable APIについて一歩踏み込んだ為になる記事がWeb上に少ない印象です。後人のため、また将来の私のため、ここに記します。(いつも忘れてしまう......)

基本的な説明

すでに有用な記事が多数存在するため、基本説明はそちらに譲ります。
公式ドキュメントが最低限詳しいのでなんとなく使う分にはいけます。

実装例

基本形の実装

後述する罠を踏まえた上で、playable leak しない基本形となる使い方をご紹介します。

// AnimationClipを再生するプレーヤー
// Playable APIのエッセンスが詰まっている
public sealed class SimpleAnimPlayer : IDisposable
{
    private Animator _animator;
    private PlayableGraph _playableGraph;

    // コンストラクタでAnimatorを保持しておく
    public SimpleAnimPlayer(Animator animator)
    {
        _animator = animator;
    }

    // playable リークしないようにちゃんとDisposeすること
    public void Dispose()
    {
        if(_playableGraph.IsValid())
        {
             _playableGraph.Destroy(); //明示的にDestroyしないとリークする
             _playableGraph = default;
        }
        _animator = null; //leaked managed shell対策
    }

    // AnimationClipを再生する
    public void PlayAnimationClip(AnimationClip clip)
    {
        // すでに再生済みならばリワインドして再利用する
        if(clip == currentClip)
        {
           clipPlayable.SetTime(0);
           clipPlayable.SetDone(false);
           return;
        }

        //_playableGraphがリークしないように古いgraphを破棄しておく
        Stop();

        // playableGraphを作成. 事前に作成せずに再生時に作成して間に合う
         _playableGraph = PlayableGraph.Create(nameof(SimpleAnimPlayer));

        // clip ノードを作成
        AnimationClipPlayable clipPlayable =
AnimationClipPlayable.Create(_playableGraph, clip);

        // output ノードを作成
        AnimationPlayableOutput playableOutput = AnimationPlayableOutput.Create(
            _playableGraph,
            nameof(SimpleAnimPlayer),
            _animator);

        // output <- clip の形に接続
        playableOutput.SetSourcePlayable(clipPlayable);

        // グラフ全体を再生
        _playableGraph.Play();
    }

    // 再生中のAnimationClipを停止する
    // Pauseと異なり完全に破棄する
    public void Stop()
    {
        if(_playableGraph.IsValid())
        {
             _playableGraph.Destroy(); //明示的にDestroyしないとリークする
             _playableGraph = default;
        }
    }
}


こういうGraphになります。

上記実装が基本形です。生成と破棄をペアにして必ずplayableGraphがリークしないように気を付けましょう。AudioやVideoやその他オレオレPlayableを実装する際もこの形を守ればOKです。

Animatorと同期させる実装

後述する AnimationPlayableUtilityを使ったバージョンをこうなります。

// Animatorに同期してAnimationClipを再生するプレーヤー
public sealed class AnimatorSyncPlayer : IDisposable
{
    private Animator _animator;
    private PlayableGraph _playableGraph;

    // コンストラクタでAnimatorを保持しておく
    public AnimatorSyncPlayer(Animator animator)
    {
        _animator = animator;
    }

    // playable リークしないようにちゃんとDisposeすること
    public void Dispose()
    {
        if(_playableGraph.IsValid())
        {
             _playableGraph.Destroy(); //明示的にDestroyしないとリークする
             _playableGraph = default;
        }
        _animator = null; //leaked managed shell対策
    }

    // AnimationClipを再生する
    public void PlayAnimationClip(AnimationClip clip)
    {
        // すでに再生済みならばリワインドして再利用する
        if(clip == currentClip)
        {
           clipPlayable.SetTime(0);
           clipPlayable.SetDone(false);
           return;
        }
        //_playableGraphがリークしないように古いgraphを破棄しておく
        Stop();

        // playableGraphを作成&接続&同期設定&Playまでまとめて実行
AnimationClipPlayable clipPlayable = AnimationPlayableUtilities.PlayClip(_animator, clip, out _playableGraph);

         clipPlayable.SetTime(0);
         clipPlayable.SetDuration(clip.duration);
         clipPlayable.SetDone(false);
    }

    // 再生中のAnimationClipを停止する
    // Pauseと異なり完全に破棄する
    public void Stop()
    {
        if(_playableGraph.IsValid())
        {
             _playableGraph.Destroy(); //明示的にDestroyしないとリークする
             _playableGraph = default;
        }
    }
}

Graphの形は同じなので省略します。

解説: 基本の使い方について

Playable Graph は 自動で破棄されないため、生成したら責任をもって破棄する必要があります。playableGraph.Destroy されないGraphは永続するようです。Animatorや AudioSourceがDestroyされようが関係ありません。graphは GameObjectではないからです。

PlayableGraphというstructなハンドルを介して UnityEngine側とやり取りしているだけなので、ハンドルに紐づくエンジン側のインスタンスをちゃんと破棄してあげましょう。graph.IsValid() でハンドルの生死は判定可能です。

AnimationPlayableUtilityがおすすめ

単にAnimationClip を再生するだけなら AnimationPlayableUtility を使用するのが推奨です。
なぜなら、これらは AnimatorのUpdateモード、CullingModeと同期してくれるからです。

Animatorを止めてもAnimationClipPlayableは動き続ける

Playable APIの罠その1です。
Animator.enabled=falseにすると見た目上モーション更新は止まりますが、裏側のPlayableGraphは元気に動いておりAnimationCilpのEvaluateは実行され続けています。つまりAnimationClipのEvaluateは実行され続けますし、AnimationClilpPlayableのTimeは進行しています。PlayableAPI 自体はMonoBehaviourやComponentではないので、OnDisableや OnDestroyも関係ありません。明示的に止めない限りplayされ続けます。これに気付かないとplayableの時間が進んだり、cpu負荷をこっそり計上したりと問題になります。

この仕様は考えてみれば当然で、Animatorと playable APIは無関係です。AnimationOutputPlayable とAnimatorが関連しているだけで、Playable Graph自体は Animatorと関係ありません。Playable Graph はAudioやスクリプトも更新し続けなければいけないので、Animatorが止まったところで動いていて欲しいはずです。再生したのならば、きちんと止めましょう。

なお、Animator.Stopは廃止されAnimatorの停止の方法は正式にAnimator.enabled=false になりました。

AnimationClipPlayableは標準でAnimatorと同期しない

Playable APIの罠その2です。
単に作成した AnimationClipPlayable は Animator と同期しません。
つまり、AnimatorのUpdateModeおよびCullingModeを参照しません。

たとえば、AnimatorがカリングされてUpdateされなかったとしても、AnimationClipPlayableは進行することがあります。

Animatorと同期したい場合AnimationPlayableUtilityを使えば、Animatorと同期するようになります。Animator.cullingMode=CullCompletelyに設定して、Animatorがcullされたとき、AnimationClipPlayableもPausedになってくれます。

残念なことにAnimationPlayableUtility以外に AnimaitionClipPlayable を Animatorと同期することはできません。

graph.SyncUpdateAndTimeMode(animator);

という拡張メソッドが存在するのですが、internal メソッドになっており、我々は直接実行することができません。そのため、AnimationPlayableUtilityを使うしか他に手はありません。

Custom playable graph with ‘Animate Physics’ turned on

Culling Mode によって止まったり止まらなかったりする

CullingMode.CullCompletely は Animator配下のRendererすべてが非表示のとき、AnimatorをUpdateしないというものです。
AnimatorにリンクしたAnimationClipPlayable は CullされたときPause状態になります。

Animator.enabled=false でも AnimationClipPlayableは止まりません。
CullingModeでCullされたときはPauseされます。
逆にCullされないときは常にPlayingのようです。
Cullされる条件は Animator.enabled になった時点で認識された配下のRendererインスタンスです。
そのため、動的にRendererオブジェクトを増減させた場合、CullingModeの評価対象になりません。
評価対象にするには Animator.enabled を falseにしてtrueにするとその時点のRendererで評価してくれるようになります。

CullingModeの発動条件は以下のいずれかを満たすときのようです。

  1. 配下のすべてのRendererインスタンスのRenderer.enabled がfalseである
  2. 配下のすべてのRendererインスタンスのバウンディングボックスがすべてのカメラの視錐台の外にある

1は描画物が存在しないため、AnimatorのCullCompleteの条件に合います。逆に一部のパーツでも描画されるならCullCompleteの条件に合わないためアニメーションします。

2はフラスタムカリングによるものです。Sceneカメラを含めて判定されるため、Sceneビューで写っている間はCull対象にならないことに注意してください。デバッグのためにSceneビューで様子を眺めにいくとアニメーションしてしまいます。

※ Occlusion Cullingは調査していません。

isDone呼ばれない問題

ドキュメントにちゃんと書いてあります。ふつうはそんなところまで読まないので罠を踏みます。
isDone が有効になるためには SetDurationで durationを設定しておく必要があります。
デフォルトでは +infに設定してあり、無限durationです。
SetDurationで有効な値をセットしておき、初めてtimeがdurationを超えたときにisDoneがtrueとなります。また、一度isDoneがtrueになったら自動でfalseに戻ることはりません。
リワインド、再利用、ループ処理などを適切に行うためには自前で SetDone(false)でisDoneフラグをもとに戻して差し上げる必要があります。

         clipPlayable.SetTime(0);
         clipPlayable.SetDuration(clip.duration); //これ重要
         clipPlayable.SetDone(false); // フラグも下げとくのが安全

isDoneの想定された使い方としては N回ループ再生したときに doneとする、というものかと想像します。例えばSetDuration(lengh * N) としたならば、N倍長再生したときに完了します。wrapモードがループならば、N回ループ再生したといえますし、wrapモードがpingpongならN往復したといえます。

このように、(無駄に)拡張性が高いため罠になりがちです。Animationというものは、1回再生するか無限ループしたいかのどちらかであることがほとんどです。アイドル、死亡、エアリアルのようにモーション遷移はコードからの割り込みで制御したいのですから、任意のdurationを指定することはほぼありません。データ駆動とするためにclip側に持たせるか、別途データシートに持たせていることでしょう。

直感的な操作のため、isDone を期待通りに動作させるためには SetDuration(clip.length)と設定することを推奨します。無限ループ再生するときはisDoneとならないように SetDuration(double.MaxValue)にしてもいいでしょう。

Animancer も Playable APIだよ

Animancer という 有名アニメーションAsset も裏で Playable APIを使用しています。
本記事を読んで、「よし、アニメーション周りを自前実装するぞ!」とやる気になった方は、一度Animancer Liteを評価してみてください。気に入ったなら Animancer Pro を買えばいいですし、それでも満たせない機能があるならば自前実装するのがよいでしょう。

https://kybernetik.com.au/animancer/docs/source/graph-structure/

※ 本記事は広告ではありません。 Playable API の参考実装を調査していたら Animancerの中にいたのを発見したまでです。

Playable Graph Visualizer

Playable APIの実装はこれがないと始まりません。それぐらい重要なパッケージです。
2018年で開発停止しておりますが、もう機能追加の必要がないということなのかもしれません。
少しだけ注意点があります。

https://github.com/Unity-Technologies/graph-visualizer

Playable Graph Visualizer の注意点

UnityEditor 上では 結構なGC.Alloc を発生させます。メモリリークや Playable リークを調査するときに騙されないようにしましょう。必要ないときは タブを閉じておく、もしくは別のタブにドックして表示しないようにしておくとよいでしょう。
Playable Graph Visualizer は EditorOnlyであり、Runtime には搭載されないため実機で問題を起こすことはありません。

Runtime用のstatic インスタンスに注意

Playable Graph Visualizer のrutime用ブリッジクラスとして、GraphVisualizerClientが存在します。GraphVisualizerClient クラスがシングルトンになっているため、うかつに登録してGraphVisualizerClient.Clear が漏れるとほんの少しメモリリークします。

PlaybleGraph playableGraph = PlayableGraph.Create("ふがふが");
GraphVisualizerClient.Show( playableGraph); // PlayableGraphVisualizerWindowに明示的に表示できる. List<PlaybleGraph>へのAddであるので、

... // なんかする

GraphVisualizerClient.Hide( playableGraph); // 明示的にHideしてList<PlayableGraph>からRemoveしてあげましょう
playableGraph.Destroy();

static 変数ということは Unityの Domain Reloadにも対応しないといけないので、GraphVisualizerClientを使用する場合は、適切なタイミングでClearしてあげましょう。

正直なところUnityEditor上では自動的にgraphがshow/hideされているようなので、GraphVisualizerClient は使わなくてもいいと思います。

Discussion