はじめに
UnityでGUIを構築する際、uGUIや新しい機能であるUI Toolkitが利用できますが、画面遷移のアニメーションのふるまいなどはユーザー側に任せられています。ユーザーで用意するぶん、いくらでもカスタマイズできますが、ギミックやアニメーションの処理を用意するのに手間な部分でもあります。
私はUnityのTimelineについて触れる機会が少なく、これまであまり詳しいことが分かっていませんでしたが、Timelineはスクリプトからの操作やプロパティの変更ができる箇所も多く、カットシーンやイベントシーン以外の用途でもTimelineが利用できそうということが分かりました。今回その一例としてGUIの画面遷移(トランジション)の処理をTimelineを使って行うことを記事に書きました。
今回紹介する内容は個人のGitHubにサンプルのプロジェクトファイルを置いていますので、触っていただけると嬉しいです。
事前準備
今回の記事では以下のUnityのバージョンとパッケージを利用しています。
- Unity 2020.3.25f1
- Timeline 1.4.8
- Input System 1.2.0
- Unity UI 1.0.0
対象のGUIパネルの作成とAnimatorコンポーネントの追加
トランジション効果を持たせたいパネルを作成し、それぞれAnimatorコンポーネントを追加します。これはAnimationとして動かす際に必要になります。Animator ControllerやAvatarはNoneのままにしておきます。
また、以下のセクションで登場するAnimationはTransformを持つGameObjectから作成する必要があり、RectTransformとTransformの原点座標を揃える必要があります。GUIパネルとして利用したいGameObjectのRectTransformの以下のように設定します。
Anchor Presetsは下図のように[Middle Center]にします。(AnchorsのPivotは x: 0.5, y: 0.5に設定)他設定は上図のAnchorsを参考にしてください。
Animationを作成する
まずは遷移元になるAnimationと遷移先になるAnimationをそれぞれ作成します。Projectパネルから任意のディレクトリで右クリック、 Create > Animation から2つほどAnimationを作成します。
それぞれのAnimationに適当な名前を付けたら、Hierarchyに新しくGameObjectを2つ分用意し、先ほど作成したAnimationをD&Dで追加します。
Projectパネルから作成したAnimationの一つをダブルクリックしてアニメーションを編集します。
ここで一度Hierarchyから対象のAnimationが追加されているGameObjectを選択します。この操作をしないと右側のAdd Propertyのボタンが押せないので注意してください。
続けてAdd Propertyを押します。
追加するプロパティは Transform > Position で、Transformの「▷」マークを押すことで候補が出てきます。右側の「+」マークを押して追加します。
デフォルトでキーフレームが0:00の箇所と1:00(1秒相当)の箇所に入っていますので、末尾のキーフレームのマーカーをドラッグしてを0:30の位置に動かします。(ここは任意の時間でも大丈夫です)
キーフレームを上書き編集するため、Recordingボタンを押します。
一つの目のアニメーションは画面外にスライドするアニメーションを作るので、最後のキーフレームを選択します。
横の数値はCanvas Scalerの画面幅を基準に設定します。左側へのスライドのため、マイナスの数値が入っています。下記数値と異なる場合は自身のCanvasScalerの数値を基準に入れてください。
最初のキーフレームの値と最後のキーフレームの値が以下のようになっていればOKです。
もう一方の方は右側の画面外からフレームインするように、終わりのキーフレームの位置を0にするように設定します。
編集が終わったら、使用したGameObjectと、自動生成されたAnimator Controllerは不要になりますので削除します。
Timelineを作成する
次にTimelineを作成します。Projectパネル上で右クリックし、 Create > Timelineを選択します。
名前をわかりやすいものに変更しておきます。
Hierarchyに空のGameObjectを作成し、そのGameObjectにPlayableDirectorのコンポーネントを追加します。
Inspectorから先ほど作成したTimelineをPlayableDirectorのPlayableの箇所にD&Dします。ついでにPlay On Awakeのチェックも外して、実行時自動的にTimelineが再生されないようにします。
さきほど作成したTimelineをダブルクリックしてTimelineのパネルを開きます。
Timelineのパネルが開いたら、一度HierarchyのPlayableDirectorをアタッチしたGameObjectを選択し、Timelineパネルをロックしておきます。
Animation Trackを作成する
Timelineの左側のところで右クリックし、メニューからAnimation Trackを二つ用意します。それぞれ遷移元のGUIパネルのアニメーション、遷移先のGUIパネルのアニメーション用に利用します。
作成したAnimation Trackをリネームします。Timelineの画面上に追加された項目の、枠の部分もしくは、下図のようにアイコンの部分をクリックすることで選択できます。
リネームはInspectorから行います。ついでにApply Avatar Maskの箇所のチェックを外します。
もう一方のAnimation Trackも同じようにリネームします。
AnimationTrackにAnimationを追加する
それぞれのAnimationTrackに作成していたAnimationを追加します。Animationは任意の位置に追加すればよいですが、今回の画面遷移の仕組みではアニメーションの開始と終了がそろっていた方が良いです。
マウスホイールを使ってTimelineの縮尺を変えることができるので、追加しづらいときは縮尺を変えてみてください。
追加したTimelineパネル上のAnimationをクリックし、Inspectorから操作を行います。
Animation Playable Asset内のにある、Remove Start Offsetの項目のチェックを外します。Remove Start Offsetのチェックを外しておくことで、Animationに設定したPositionのアニメーションが絶対位置として反映されるようになります。今回の仕組みには必須です。
※Remove Start Offsetの項目は通常のTransformを持ったGameObjectから作ったAnimationのみ利用できます。GUIのようにRectTransformを持ったものからAnimationを作るとこの項目は表示されないので注意してください。*1
もう一方のTimeline上に配置したAnimationも同じようにRemove Start Offsetのチェックを外しておきます。
アニメーションをチェックする
一度アニメーションの挙動を確認してみます。HierarchyのPlayableDirectorをアタッチしたGameObjectを選択し、Inspectorを操作します。
PlayableDirectorコンポーネントのBindingsの項目にAnimationTrackが追加されています。それぞれの箇所にHierarhy側のトランジションさせたいGUIを持つGameObjectをセットします。
Timelineパネルから再生ボタンを押すか、数字目盛の箇所をドラッグして動きを確認します。
うまく動かないときは、Animationの確認、AnimationTrackの確認、Previewのボタンを押したり、パネルのロックを外して再選択してみるなどの操作を行ってください。
確認が終わったらPlayableDirectorコンポーネントのBindingsから先ほどセットしたGameObjectを外し、Noneに戻しておきます。
SignalAssetを作成する
次にSignalAssetを作成します。このSignalAssetはTimeline上での再生位置、再生終了の検出のために利用します。Projectパネル上で右クリックし、 Create > Signal を選択しSignalAssetを作成します。
二つぶん作り、それぞれ 「Start~」、「End~」という感じに名前を付けます。これはトランジション開始、終了をそれぞれ表すためにつけています。このルールは後で必要になってきます。
次にTimelineにこのSignalAssetをマーカーとして登録します。
マーカーを見えるようにするため、Timelineの数字目盛の箇所を右クリックし、 Show makersを選択し、Markersの項目を表示します。
SignalAssetを追加し、マーカーとして登録します。それぞれ遷移アニメーションの開始位置、終了位置にマーカーを配置します。
SignalReceiverコンポーネントを追加する
PlayableDirectorのComponentが追加されているGameObjectにSignalReceiverコンポーネントを追加しておきます。SignalReiverコンポーネントは追加するのみで、このコンポーネントに対してはInspectorからは何も操作しません。(後でスクリプトで操作できるようにするためです)
トランジションを行うためのスクリプトを追加する
さらに以下のようなスクリプトを作成し、PlayableDirector、SignalReceiverを持っているGameObjectにコンポーネントに追加します。サンプルではMonoBehaviour継承クラスで、TimelineTransitionという名前にしています。
ソースコード全体はこんな感じです。今回本文が長いため、ソースコードの解説は割愛させて頂きます。StartTransition()のメソッドからトランジション効果を呼び出すことができます。
ギミック自体はとても面白いので、Playable APIを含めて今後別の記事で紹介できるかもしれません。
#nullable enable
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Playables;
using UnityEngine.Timeline;
[RequireComponent(typeof(PlayableDirector), typeof(SignalAsset))]
public class TimelineTransition : MonoBehaviour
{
[SerializeField]
private PlayableDirector? playableDirector;
[SerializeField]
private string startMarkerPrefix = "Start";
[SerializeField]
private string stopMarkerPrefix = "End";
[SerializeField]
private string sourceTrackName = "TransitionA";
[SerializeField]
private string destinationTrackName = "TransitionB";
private readonly Dictionary<string, double> startMarkerTimes = new Dictionary<string, double>();
private TrackAsset? sourceTrackAsset;
private TrackAsset? destinationTrackAsset;
public IReadOnlyList<string>? StartMarkers => startMarkerTimes.Keys.Any() ? startMarkerTimes.Keys.ToList() : null;
private GameObject? targetObjectToDisable;
public void StartTransition(
Animator? sourceAnimator,
Animator? destinationAnimator,
string transitionName,
bool disableSourceAfterTransition = false,
bool enableDestinationBeforeTransition = false)
{
if (playableDirector == null) return;
if (playableDirector.state == PlayState.Playing) return;
if (sourceTrackAsset != null && sourceAnimator != null)
{
targetObjectToDisable = disableSourceAfterTransition ? sourceAnimator.gameObject : null;
playableDirector.SetGenericBinding(sourceTrackAsset, sourceAnimator);
}
if (destinationTrackAsset != null && destinationAnimator != null)
{
if (enableDestinationBeforeTransition)
{
destinationAnimator.gameObject.SetActive(true);
}
playableDirector.SetGenericBinding(destinationTrackAsset, destinationAnimator);
}
StartAnimationAtMarker(transitionName);
}
private void StartAnimationAtMarker(string markerName)
{
if (startMarkerTimes.ContainsKey(markerName))
{
if (playableDirector == null) return;
playableDirector.time = startMarkerTimes[markerName];
playableDirector.Play();
}
}
#region MonoBehaviour implements
private void Reset()
{
playableDirector = gameObject.GetComponent<PlayableDirector>();
}
private void Awake()
{
if (playableDirector == null) return;
var stopTimelineEvent = new UnityEvent();
stopTimelineEvent.AddListener(() =>
{
targetObjectToDisable?.SetActive(false);
targetObjectToDisable = null;
playableDirector?.Stop();
});
var signalReceiver = gameObject.GetComponent<SignalReceiver>();
var timelineAsset = playableDirector?.playableAsset as TimelineAsset;
foreach (var marker in timelineAsset!.markerTrack.GetMarkers())
{
var signalEmitter = marker as SignalEmitter;
if (signalEmitter == null) continue;
var signalAsset = signalEmitter.asset;
if (signalAsset.name.Contains(startMarkerPrefix))
{
startMarkerTimes.Add(signalAsset.name, marker.time);
}
if (signalAsset.name.Contains(stopMarkerPrefix))
{
if (!signalReceiver.GetRegisteredSignals().Contains(signalAsset))
{
signalReceiver.AddReaction(signalAsset, stopTimelineEvent);
}
}
}
foreach (var trackAsset in timelineAsset!.GetRootTracks())
{
if (trackAsset as AnimationTrack == null) continue;
if (trackAsset.name.Contains(sourceTrackName))
{
sourceTrackAsset = trackAsset;
}
if (trackAsset.name.Contains(destinationTrackName))
{
destinationTrackAsset = trackAsset;
}
}
}
#endregion
}
それぞれのプロパティは下図のような関係持っています。
Start Maker Prefixはアニメーション開始のマーカーを識別するためのキーワード(接頭辞)、Stop Marker Prefixはアニメーション停止のマーカーを識別するためのキーワードとして設定します。このマーカーはSignalAssetの名前を基準にして行っています。
SourceTrackNameとDestinationTrackNameはそれぞれアニメーションをさせたいAnimationTrackの名前を文字列で指定しています。
トランジション効果を呼び出す処理を追加する
各GUIのボタンに相当するGameObjectにスクリプトを追加します。DestinationAnimatorに切り替え先のパネルをアタッチします。
TransitionControllerのソースコードはこんな感じです。現在ボタンに設定されている親のGameObjectのAnimatorをsourceAnimatorに指定しています。GUIボタンからはOnButtonClicked()メソッドから呼び出すように定義しています。
timelineTransition?.StartTransition()でトランジション効果を呼び出します。transitionNameのプロパティに指定されている"StartTransition"はTimelineのマーカー名で、アニメーション開始位置として設定しているSignalAssetの名前を入れています。
#nullable enable
using UnityEngine;
public class TransitionController : MonoBehaviour
{
[SerializeField]
private Animator? destinationAnimator;
[SerializeField]
private string transitionName = "StartTransition";
private Animator? sourceAnimator;
private TimelineTransition? timelineTransition;
public void OnButtonClicked()
{
if (timelineTransition != null && sourceAnimator != null && destinationAnimator != null)
{
if (sourceAnimator == destinationAnimator) return;
StartTransition(sourceAnimator, destinationAnimator);
}
}
private void StartTransition(in Animator source, in Animator destination)
{
timelineTransition?.StartTransition(source, destination, transitionName);
}
#region MonoBehaviour implements
private void Start()
{
timelineTransition = FindObjectOfType<TimelineTransition>();
var currentTransform = transform;
while (currentTransform.parent != null)
{
var parent = currentTransform.parent.gameObject;
var animator = parent.GetComponent<Animator>();
if (animator != null)
{
sourceAnimator = animator;
break;
}
currentTransform = currentTransform.parent;
}
}
#endregion
}
ButtonコンポーネントのOnClickイベントに上記スクリプトのメソッドを忘れず追加しておきます。
他のボタンコンポーネントも同じように、遷移先のGUIパネルを設定します。
できたもの
以下の動画が今回できたものになります。
また、今回の記事には解説していませんが、GitHubサンプルには他の例も入れています。
終わりに
以前からUnityのTimelineはPlayableTrack をはじめとした独自拡張が可能で、いろいろな用途に応用できそうなのは分かっていましたが、今回改めてその拡張性や新たな可能性を感じることができました!
TimelineそのものはPlayable APIの機能を使っているようなので、こちらへの理解も進めるとさらに広く応用が利きそうです。
お知らせ
弊社では現在様々な職種を募集しています。興味がある方は是非、以下のページを覗いてみてください!
付録
*1:RectTransformからAnimationを作ろうとする場合、Add Propertyの項目がTransformではなく、RectTransformになっています。