Timelineアニメーションの移動先を外部から指定する(その2、Unityメモ)
その1からの続きです。字数だけ見るとすごいことになっていますが、ほとんどコードの分です。
前回のまとめ:PlayableDirectorにキーと値を設定してデータを渡す
Timeline上でユニットを固定の目標地点ではなく任意のターゲットに向かって動かすため、Transform Tween Trackを拡張します。
やりたいことは以下の通りです。
・動かすGameObject(行動者とかターゲット)とは別に、動きのパラメータとして地点を指すGameObjectを指定
・シーンから切り離されたアセットに対してシーン上のオブジェクトを与える
・実行時にその都度シーン上のオブジェクトを与える
そのためにPlayableDirectorにキーと値を設定する方法をとり、PlayableDirectorを介してデータを渡すことにしました。
テスト用にPlayableDirectorにキーと値を設定するためのコンポーネントを別に実装しました。実際のゲームでは、ゲームシステムがPlayableDirectorにアクセスすることになります。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
// PlayableDirectorが保持するExposedReferenceのキーと値を外部から設定。
// Clip側でGetReferenceValueすることで、Timeline外部から与えた値を取得できる。
public class ReferenceValueSetter : MonoBehaviour
{
//テスト用にペア数を固定
//public Dictionary<PropertyName, Object> RefValue;
public PropertyName id1;
public Transform value1;
public PropertyName id2;
public Transform value2;
private PlayableDirector m_Director;
// Start is called before the first frame update
void Start()
{
m_Director = gameObject.GetComponent<PlayableDirector>();
//値をセット
m_Director.SetReferenceValue(id1, value1);
m_Director.SetReferenceValue(id2, value2);
}
}
Transform Tween Trackを拡張する
Clip, Track, BoundObjectの関係
Transform Tween Trackの拡張では、Timeline上のオブジェクトを扱う必要があります。特にトラック・クリップ・バウンドオブジェクト(Timelineで動かすオブジェクト。Bindされたオブジェクト)の関係を知ることが重要でした。以下の記事内の図がわかりやすかったです。
ClipとそのPlayableBehaviour、TrackとそのPlayableBehaviour
Transform Tween Trackの具体的な拡張方法は、まさに公式のTransform Tween Trackの解説記事を参考にしました。カスタムトラック全般の作り方も順を追ってつかめます。
要するに、ClipとそのPlayableBehaviourを実装し、さらにTrackとそのPlayableBehaviourを実装する、ということです。ClipやTrackはインスタンスの生成を中心に実装し、PlayableBehaviourの方に実際の動きの処理を書きます。いずれもイベントハンドラを実装する形で書きます。
ClipのPlayableBehaviourとTrackのPlayableBehaviourの使い分けですが、1クリップ内で完結する処理はClip用のPlayableBehaviourに書き、クリップのブレンド(モーフィング)など、クリップが重なる場合の処理はTrack用のPlayableBehaviourに書きます。
バウンドオブジェクトの取得
ただ、上の記事にはTimeline側からバウンドオブジェクト(動かすオブジェクト)を取得する方法が載っていないので調べました。
バウンドオブジェクトはTrackやClipの生成メソッド内で取得します。PlayableAsset.CreatePlayable()、TrackAsset.CreateTrackMixer() がそれぞれの生成メソッドです。
生成メソッドの第二引数で与えられるGameObjectがPlayableDirectorの親となるGameObjectとなっています。そこからPlayableDirectorを参照し、PlayableDirector.GetGenericBinding()でバウンドオブジェクトを取得します。トラックが複数ある場合は、トラック名をキーとして指定します。なのでトラック名は規約として決めておく必要があります。
取得したバウンドオブジェクトは、TrackだけでなくPlayableBehaviourのメンバにも定義・設定し、PlayableBehaviourの処理で使用できるようにしておきます。
なお、初期化処理は以下の順で行われます。実装の説明もこの順で行います。
1. TrackのCreateTrackMixer()
2. ClipのCreatePlayable()
3. TrackのPlayableBehaviourのOnGraphStart()
4. ClipのPlayableBehaviourのOnGraphStart()
Trackの初期化処理
まずTrackの初期化処理の実装です。Track内で生成したPlayableBehaviourに、PlayableDirectorから取得したバウンドオブジェクトを設定します。
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
/* Timeline外部からGameObjectを基準位置として与えてTweenする。Unityのサンプルから改変
* https://docs.unity3d.com/Packages/com.unity.timeline@1.8/manual/smpl_custom_tween.html
*/
[TrackBindingType(typeof(Transform))]
[TrackClipType(typeof(InjectionTweenClip))]
public class InjectionTweenTrack : TrackAsset
{
/*
* インスタンス生成
*/
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
{
var playable = ScriptPlayable<InjectionTweenMixerBehaviour>.Create(graph, inputCount);
// 動かすオブジェクトをtrackBindingを通して取得し、InjectionTweenMixerBehaviourにセットする。
// https://qiita.com/00riono/items/11b32c537b67fb6dd6c5 を参考に実装
var director = go.GetComponent<PlayableDirector>();
if (director is not null)
{
var trackBinding = director.GetGenericBinding(this) as Transform;
playable.GetBehaviour().trackBinding = trackBinding;
}
return playable;
}
}
ClipからのPlayableBehaviourの生成と初期化
Clipオブジェクトの役割はPlayableBehaviourオブジェクトを生成して初期化することと、そのためのパラメータを保持することです。シリアライズもClipのインスタンスを対象とします。Clipの初期化処理は、Clip自体の初期化というよりは、Trackに紐づくPlayableBehaviourを初期化することがメインです。
なお開始地点・終了地点の指定について、当初はキーだけを持たせていたのですが、結局はGameObjectを直接持つExposedReferenceも追加しました。というのも、エディタ編集時にはPlayableDirectorが生成されていないため、キーを介してGameObjectを得ることができません。そのため、エディタ編集時のプレビューではExposedReferenceから地点を指すGameObjectを参照し、ゲーム実行時はキーを使ってPlayableDirectorからオブジェクトを取得するようにしました。
結局はExposedReferenceも持たせることにしたのですが、そうすると規約で決め打ちするIDとは別に、演出データ内部だけで使える途中地点用のGameObjectを自由に追加できるので、そこまで悪くはないのかなと。
コードは以下の通りにしました。
using UnityEngine;
using UnityEngine.Playables;
/* PlayableDirectorはExposedReferenceのキーと値のペアを保持している。
* あらかじめ外部コンポーネントからPlayableDirectorにIDとGameObjectを登録しておき、
* Clip生成時に所定のIDを使って対応するGameObjectを得る。
*/
[System.Serializable]
public class InjectionTweenClip : PlayableAsset
{
/*
* PlayableBehaviourにパラメータを設定するためのプレースホルダ
*/
//基準位置のGameObjectを参照するためのID
public PropertyName startID;
public PropertyName endID;
public bool shouldTweenPosition;
public bool shouldTweenRotation;
public bool shouldTweenScale;
public AnimationCurve curve;
//Editor時のみ有効
[SerializeField]
private ExposedReference<Transform> startRef;
[SerializeField]
private ExposedReference<Transform> endRef;
/*
* インスタンス生成
*/
// Factory method that generates a playable based on this asset
public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
{
// create a new behaviour
var playable = ScriptPlayable<InjectionTweenBehaviour>.Create(graph);
var tween = playable.GetBehaviour();
tween.startID = startID;
tween.endID = endID;
// ExposedReferenceを直接読む。Editor時のみ有効
tween.startLocation = startRef.Resolve(graph.GetResolver());
tween.endLocation = endRef.Resolve(graph.GetResolver());
// set the behaviour member
tween.shouldTweenPosition = shouldTweenPosition;
tween.shouldTweenRotation = shouldTweenRotation;
tween.shouldTweenScale = shouldTweenScale;
tween.curve = curve;
return playable;
}
}
TrackのPlayableBehaviourの処理
OnGraphStart()で初期化処理をし、PrepareFrame()でフレームごとの更新処理、主にクリップ間のブレンド処理を行います。(開始地点と終了地点の間のブレンドはClip側で実装、後述)
OnGraphStop()の処理ですが、プレビュー後に初期位置に戻しています。特にプレビュー時に手でカーソルを動かした後にゲームモードで再生すると、そのままだと再生時にあった位置で初期位置を上書きされてしまうため、OnGraphStopの中で初期位置を直接再設定しています。TrackオブジェクトでのGatherProperties()も試してみたのですが、あまり上手く動かなかったので上のコードからは外しています。
クリップ間のブレンドは単純に重みの線形和、つまり線形補間にしています。ブレンドカーブを適用できると演出の幅が広がりそう。
ちなみに、回転のブレンドはクォータニオンを回転軸と回転量に変換して普通の線形補間をしています。クリップごとに反復して加算できるような球面線形補間の計算方法がなさそうなので…
using UnityEngine;
using UnityEngine.Playables;
// A behaviour that is attached to a playable
public class InjectionTweenMixerBehaviour : PlayableBehaviour
{
public Transform trackBinding { get; set; } //動かすオブジェクトをTrackからセットする
private Vector3 init_pos;
private Vector3 init_scale;
private Quaternion init_rot;
private float init_rot_angle; //クォータニオンを線形補間するための一時変数
private Vector3 init_rot_axis; //クォータニオンを線形補間するための一時変数
// Called when the owning graph starts playing
public override void OnGraphStart(Playable playable)
{
//初期値を設定
init_pos = trackBinding.localPosition;
init_scale = trackBinding.localScale;
init_rot = trackBinding.localRotation;
init_rot.ToAngleAxis(out init_rot_angle, out init_rot_axis);
//Track上の全Clipについて補間
int inputCount = playable.GetInputCount(); //Clipの個数
for (int i = 0; i < inputCount; i++)
{
// get the input connected to the mixer
// Playableは構造体のため、as演算子は使えない
ScriptPlayable<InjectionTweenBehaviour> input = (ScriptPlayable<InjectionTweenBehaviour>)playable.GetInput(i);
InjectionTweenBehaviour tweenInput = input.GetBehaviour();
if (tweenInput is null) continue;
tweenInput.trackBinding = trackBinding;
}
}
// Called when the owning graph stops playing
public override void OnGraphStop(Playable playable)
{
//初期値に戻す
#if UNITY_EDITOR
trackBinding.localPosition = init_pos;
trackBinding.localScale = init_scale;
trackBinding.localRotation = init_rot;
#endif
}
// Called each frame while the state is set to Play
// TrackのPlayableBehaviourの場合、playableはTrackを表す
public override void PrepareFrame(Playable playable, FrameData info)
{
Vector3 diff_pos = Vector3.zero;
Vector3 diff_scale = Vector3.zero;
//クォータニオンを線形補間するための一時変数
float diff_rot_angle = 0.0f;
Vector3 diff_rot_axis = Vector3.zero;
//Track上の全Clipについて補間
int inputCount = playable.GetInputCount(); //Clipの個数
for (int i = 0; i < inputCount; i++)
{
// get the input connected to the mixer
// Playableは構造体のため、as演算子は使えない
ScriptPlayable<InjectionTweenBehaviour> input = (ScriptPlayable<InjectionTweenBehaviour>)playable.GetInput(i);
// get the weight of the connection
// Clip同士でブレンドされる場合は、それぞれのClipの重みが(0, 1)の間の値になる。またClip間のinputWeightの総和は1.0
float inputWeight = playable.GetInputWeight(i);
if (inputWeight <= 0.0f) continue;
// get the clip's behaviour
InjectionTweenBehaviour tweenInput = input.GetBehaviour();
if (tweenInput is null) continue;
// クリップの内部状態に対して重みをかける
if (tweenInput.shouldTweenPosition)
{
diff_pos += inputWeight * tweenInput.diff_pos;
}
if (tweenInput.shouldTweenScale)
{
diff_scale += inputWeight * tweenInput.diff_scale;
}
if (tweenInput.shouldTweenRotation)
{
//クリップ間の重みをかけて加算=線形補間(lerp)
//TODO: 逐次的に球面線形補間(slerp)できる方法があれば採用
float lerp_rot_angle;
Vector3 lerp_rot_axis;
tweenInput.diff_rot.ToAngleAxis(out lerp_rot_angle, out lerp_rot_axis);
diff_rot_angle += inputWeight * lerp_rot_angle;
diff_rot_axis += inputWeight * lerp_rot_axis;
}
}
//動かすオブジェクトの位置を変更
trackBinding.localPosition = init_pos + diff_pos;
trackBinding.localScale = init_scale + diff_scale;
trackBinding.localRotation = Quaternion.AngleAxis(diff_rot_angle, diff_rot_axis);
}
}
ClipのPlayableBehaviourの処理
オブジェクトの初期化処理や、1クリップ内で完結するフレームごとの処理を書きます。開始地点と終了地点の間のTransformのブレンドはこのオブジェクトで行います。
初期化処理では、Clipから与えられたIDを使って対応するGameObjectを取得し、始点と終点の情報を設定します。
開始地点と終了地点とのブレンドは線形補間にしています。カーブに対応する場合はここにカーブを適用します。ここでの回転の補間は球面線形補間を使っています。
using UnityEngine;
using UnityEngine.Playables;
// A behaviour that is attached to a playable
public class InjectionTweenBehaviour : PlayableBehaviour
{
public Transform startLocation;
public Transform endLocation;
public PropertyName startID;
public PropertyName endID;
public Transform trackBinding { get; set; } //動かすオブジェクトをTrackからセットする
public bool shouldTweenPosition;
public bool shouldTweenRotation;
public bool shouldTweenScale;
public AnimationCurve curve;
//Timeline再生時の変化量
public Vector3 diff_pos { get; private set; } = Vector3.zero;
public Vector3 diff_scale { get; private set; } = Vector3.zero;
public Quaternion diff_rot { get; private set; } = Quaternion.identity;
//開始地点・終了地点
Vector3 start_pos = Vector3.zero;
Vector3 end_pos = Vector3.zero;
Vector3 start_scale = Vector3.zero;
Vector3 end_scale = Vector3.zero;
Quaternion start_rot = Quaternion.identity;
Quaternion end_rot = Quaternion.identity;
// Behaviourの生成時にLocationを取得。ClipのCreatePlayableのタイミングでは早すぎる
// Called when the owning graph starts playing
public override void OnGraphStart(Playable playable)
{
var graph = playable.GetGraph();
// IDに対応するGameObjectを得る
bool startIsValid;
Transform start = graph.GetResolver().GetReferenceValue(startID, out startIsValid) as Transform;
if (startIsValid)
{
startLocation = start;
}
bool endIsValid;
Transform end = graph.GetResolver().GetReferenceValue(endID, out endIsValid) as Transform;
if (endIsValid)
{
endLocation = end;
}
//始点と終点を設定
start_pos = (startLocation is null) ? Vector3.zero : startLocation.localPosition - trackBinding.localPosition;
end_pos = (endLocation is null) ? Vector3.zero : endLocation.localPosition - trackBinding.localPosition;
start_scale = (startLocation is null) ? Vector3.zero : startLocation.localScale - trackBinding.localScale;
end_scale = (endLocation is null) ? Vector3.zero : endLocation.localScale - trackBinding.localScale;
start_rot = (startLocation is null) ? trackBinding.localRotation : startLocation.localRotation;
end_rot = (endLocation is null) ? trackBinding.localRotation : endLocation.localRotation;
}
// Called each frame while the state is set to Play
// ClipのPlayableBehaviourの場合、playableはClipを表す
public override void PrepareFrame(Playable playable, FrameData info)
{
// フレーム内の進捗に従って線形補間
// TODO:カーブを適用
float progress = (float)(playable.GetTime() / playable.GetDuration());
if (shouldTweenPosition)
{
diff_pos = Vector3.Lerp(start_pos, end_pos, progress);
}
if (shouldTweenScale)
{
diff_scale = Vector3.Lerp(start_scale, end_scale, progress);
}
if (shouldTweenRotation)
{
//クォータニオンをフレーム内の進捗に従って球面線形補間(slerp)
diff_rot = Quaternion.Slerp(start_rot, end_rot, progress);
}
}
}
画像を動かしてみる
テスト用に立ち絵を動かしました。画像はタイムラインエディタのプレビューですが、ゲーム実行時も同様に動きました。元々プレビューで動作確認で
きることが目標だったのでこれで一応OKです。
2つ目のクリップと3つ目のクリップは途中でブレンドしています。この場合、前のクリップの中のモーフィングと次のクリップ内のモーフィングがさらにモーフィングされるので、複雑な動きになっています。
このテストでは地点をStart,Endだけ指定しましたが、2点に限らずPlayableDirectorに登録できるので、キーと地点のオブジェクトの情報はDictionaryで持たせるのが良いかもしれません。その辺はエフェクト演出を組んでみて決めようと思います。
この記事が気に入ったらサポートをしてみませんか?