はじめに
これは『Unityゆるふわサマーアドベントカレンダー2019』の初日の記事です。今回はUnityのTimelineに関するちょっと変わった使い方の紹介をしたいと思います。
UnityのTimelineはマルチトラックに演出等を作成するための標準機能で、主にゲーム中のムービーシーン・カットシーンの作成に役立つものです。標準のAnimationと異なり、復数のアセットを同期的に動かすことが出来るため、複雑な演出をほとんどコードを書かずに実現することが出来ます。
そんな優れたTimelineですが、基本的にはその名の通り時系列に沿った演出が主になります。しかし、コンテンツによってクリップの長さを可変にすることで出来る表現があるのではないかと思いました。そこで今回はオーディオの長さに応じて自動的に再生位置を調整するTrackを実装してみました。
セリフを読み終わるまでループで待つTrack
こちらのリポジトリを改変する形で作りました。
参考: tsubaki/Timeline-Loop
© UTJ/UCLこれはブログ埋め込み用の動画です pic.twitter.com/mgtmNskpSW
— Nakaji Kohki / リリカちゃん (@nkjzm) July 31, 2019
音声が再生し終わるまで、LoopClip
の上を繰り返しシークバーが移動している様子が確認出来ると思います。
実装
ほぼほぼtsubakiさんのままですが、折角なので解説込みで紹介します。
まずTimelineの基本的な構成の話です。イメージとしてはTrackAsset
という枠を用意して、その中にPlayableAsset
というクリップを配置します。そしてPlayableAsset
の位置に合わせてPlayableBehaviour
のライフサイクルが回りだす感じです。PlayableBehaviour
は実行時に生成されるため、Hierarchy上の参照等はPlayableAsset
から受け渡してもらう必要があります。
主に3種類のスクリプトに分かれているため一見複雑ですが、PlayableBehaviour
を動かすためにゴニョゴニョ準備をしている、というイメージがわかりやすいと思います。
参考: そろそろUnity2017のTimelineの基礎を押さえておこう - 渋谷ほととぎす通信
ではPlayableBehaviour
を継承したこのクラスから紹介していきます。
using System;
using UnityEngine;
using UnityEngine.Playables;
[Serializable]
public class LoopBehaviour : PlayableBehaviour
{
public PlayableDirector director { get; set; }
public WaitTimeline waitTimeline { get; set; }
public AudioSource audioSource { get; set; }
public AudioClip audioClip { get; set; }
public override void OnBehaviourPlay(Playable playable, FrameData info)
{
if (!audioSource.isPlaying)
{
audioSource.PlayOneShot(audioClip, 1);
}
}
float timer = 0f;
public override void PrepareFrame(Playable playable, FrameData info)
{
timer += Time.deltaTime;
if (timer < audioClip.length)
{
return;
}
waitTimeline.trigger = true;
}
public override void OnBehaviourPause(Playable playable, FrameData info)
{
if (waitTimeline.trigger == true)
{
waitTimeline.trigger = false;
return;
}
director.time -= playable.GetDuration();
}
}
PlayableBehaviour
はタイミングに応じていくつかの関数が呼び出されます。今回のイメージとしては以下の様になっています。
まずはPlayableAsset
の再生時に実行されるOnBehaviourPlay()
では音声の再生をしています。
public override void OnBehaviourPlay(Playable playable, FrameData info)
{
if (!audioSource.isPlaying)
{
audioSource.PlayOneShot(audioClip, 1);
}
}
続くPrepareFrame()
は毎フレーム呼ばれる関数で、経過時間の計測を行なっています。また、経過時間がaudioClip
の長さを超えた時にtrigger
をTrueにしています。
public override void PrepareFrame(Playable playable, FrameData info)
{
timer += Time.deltaTime;
if (timer < audioClip.length)
{
return;
}
waitTimeline.trigger = true;
}
最後のOnBehaviourPause()
は名前からは想像しづらいのですが、ポーズした時だけでなくPlayableAsset
の最後のフレームでも呼び出される関数です。この時にtrigger
がTrue、つまり再生が終わっていたらそのままにし、終わっていなければTimelineの時間を巻き戻しています。
public override void OnBehaviourPause(Playable playable, FrameData info)
{
if (waitTimeline.trigger == true)
{
waitTimeline.trigger = false;
return;
}
director.time -= playable.GetDuration();
}
LoopBehaviour
を実行するための参照はPlayableAsset
を継承したクラスで行なっています。Hierarchy上の参照に関してはCreatePlayable(PlayableGraph graph, GameObject owner)
のowner
、つまりPlayableDirectorコンポーネントを持つGameObjectを経由して取得しています。
using System;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
[Serializable]
public class LoopClip : PlayableAsset, ITimelineClipAsset
{
public ClipCaps clipCaps { get { return ClipCaps.None; } }
[SerializeField]
AudioClip audioClip = null;
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<LoopBehaviour>.Create(graph);
LoopBehaviour beheviour = playable.GetBehaviour();
beheviour.director = owner.GetComponent<PlayableDirector>();
beheviour.waitTimeline = owner.GetComponent<WaitTimeline>();
beheviour.audioSource = owner.GetComponent<AudioSource>();
beheviour.audioClip = audioClip;
return playable;
}
}
再生するAudioClip
だけはPlayableAsset
毎に異なるため、PlayableAsset
のInspector上から指定する形式にしています。
最後はPlayableAsset
を配置するためのTrackAsset
の実装です。これは単に継承してTrackClipType
で紐付けをするだけでした。
using UnityEngine.Timeline;
[TrackColor(1f, 0.2794118f, 0.7117646f)]
[TrackClipType(typeof(LoopClip))]
public class LoopTrack : TrackAsset
{
}
LoopClip配置時のコツ
ループ時には再生位置が巻き戻るため、再生中のアニメーションが不自然に切り替わってしまう発生しました。
今回は再生するAudioClip
の長さの等倍の感覚で再生することで、切り替わり時の違和感を軽減させられました。
最後に
元々はTimelineでノベルエディタのようなようなものを作りたくて検証しはじめました。Timelineのダイナミックな演出と長さが異なるセリフの効率的な管理を両立できると思ったのですが、ループ中の制約がかなり厳しいことに気が付きました。動的に出来るメリットとしては他にも、音声を外部から参照できる形にして、ユーザーがセリフを差し替えられるツールなども作れると思います。Default Playablesを使えば字幕なども出すことが出来るので、もしやる気があったら作ったみたいと思います。
そんな感じのゆるゆわ記事でした。明日の『Unityゆるふわサマーアドベントカレンダー2019』は@pCYSl5EDgoさんによる「LINQ to NativeArrayについてなんか書く」です。お楽しみに。