[Unity]Vector3で座標を指定してオブジェクトを移動するTimeline拡張

AvatarPosted by

この記事は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の拡張方法は主に三つあります。

  1. Animation TrackとMonobehaviourをかませて対応
  2. Control Track
  3. カスタム Playable Track

今回はカスタムPlayable Trackで実装します。

一個のカスタムPlayable Trackを実現するために、5つのクラスが用意されました。

  1. Track:オブジェクトのバインド、クリップの種類を決めるクラス
  2. Clip:クリップのアセット情報
  3. Behaviour:クリップパラメータの制御
  4. Mixer:クリップ全体を取得、制御
  5. Drawer:Behaviourのエディタ拡張、美しく見えるように

実際のスクリプトを書く

  1. まずは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の拡張実装について書こうと思います。お楽しみに!!