ゆいブロ

自分の好きなことや知見をまとめていくブログです。

スポンサーリンク

【Unity】TimelineのTrackを別のTimelineにコピーする

はじめに

https://docs.unity3d.com/ja/2018.4/uploads/Main/timeline_splash.jpg

TimelineAsset内のTrackを、別のTimelineにコピーしなければいけない時があり、
その際に他のTimelineにデータをコピーする方法を調べたのでまとめます。

この方法だと、TimelineのBind設定も引き継いでくれます(例外あり)。 引き継げないものに関しては後述します。

実装の注意点

Unity2019.2.17f1にて実装しております。

System.Reflectionを使用しているため、Unity2018以前などではネームスペースが変更されておりMethodInfo等が取得できない可能性があります。

実装方法

/// <summary>
/// Timelineの複製
/// </summary>
///
/// <param name="fromDirector"> 複製元のPlayableDirector </param>
/// <param name="toDirector"> 複製先のPlayableDirector </param>
private void DuplicateTimeline(PlayableDirector fromDirector, PlayableDirector toDirector )
{
    if( toDirector.playableAsset is TimelineAsset targetTimelineAsset )
    {
        if( fromDirector.playableAsset is TimelineAsset timelineAsset )
        {
            Assembly assembly = Assembly.Load( "Unity.Timeline.Editor" );

            // AssemblyからTimelineEditorの機能をReflectionで取得してくる
            var trackExtensionsType = assembly.GetType( "UnityEditor.Timeline.TrackExtensions" );

            if( trackExtensionsType == null )
            {
                Debug.LogWarning( "TrackExtensions not found." );
            }

            var duplicateInfo = trackExtensionsType.GetMethod( "Duplicate",
                                BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Static );

            if( duplicateInfo == null )
            {
                Debug.LogWarning( "TrackExtensions.Duplicate not found." );
            }

            // Duplicateメソッドに渡す引数の配列をあらかじめ作成
            // Duplicateメソッドを呼ぶ際はそれぞれの複製物をindex:0に代入して渡す
            // TrackAsset, ターゲットのPlayableDirector, null, コピー先のTimelineAssetの順
            var duplicateArgs = new [] { ( object )null, toDirector, null, targetTimelineAsset };

            // タイムラインアセットのルートトラックを全て複製する
            // ルートトラックを複製する際に、そのトラックの子トラックも複製されるみたい
            foreach( TrackAsset source in timelineAsset.GetRootTracks() )
            {
                // 設定していないのにMarkerTrackが作成されることがあるのでスキップする
                         // この部分は各自カスタマイズしてください。
                if( source is MarkerTrack )
                {
                    continue;
                }

                // パラメータ配列の先頭をコピーしたいトラックに置き換える
                duplicateArgs [0] = source;
                // TrackをCloneして、作成したグループトラックに紐づける
                TrackAsset clone = duplicateInfo.Invoke( null, duplicateArgs ) as TrackAsset;

                // Bind設定を反映させる
                CopyBindings( source, clone, fromDirector, toDirector );
            }

            // リフレッシュしないとTimeline Editorに反映されないらしいので実行する
            TimelineEditor.Refresh( RefreshReason.ContentsAddedOrRemoved );
        }
    }
}

/// <summary>
/// バインド設定をコピーする
/// </summary>
///
/// <param name="fromTrack"> 複製元のトラック </param>
/// <param name="toTrack"> 複製先のトラック </param>
/// <param name="fromDirector"> 複製元のPlayableDirector </param>
/// <param name="toDirector"> 複製先のPlayableDirector </param>
private void CopyBindings( TrackAsset fromTrack, TrackAsset toTrack, PlayableDirector fromDirector,
                           PlayableDirector toDirector )
{
    toDirector.SetGenericBinding( toTrack, fromDirector.GetGenericBinding( fromTrack ) );

    if( fromTrack.GetChildTracks().Any() )
    {
        // 子トラックも複製されているはずなので、それぞれ再帰的にバインドする
        var fromChildren = fromTrack.GetChildTracks().ToArray();
        var toChildren = toTrack.GetChildTracks().ToArray();

        for( var i = 0; i < fromChildren.Length; i++ )
        {
            CopyBindings( fromChildren[i], toChildren[i], fromDirector, toDirector );
        }
    }
}

解説

Reflectionで内部APIにアクセスする

Assembly assembly = Assembly.Load( "Unity.Timeline.Editor" );

// AssemblyからTimelineEditorの機能をReflectionで取得してくる
var trackExtensionsType = assembly.GetType( "UnityEditor.Timeline.TrackExtensions" );

if( trackExtensionsType == null )
{
    Debug.LogWarning( "TrackExtensions not found." );
}

var duplicateInfo = trackExtensionsType.GetMethod( "Duplicate",
                    BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Static );

if( duplicateInfo == null )
{
    Debug.LogWarning( "TrackExtensions.Duplicate not found." );
}

Assembly.Loadを使用して、通常では呼び出せないクラスやメソッドにアクセスしています。

"Unity.Timeline.Editor"や"UnityEditor.Timeline.TrackExtensions"の部分はUnityのバージョンによって変わることがあります。 (現にUnity2018では別の名前になっていたはずです…)

内部APIを確認するには、下記の方法で行うと見つかります。 アクセスできない場合は試してみてください。

tsubakit1.hateblo.jp

Duplicateメソッドを呼ぶ

var duplicateArgs = new [] { ( object )null, toDirector, null, targetTimelineAsset };

この一文で、Duplicateメソッドの引数を配列として定義しています。

Reflectionで無理やり呼び出す場合、いつものように引数を一つずつ指定することはできず、 引数を配列として作成し、渡してあげなければ動きません。

index0に、コピーしたいTrackAssetを、index3に、コピー先のTimelineAssetを入れるようにしてください。

// パラメータ配列の先頭をコピーしたいトラックに置き換える
duplicateArgs [0] = source;
// TrackをCloneして、作成したグループトラックに紐づける
TrackAsset clone = duplicateInfo.Invoke( null, duplicateArgs ) as TrackAsset;

この部分で、foreach文で取得してきたTrackAssetを先ほどの配列のindex0に代入し、その配列を元にDuplicateメソッドを呼びます。 Invokeメソッドの引数は

public object Invoke (object obj, object[] parameters);

となっているため、一番目にnullを、二番目にDuplicateの引数をobject配列にしたものを入れています。

詳細は下記のドキュメントを参照してください。 docs.microsoft.com

Bindのコピー

toDirector.SetGenericBinding( toTrack, fromDirector.GetGenericBinding( fromTrack ) );

この部分で、複製元のBind情報を元に、複製先のPlayableDirectorにBind情報をコピーしています。 ただ、この方法だと、Bindしているオブジェクトが複製元のTimelineのPrefab内にある、と言った場合に参照切れになってしまいます。

解決方法を模索したのですが上手い方法が思いつきませんでしたのでいったんこの形にしています。 (AnimationTrack内のClipにBindされているAnimationClipのBind情報などはPrefabに関係ないため上手くコピーできます。)

また、この方法だとCinemachineTrack内のClipにBindされているVirtualCameraの参照が上手くコピーできませんでした。 これについては別途記事にします。