この記事はAdvent Calendar 2018の11日目の記事です。
こんにちは、クライアントエンジニアの楊です。
今回、座標(Vector3)を指定してオブジェクトを移動するUnity TimelineのPlayableを自作してみたので、その実装について書いてみようと思います。
※この記事はGRIPHONE Advent Calendar 2018 11日目の記事です。
https://qiita.com/advent-calendar/2018/griphone
https://adventar.org/calendars/3147
Timelineの機能について
元々のTimelineは単純な機能を提供しています。Default Playablesをインポートして、もっと豊富な機能を利用できます。
Default PlayablesにTransform Tween Trackという機能が含まれています。起点と終点にGameObjectを配置し、Timelineの進行に従って、オブジェクトを起点から終点に移動することができます。移動する度に起点と終点のGameObjectの配置が必要となりますが、場合によって、不便なことになります。
今回は、起点と終点をVector3で指定して、連続移動できるようなTimeline拡張の簡単な実装をしてみました。
Timelineの拡張方法
Timelineの拡張方法は主に三つあります。
- Animation TrackとMonobehaviourをかませて対応
- Control Track
- カスタム Playable Track
今回はカスタムPlayable Trackで実装します。
一個のカスタムPlayable Trackを実現するために、5つのクラスが用意されました。
- Track:オブジェクトのバインド、クリップの種類を決めるクラス
- Clip:クリップのアセット情報
- Behaviour:クリップパラメータの制御
- Mixer:クリップ全体を取得、制御
- Drawer:Behaviourのエディタ拡張、美しく見えるように
実際のスクリプトを書く
- まずはTrackのスクリプトです。
- 移動したいオブジェクトのバインドタイプ(Transform)
- クリップ定義
- トラックの色
- Mixerを生成して、管理すること
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
[TrackColor(1f, 0f, 0.01960784f)]
[TrackClipType(typeof(PlayableTransportClip))]
[TrackBindingType(typeof(Transform))]
public class PlayableTransportTrack : TrackAsset
{
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
{
return ScriptPlayable.Create (graph, inputCount);
}
}
2. 次はClipです。
- 変更できるパラメータを定義する(起点と終点の座標)
using System;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
[Serializable]
public class PlayableTransportClip : PlayableAsset, ITimelineClipAsset
{
public Vector3 StartPos;
public Vector3 EndPos;
public double EndTime { get; set; }
public ClipCaps clipCaps
{
get { return ClipCaps.None; }
}
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<PlayableTransportBehaviour>.Create (graph);
var clone = playable.GetBehaviour ();
clone.StartPos = StartPos;
clone.EndPos = EndPos;
clone.EndTime = EndTime;
return playable;
}
}
3. 次はパラメータの変更処理Behaviourです。
[Serializable]
public class PlayableTransportBehaviour : PlayableBehaviour
{
public Vector3 StartPos;
public Vector3 EndPos;
public double EndTime { get; set; }
// フレーム再生中オブジェクトを移動させる
public void Transport(Transform transform, float rate)
{
var easingValueX = Mathf.Lerp(StartPos.x, EndPos.x, rate);
var easingValueY = Mathf.Lerp(StartPos.y, EndPos.y, rate);
var easingValueZ = Mathf.Lerp(StartPos.z, EndPos.z, rate);
transform.position = new Vector3(easingValueX, easingValueY, easingValueZ);
}
}
4. 最後はクリップ全体制御するMixerBehaviourです。
- トラック全体にわたってProcessFrame()を呼び出す
- トラック全体が見れるので、BehaviourのProcessFrameを使わず、こっちのProcessFrame()を使うべき。
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
public class PlayableTransportMixerBehaviour : PlayableBehaviour
{
// 移動させるパペットのTransform.
private Transform _transformBinding;
// 進捗の最大値.
private const float MaxProgress = 1f;
// Playableを示すので固定値0.
private const int RootPlayable = 0;
// クリップ情報.
protected List<ClipInfo> ClipInfoList;
protected class ClipInfo
{
// PlayableGraphに生成されたPlayableのインスタンス.
public ScriptPlayable<PlayableTransportBehaviour> Playable;
// PlayableBehaviourのインスタンス.
public PlayableTransportBehaviour Behaviour;
// トラック上のクリップの終端.
public double EndTime;
// クリップがSeek済みかどうか.
public bool IsSeeked;
}
private bool HasClips { get; set; }
// グラフの再生時に呼ばれる
public override void OnGraphStart(Playable playable)
{
int inputCount = playable.GetInputCount();
HasClips = inputCount > 0;
if (HasClips)
{
// PlayableGraph開始時に必要なClip情報を事前にリスト化しておく.
ClipInfoList = new List<ClipInfo>();
for (int i = 0; i < inputCount; i++)
{
var instance = (ScriptPlayable<PlayableTransportBehaviour>)playable.GetInput(i);
var data = new ClipInfo
{
Playable = instance,
Behaviour = instance.GetBehaviour(),
EndTime = instance.GetBehaviour().EndTime,
IsSeeked = false
};
ClipInfoList.Add(data);
}
ClipInfoList = ClipInfoList.OrderBy(data => data.EndTime).ToList();
}
}
// グラフの停止時に呼ばれる
public override void OnGraphStop(Playable playable)
{
_transformBinding = null;
ClipInfoList = null;
}
// フレーム再生中
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
_transformBinding = playerData as Transform;
if (_transformBinding != null && HasClips)
{
ProcessFrameTransport();
}
}
// フレームPause
public override void OnBehaviourPause(Playable playable, FrameData info)
{
if (HasClips)
{
var lastClip = ClipInfoList.Last();
if (lastClip.Behaviour != null && _transformBinding != null)
{
lastClip.Behaviour.Transport(_transformBinding, MaxProgress);
}
}
}
// フレームごとにTransportを行う
private void ProcessFrameTransport()
{
var playingClip = ClipInfoList.FirstOrDefault(info => info.Playable.GetPlayState() == PlayState.Playing);
ClipInfoList.ForEach(
info =>
{
var playable = info.Playable;
var behaviour = info.Behaviour;
if (behaviour != null)
{
// SeekがClip外、かつ、まだ一度も該当ClipがSeekされていない、かつ、ほかのClip内で現在Seekしていない.
// つまりTrack上の空白.
var isOver = playable.GetGraph().GetRootPlayable(RootPlayable).GetTime() >= info.EndTime;
if (isOver && info.IsSeeked == false && playingClip == null)
{
// 最終値を強制的に適用.
behaviour.Transport(_transformBinding, MaxProgress);
info.IsSeeked = true;
}
// Seek中のClip、かつ、Behaviourが一致.
else if (playingClip != null && playingClip.Behaviour == behaviour)
{
var progress = GetPlayableProgress(playable);
behaviour.Transport(_transformBinding, progress);
}
}
});
}
// Playableの進捗を取得する
private float GetPlayableProgress(Playable playable)
{
var getTime = playable.GetTime();
var duration = playable.GetDuration();
var progress = Mathf.Clamp01((float)(getTime / duration));
return progress;
}
}
実装結果
Transformの代わりに、座標を指定して、Transform Tween Trackのように実装ができました。
各クリップに起点と終点を指定して、複数の座標に移動することも可能です。
まとめ
今回、座標(Vector3)を指定してオブジェクトを移動するTimeline拡張を実装しました。
連続移動できましたが、クリップ間のブレンド(カーブ移動)が今後の課題となっています。
次回は他のTimelineの拡張実装について書こうと思います。お楽しみに!!