ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

【Unity】TimelineのClipにBehaviourを生成させない

Timeline(というかPlayableAPI)の基本

Timelineはその実態を掴むのが結構難しいパッケージだと思います。(実態を掴まなくてもなんとなく使えてしまうんですが。)

まず今回の話の前提として「TrackAssetやTimelineClipはScriptableObjectで、実際にアニメーションを再生したり動作する際には動作のためのインスタンス(Playable)を生成している」という事実をちゃんと理解する必要があります。 公式に解説があればいいんですが、公式マニュアルは「どう使うか」の部分ばかりで「どう実装されているか」はあまり見当たりません。TimelineパッケージもUnityエディタ同様根っこの部分はC++で実装されており、C++実装部分は非公開なので最終的にはユーザーの方で調査するしかないです。Unityは本当にこういうところがよくないです。

さて、Timelineの理解のために個人的にかなり参考になったものをいくつか挙げておきます。

qiita.com

2019年と少し古い記事ですが、Timelineを構成する要素を簡潔に網羅してくれています。

yotiky.hatenablog.com

Playableの本質であるPlayableGraphの動作について詳細にまとめられています。詳細な分ちょっと長いですが、内部の理解のためには余計な(どう実装するかみたいな)情報無くまとめられているのでじっくり読むと理解が深まるのではないかと思います。

ClipのBehaviourいらなくね?

前述の参考記事においてこのような記述がありました。

qiita.com

自分でつくったカスタムのクリップに対して、 実行中でもエディタ上でも同じように動いてもらおうとすると、意外と苦労しました。
これは、前述しましたが、クリップのPlayableをプログラミングするだけでは、「クリップが置かれていないフレーム」の挙動を指定することができないため、といった理由が大きいと思ってます。
そこで、自分は、基本的には クリップからカスタムのPlayableを生成するということは一切せず*1、トラック側で毎フレーム、全クリップのデータを見ることで フレームごとの状態を決める、というやりかたにすべて統一することにしました。

これについては自分も同意見です。 「クリップのBehaviourには動作は任せず、トラックのBehaviourに全て記述する」というのはTimelineの実情(実際にはTimelineGUIの実情)を考えればかなり妥当なのではないかと思います。

ClipはPlayableの生成は必ず必要

しかし、TimelineはPlayableAPIをベースにしているため、ただのScriptableObjectであるクリップは動作のためにPlayableを生成することを義務付けています。実装としては、クリップとしてTimelineGUI上で扱う条件として、「ScriptableObjectであり且つIPlayableAssetを実装していること」を具体的に指定しています。(そしてTimelineAssetクラスがこの2つを継承 / 実装しているクラスなので、クリップはTimelineAssetを継承するように言われるわけです)

IPlayableAssetの中身は以下のようになっています。

public interface IPlayableAsset
{
    Playable CreatePlayable(PlayableGraph graph, GameObject owner);

    double duration { get; }

    IEnumerable<PlayableBinding> outputs { get; }
}

durationは確かに必要だと思いますが、クリップをただのデータとして扱う場合には不要なCreatePlayableとoutputsもしっかり指定されています。

CreatePlayable

CreatePlayableには、一般的には以下のような処理が書かれます。

public class CustomClip : PlayableAsset
{
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        var playable = ScriptPlayable<CustomBehaviour>.Create(graph);
        var behaviour = playable.GetBehaviour();
        behaviour.Initialize(); // カスタムBehaviour毎に必要ななんらかの初期化
        return playable;
    }
}

要するにClipに対応したBehaviourと、PlayableGraphに追加するPlayableの2つを生成し、PlayableにはBehaviourを渡す、という処理です。

Playableの生成は必要でも、Behaviourの生成は必要ではない

しかしここで1つ疑問が浮かびます。それは、先述の「クリップのBehaviourには動作は任せず、トラックのBehaviourに全て記述する」という方針を取った場合、クリップのBehaviourは何のためにあるのか、ということです。この方針の場合、もはやクリップはただの値が固定のデータでしかないです。
クリップのBehaviourの役割はというと、ブレンドが有効な際にトラックのBehaviour側でplayable.GetInputWeight(index)して各時刻の重みを取得するくらいです。が、別にこれBehaviourを使う必要無いですよね。各時刻の重みは元々クリップに設定している値ですから。実際、以下のようにクリップから重みを取得できます。

// localTime: そのクリップの(直属のトラックの)直属のPlayableDirectorのtime
var weight = clip.EvaluateMixIn(localTime) * clip.EvaluateMixOut(localTime);

これはTimelineパッケージ内でも使われている計算式です。

Playableはテキトーにdefaultで生成しておいても動く

というわけで、クリップのBehaviour生成は実は必須ではありません。トラックのBehaviourに全て任せる方針ならば、以下のようにCreatePlayableをテキトーに行っても問題ありません。

public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
{
    return default;
}

このPlayable生成すらしたくないんですが、クリップはIPlayableAssetの実装が必須な設計になっているので渋々生成しています。

余談: Timelineパッケージの違和感

クリップがIPlayableAssetの実装を義務付けておきながら、実際の動作としてはPlayableは必須ではない、というのはかなり気持ちが悪いです。これは、Timelineパッケージ、というよりそのGUIの問題だと思います。TimelineはPlayableAPIという柔軟なAPIをベースにしながら、実際にはトラックやクリップといった具体的なレイヤー構造を指定しています。さらに、GUIの上で必要だったものと、動作の上で必要だったものを分けていない(インターフェースが適切に分離されていない)ために、本来必須ではないものを入れなくてはならない状態になっています。これは理解が難しい原因にもなっていると思います。

正直設計ミスに感じます。
MonoBehaviourというかTransformでも、アバターなどのボーン構造がヒエラルキーで見れてしまう点が非常に気持ちが悪く問題だと思っているのですが(GetComponentInChildrenとかでボーンやメッシュにアクセスできるのヤバい)、なんかそれと似た感じで、設計に必要な柔軟性というものを取り違えてるんじゃないか?と思ってしまいます。

*1:実際にはPlayableを生成しない、というのは不可能なので、「これはCreatePlayableを行わない」という意味とは異なるか、もしくは記事執筆時点からTimelineパッケージの中身が変わっているか、だと思います。