Unity Timeline の拡張パターン
Unity の Timeline はスクリプトで拡張できるのも良いところですが、その度合いによってさまざまな方法があります。そこで、できることとできないことを明示しながらそれぞれの方法をまとめます。
Unity 公式記事とそっくりにも見えますが、公式記事のシンタックスハイライトが最近効いていなくてちょっと読みづらいこと、現在のバージョンでは通じない部分があること、各構成のデメリットを示すことで、少なくとも自分が開発する際の資料としてより有用なものとするために筆を取りました。
環境
- Unity 2022.3.15f1
- Timeline 1.7.6
短いまとめ
構成 | こんな時に |
---|---|
構成 1: ITimeControl | クリップの出入りと時間さえわかれば良い時 |
構成 2: PlayableBehaviour と PlayableAsset | クリップの Inspector で値を変更したい時 |
構成 3: 構成 2 に TrackAsset を加える | 構成 2 に加えて: 1つのコンポーネントに複数のクリップで操作をしたい時 |
構成 4: 構成 3 にミキサーを加える | 構成 3 に加えて: クリップのブレンディングや初期値の復元を行いたい時 |
構成 5: PlayableBehaviour をテンプレート化する | Timeline 上でアニメーションを作りたい時 |
以下でそれぞれの構成について、実装を含めて詳しく見ていきます。
構成 1: ITimeControl
最もシンプルな方法です。ITimeControl
を継承した MonoBehaviour
を用意し、Timeline の Control Track に配置します。
できること
クリップの開始、再生中、終了のコールバックを受け取ることができます。実装もシンプルです。
できないこと
クリップの Inspector を拡張することはできません。Timeline 上でパラメータを変えたい場合は選択肢から外れます。
上のキャプチャは次のようなコードで動いています。
using TMPro;
using UnityEngine;
using UnityEngine.Timeline;
public class TimeTextSetter : MonoBehaviour, ITimeControl
{
[SerializeField]
;
TMP_Text tmp
public void SetTime(double time)
{
.text = time.ToString(@"F2");
tmp}
public void OnControlTimeStart()
{
.SetActive(true);
gameObject}
public void OnControlTimeStop()
{
.SetActive(false);
gameObject}
}
構成 2: PlayableBehaviour と PlayableAsset
クリップの Inspector に値や参照を露出させることができるようになります。 PlayableBehaviour
に振る舞いを書き、PlayableAsset
から値や参照を注入します。
できること
クリップ毎に Inspector でパラメータを操作できます。また、利用できるコールバックが多く、細かい制御が行えます。
できないこと
シーン上の単一のコンポーネントを複数のクリップから操作する場合には向いていません。その場合は構成 3 を検討します。次のようなデメリットがあるからです。
- 参照をクリップごとに刺していくのが大変。
- シークした時点より前のクリップの処理が行われないので、エディタ上で使いづらい。(解決法をご存知の方がいらしたら教えて下さい。)
OnGraphStop()
でプレビュー解除時に初期値に戻すコードを書いても、一番最後のクリップのものだけが反映されてしまう。(前のクリップから順にOnGraphStop()
が呼ばれるため。)
上のキャプチャは次のようなコードで動いています。PlayableBehaviour
は振る舞いを定義するもので、
using TMPro;
using UnityEngine;
using UnityEngine.Playables;
public class TextGradientPlayableBehaviour : PlayableBehaviour
{
public TMP_Text Target;
public Color StartColor;
public Color EndColor;
public override void OnGraphStop(Playable playable)
{
if (!Application.isPlaying)
{
// エディタ上でプレビューを解除した時に、初期値に戻す
// 不要な差分が出づらくなってバージョン管理的に嬉しいです
.color = StartColor;
Target}
}
public override void PrepareFrame(Playable playable, FrameData info)
{
var normalizedTime = (float)(playable.GetTime() / playable.GetDuration());
.color = Color.Lerp(StartColor, EndColor, normalizedTime);
Target}
}
PlayableAsset
は Timeline 上に置くクリップやその Inspector を定義し、PlayableBehaviour
に値や参照を渡すものです。
using TMPro;
using UnityEngine;
using UnityEngine.Playables;
[System.Serializable]
public class TextGradientPlayableAsset : PlayableAsset
{
[SerializeField, Header("操作対象のテキスト")]
<TMP_Text> target;
ExposedReference
[SerializeField, Header("フェード設定")]
;
Color startColor
[SerializeField]
;
Color endColor
public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
{
var behaviour = new TextGradientPlayableBehaviour
{
= target.Resolve(graph.GetResolver()),
Target = startColor,
StartColor = endColor,
EndColor };
return ScriptPlayable<TextGradientPlayableBehaviour>.Create(graph, behaviour);
}
}
構成 3: 構成 2 に TrackAsset を加える
トラックに刺した参照を PlayableBehaviour から取得できます。
できること
トラックに参照を刺したり、配置できるクリップの種類、帯の色を指定できます。これにより、トラックの見た目のわかりやすさも向上します。1 つのコンポーネントをたくさんのクリップで操作したい場合に選択肢となります。
できないこと
構成 2 と同じく、トラック全体について 1 度のコールバックを受け取ること。そのため、プレビュー終了時に初期値を復元することが難しいか不可能になります。この理由で、個人的には構成 4 にせざるを得ないことが多いと考えています。
TrackAsset
を継承して、カスタムトラックを定義します。
using TMPro;
using UnityEngine.Timeline;
[TrackClipType(typeof(TextGradientPlayableAsset))]
[TrackBindingType(typeof(TMP_Text))]
[TrackColor(0.7f, 0.2f, 0.7f)]
public class TextGradientTrack : TrackAsset { }
トラックにバインドしたコンポーネントは、ProcessFrame
で取得します。
using TMPro;
using UnityEngine;
using UnityEngine.Playables;
public class TextGradientPlayableBehaviour : PlayableBehaviour
{
; // private に変更
TMP_Text target
public Color StartColor;
public Color EndColor;
// OnGraphStop での初期値復元は、最後のトラックが適用されてしまい、目的を達成できないので削除
// バインドされたコンポーネントを取得するために playerData が必要なので PrepareFrame を ProcessFrame に
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
= playerData as TMP_Text;
target
if (!target) return;
var normalizedTime = (float)(playable.GetTime() / playable.GetDuration());
.color = Color.Lerp(StartColor, EndColor, normalizedTime);
target}
}
次のコードでは、TrackAsset
から刺すことになったので TMP_Text
を削除します。このサンプルではその箇所をコメントアウトで示しています。
// using TMPro;
using UnityEngine;
using UnityEngine.Playables;
[System.Serializable]
public class TextGradientPlayableAsset : PlayableAsset
{
// [SerializeField, Header("操作対象のテキスト")]
// ExposedReference<TMP_Text> target;
[SerializeField, Header("フェード設定")]
;
Color startColor
[SerializeField]
;
Color endColor
public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
{
var behaviour = new TextGradientPlayableBehaviour
{
// Target = target.Resolve(graph.GetResolver()),
= startColor,
StartColor = endColor,
EndColor };
return ScriptPlayable<TextGradientPlayableBehaviour>.Create(graph, behaviour);
}
}
構成 4: 構成 3 にミキサーを加える
できること
トラック全体に対して 1 回だけ呼び出されるコールバックを受け取ったり、クリップのブレンディングができます。
できないこと
Timeline 上でキーフレームを使って値をアニメートすることはできません。その場合は構成 5 を検討します。
TrackAsset
でミキサーとなるスクリプトを指定することで、対象のスクリプトがミキサーとして動作するようになります。
using TMPro;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
[TrackClipType(typeof(TextGradientPlayableAsset))]
[TrackBindingType(typeof(TMP_Text))]
[TrackColor(0.7f, 0.2f, 0.7f)]
public class TextGradientTrack : TrackAsset
{
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
{
return ScriptPlayable<TextGradientMixerBehaviour>.Create(graph, inputCount);
}
}
そしてミキサーの本体は以下です。これも PlayableBehaviour
を継承しており、各種コールバックを使用できます。そのため、初期値の復元が可能になります。
using TMPro;
using UnityEngine;
using UnityEngine.Playables;
public class TextGradientMixerBehaviour : PlayableBehaviour
{
? initialColor;
Color;
TMP_Text tmp
// エディタでプレビュー終了時に初期値を復元
public override void OnGraphStop(Playable playable)
{
if (initialColor.HasValue)
{
.color = initialColor.Value;
tmp}
}
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
= playerData as TMP_Text;
tmp if (!tmp) return;
var clipCount = playable.GetInputCount();
var color = new Color();
// エディタで初期値を復元するために、初期値を保全
var firstClip = (ScriptPlayable<TextGradientPlayableBehaviour>)playable.GetInput(0);
var firstBehaviour = firstClip.GetBehaviour();
= firstBehaviour.StartColor;
initialColor
// クリップがないタイミングで無色が適用されてしまうことを防ぐ
var weightSum = CalculateWeightSum(playable);
if (weightSum == 0f) return;
// クリップをミックスして色を作成
for (var i = 0; i < clipCount; i++)
{
var clipWeight = playable.GetInputWeight(i);
var clipPlayable = (ScriptPlayable<TextGradientPlayableBehaviour>)playable.GetInput(i);
var behaviour = clipPlayable.GetBehaviour();
var normalizedTime = (float)(clipPlayable.GetTime() / clipPlayable.GetDuration());
var currentClipColor = Color.Lerp(behaviour.StartColor, behaviour.EndColor, normalizedTime);
+= currentClipColor * clipWeight;
color }
// 色を適用
.color = color;
tmp}
float CalculateWeightSum(Playable playable)
{
var sum = 0f;
var clipCount = playable.GetInputCount();
for (var i = 0; i < clipCount; i++)
{
+= playable.GetInputWeight(i);
sum }
return sum;
}
}
反対に、元々の PlayableBehaviour
は値を持つだけのコンポーネントになります。
using UnityEngine;
using UnityEngine.Playables;
public class TextGradientPlayableBehaviour : PlayableBehaviour
{
public Color StartColor;
public Color EndColor;
}
PlayableAsset
は変更されません。
using UnityEngine;
using UnityEngine.Playables;
[System.Serializable]
public class TextGradientPlayableAsset : PlayableAsset
{
[SerializeField, Header("フェード設定")]
;
Color startColor
[SerializeField]
;
Color endColor
public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
{
var behaviour = new TextGradientPlayableBehaviour
{
= startColor,
StartColor = endColor,
EndColor };
return ScriptPlayable<TextGradientPlayableBehaviour>.Create(graph, behaviour);
}
}
構成 5: PlayableBehaviour をテンプレート化する
Timeline 上で、アニメーションを使ってパラメータを調整できるようになります。
公式の記事で十分なのでそちらを参照して下さい。
参考資料
2024年2月23日