Synamon’s Engineer blog

Synamonは「XRが当たり前の世界をつくる」ことをミッションに、未来づくりに挑戦しているXRスタートアップです。 toB領域をメインに、未来への取り組みや事業づくりにチャレンジしている企業様への支援や、XR・エンタープライズメタバース活用のユースケースづくり、継続的に使われるための仕組みづくりに取り組んでいます。 このブログでは、XR・メタバース技術とその周辺の技術、開発全般に関してエンジニアがお話しします。

UnityのTimelineを利用してGUIのトランジション効果を作ってみる

f:id:fb8r5jymw6fd:20211226110009p:plain

はじめに

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のままにしておきます。

f:id:fb8r5jymw6fd:20211226095217p:plain

 

また、以下のセクションで登場するAnimationはTransformを持つGameObjectから作成する必要があり、RectTransformとTransformの原点座標を揃える必要があります。GUIパネルとして利用したいGameObjectのRectTransformの以下のように設定します。

Anchor Presetsは下図のように[Middle Center]にします。(AnchorsのPivotは x: 0.5, y: 0.5に設定)他設定は上図のAnchorsを参考にしてください。

f:id:fb8r5jymw6fd:20211226095030p:plain

 

Animationを作成する

まずは遷移元になるAnimationと遷移先になるAnimationをそれぞれ作成します。Projectパネルから任意のディレクトリで右クリック、 Create > Animation から2つほどAnimationを作成します。

f:id:fb8r5jymw6fd:20211225173855p:plain

 

それぞれのAnimationに適当な名前を付けたら、Hierarchyに新しくGameObjectを2つ分用意し、先ほど作成したAnimationをD&Dで追加します。

f:id:fb8r5jymw6fd:20211226112331p:plain

 

Projectパネルから作成したAnimationの一つをダブルクリックしてアニメーションを編集します。

f:id:fb8r5jymw6fd:20211225175957p:plain

 

ここで一度Hierarchyから対象のAnimationが追加されているGameObjectを選択します。この操作をしないと右側のAdd Propertyのボタンが押せないので注意してください。

続けてAdd Propertyを押します。

f:id:fb8r5jymw6fd:20211225180325p:plain


追加するプロパティは Transform > Position で、Transformの「▷」マークを押すことで候補が出てきます。右側の「+」マークを押して追加します。

f:id:fb8r5jymw6fd:20211225180711p:plain

 

デフォルトでキーフレームが0:00の箇所と1:00(1秒相当)の箇所に入っていますので、末尾のキーフレームのマーカーをドラッグしてを0:30の位置に動かします。(ここは任意の時間でも大丈夫です)

f:id:fb8r5jymw6fd:20211225180905p:plain

 

キーフレームを上書き編集するため、Recordingボタンを押します。

f:id:fb8r5jymw6fd:20211225181057p:plain

 

一つの目のアニメーションは画面外にスライドするアニメーションを作るので、最後のキーフレームを選択します。

f:id:fb8r5jymw6fd:20211225181328p:plain

 

横の数値はCanvas Scalerの画面幅を基準に設定します。左側へのスライドのため、マイナスの数値が入っています。下記数値と異なる場合は自身のCanvasScalerの数値を基準に入れてください。

f:id:fb8r5jymw6fd:20211225181529p:plain

f:id:fb8r5jymw6fd:20211225181800p:plain

 

最初のキーフレームの値と最後のキーフレームの値が以下のようになっていればOKです。

f:id:fb8r5jymw6fd:20211225182301p:plain

 

もう一方の方は右側の画面外からフレームインするように、終わりのキーフレームの位置を0にするように設定します。

f:id:fb8r5jymw6fd:20211225184140p:plain

 

編集が終わったら、使用したGameObjectと、自動生成されたAnimator Controllerは不要になりますので削除します。

f:id:fb8r5jymw6fd:20211225184415p:plain

f:id:fb8r5jymw6fd:20211225184548p:plain

 

Timelineを作成する

次にTimelineを作成します。Projectパネル上で右クリックし、 Create > Timelineを選択します。

f:id:fb8r5jymw6fd:20211225184649p:plain

 

名前をわかりやすいものに変更しておきます。

f:id:fb8r5jymw6fd:20211226103050p:plain

 

Hierarchyに空のGameObjectを作成し、そのGameObjectにPlayableDirectorのコンポーネントを追加します。

f:id:fb8r5jymw6fd:20211226065048p:plain

 

Inspectorから先ほど作成したTimelineをPlayableDirectorのPlayableの箇所にD&Dします。ついでにPlay On Awakeのチェックも外して、実行時自動的にTimelineが再生されないようにします。

f:id:fb8r5jymw6fd:20211226065423p:plain

 

さきほど作成したTimelineをダブルクリックしてTimelineのパネルを開きます。

f:id:fb8r5jymw6fd:20211226064821p:plain

 

Timelineのパネルが開いたら、一度HierarchyのPlayableDirectorをアタッチしたGameObjectを選択し、Timelineパネルをロックしておきます。

f:id:fb8r5jymw6fd:20211226065725p:plain

f:id:fb8r5jymw6fd:20211226065855p:plain

 

Animation Trackを作成する

Timelineの左側のところで右クリックし、メニューからAnimation Trackを二つ用意します。それぞれ遷移元のGUIパネルのアニメーション、遷移先のGUIパネルのアニメーション用に利用します。

f:id:fb8r5jymw6fd:20211226065944p:plain

 

作成したAnimation Trackをリネームします。Timelineの画面上に追加された項目の、枠の部分もしくは、下図のようにアイコンの部分をクリックすることで選択できます。

f:id:fb8r5jymw6fd:20211226070353p:plain

 

リネームはInspectorから行います。ついでにApply Avatar Maskの箇所のチェックを外します。

f:id:fb8r5jymw6fd:20211226070547p:plain

 

もう一方のAnimation Trackも同じようにリネームします。

f:id:fb8r5jymw6fd:20211226070649p:plain

 

AnimationTrackにAnimationを追加する

それぞれのAnimationTrackに作成していたAnimationを追加します。Animationは任意の位置に追加すればよいですが、今回の画面遷移の仕組みではアニメーションの開始と終了がそろっていた方が良いです。

マウスホイールを使ってTimelineの縮尺を変えることができるので、追加しづらいときは縮尺を変えてみてください。

f:id:fb8r5jymw6fd:20211226071107p:plain

 

追加したTimelineパネル上のAnimationをクリックし、Inspectorから操作を行います。

f:id:fb8r5jymw6fd:20211226071359p:plain

 

Animation Playable Asset内のにある、Remove Start Offsetの項目のチェックを外します。Remove Start Offsetのチェックを外しておくことで、Animationに設定したPositionのアニメーションが絶対位置として反映されるようになります。今回の仕組みには必須です。

Remove Start Offsetの項目は通常のTransformを持ったGameObjectから作ったAnimationのみ利用できます。GUIのようにRectTransformを持ったものからAnimationを作るとこの項目は表示されないので注意してください。*1

f:id:fb8r5jymw6fd:20211226072507p:plain

もう一方のTimeline上に配置したAnimationも同じようにRemove Start Offsetのチェックを外しておきます。

 

アニメーションをチェックする

一度アニメーションの挙動を確認してみます。HierarchyのPlayableDirectorをアタッチしたGameObjectを選択し、Inspectorを操作します。
f:id:fb8r5jymw6fd:20211226065725p:plain

 

PlayableDirectorコンポーネントのBindingsの項目にAnimationTrackが追加されています。それぞれの箇所にHierarhy側のトランジションさせたいGUIを持つGameObjectをセットします。

f:id:fb8r5jymw6fd:20211226073320p:plain

 

Timelineパネルから再生ボタンを押すか、数字目盛の箇所をドラッグして動きを確認します。

f:id:fb8r5jymw6fd:20211226074032p:plain

f:id:fb8r5jymw6fd:20211226134942g:plain

 

うまく動かないときは、Animationの確認、AnimationTrackの確認、Previewのボタンを押したり、パネルのロックを外して再選択してみるなどの操作を行ってください。

f:id:fb8r5jymw6fd:20211226073846p:plain

 

確認が終わったらPlayableDirectorコンポーネントのBindingsから先ほどセットしたGameObjectを外し、Noneに戻しておきます。

f:id:fb8r5jymw6fd:20211226074955p:plain

 

SignalAssetを作成する

次にSignalAssetを作成します。このSignalAssetはTimeline上での再生位置、再生終了の検出のために利用します。Projectパネル上で右クリックし、 Create > Signal を選択しSignalAssetを作成します。

f:id:fb8r5jymw6fd:20211226075220p:plain

 

二つぶん作り、それぞれ 「Start~」、「End~」という感じに名前を付けます。これはトランジション開始、終了をそれぞれ表すためにつけています。このルールは後で必要になってきます。

f:id:fb8r5jymw6fd:20211226075637p:plain

 

次にTimelineにこのSignalAssetをマーカーとして登録します。

マーカーを見えるようにするため、Timelineの数字目盛の箇所を右クリックし、 Show makersを選択し、Markersの項目を表示します。

f:id:fb8r5jymw6fd:20211226082002p:plain

 

SignalAssetを追加し、マーカーとして登録します。それぞれ遷移アニメーションの開始位置、終了位置にマーカーを配置します。

f:id:fb8r5jymw6fd:20211226082517p:plain

 

SignalReceiverコンポーネントを追加する

PlayableDirectorのComponentが追加されているGameObjectにSignalReceiverコンポーネントを追加しておきます。SignalReiverコンポーネントは追加するのみで、このコンポーネントに対してはInspectorからは何も操作しません。(後でスクリプトで操作できるようにするためです)

f:id:fb8r5jymw6fd:20211226082937p:plain

 

トランジションを行うためのスクリプトを追加する

さらに以下のようなスクリプトを作成し、PlayableDirector、SignalReceiverを持っているGameObjectにコンポーネントに追加します。サンプルではMonoBehaviour継承クラスで、TimelineTransitionという名前にしています。

f:id:fb8r5jymw6fd:20211226091726p:plain

 

ソースコード全体はこんな感じです。今回本文が長いため、ソースコードの解説は割愛させて頂きます。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の名前を文字列で指定しています。

f:id:fb8r5jymw6fd:20211226094152p:plain

 

トランジション効果を呼び出す処理を追加する

各GUIのボタンに相当するGameObjectにスクリプトを追加します。DestinationAnimatorに切り替え先のパネルをアタッチします。

f:id:fb8r5jymw6fd:20211226104146p:plain

 

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イベントに上記スクリプトのメソッドを忘れず追加しておきます。

f:id:fb8r5jymw6fd:20211226105424p:plain

 

他のボタンコンポーネントも同じように、遷移先のGUIパネルを設定します。

 

できたもの

以下の動画が今回できたものになります。

f:id:fb8r5jymw6fd:20211226125314g:plain

 

また、今回の記事には解説していませんが、GitHubサンプルには他の例も入れています。

f:id:fb8r5jymw6fd:20211226125358g:plain

 

終わりに

以前からUnityのTimelineはPlayableTrack をはじめとした独自拡張が可能で、いろいろな用途に応用できそうなのは分かっていましたが、今回改めてその拡張性や新たな可能性を感じることができました!

TimelineそのものはPlayable APIの機能を使っているようなので、こちらへの理解も進めるとさらに広く応用が利きそうです。

お知らせ

弊社では現在様々な職種を募集しています。興味がある方は是非、以下のページを覗いてみてください!

twitter.com

meety.net

herp.careers

 

付録

*1:RectTransformからAnimationを作ろうとする場合、Add Propertyの項目がTransformではなく、RectTransformになっています。

f:id:fb8r5jymw6fd:20211226101751p:plain