Playables APIを使ってUnity非再生時にモーションをプレビューする方法です。
Unity2018.4.0
やりたいことと問題点
UnityでAnimationの再生を非ランタイムで行いたいとします。
Particle Systemのシミュレーションのようなイメージです。
これが意外と面倒で、やりたい事次第ではTimelineで解決出来たりはしますが、
自分で実装する必要があればPlayables APIを使う必要があります。
またその場合、下記の記事のように保存時にPlayable Graphが消える問題などにも対策を打たないといけません。
今回はこれらを解決して非ランタイムでモーションのシミュレーションができるコンポーネントを作ってみます。
PlayableBehaviourを作る
まずPlayableBehaviourを継承したクラスを作ります。
using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; public class AnimationClipPlayableBehaviour : PlayableBehaviour { public float Time { get; set; } = 0.0f; private AnimationClipPlayable _clipPlayable; private PlayableGraph _graph; private Playable _parentPlayable; public void Setup(Playable scriptPlayable, PlayableGraph graph) { _graph = graph; _parentPlayable = scriptPlayable; scriptPlayable.SetInputCount(1); scriptPlayable.SetInputWeight(0, 1.0f); } public void SetClip(AnimationClip clip) { _clipPlayable = AnimationClipPlayable.Create(_graph, clip); if (_parentPlayable.GetInputCount() >= 1) { _parentPlayable.DisconnectInput(0); } _graph.Connect(_clipPlayable, 0, _parentPlayable, 0); } public override void PrepareFrame(Playable scriptPlayable, FrameData info) { if (!_clipPlayable.IsValid()) { return; } // info.deltaTimeではなくてTimeを使って処理する _clipPlayable.SetTime(Time); } }
ここで、Timeというpublicな変数を用意し、PrepareFrameではPlayableGraphのdeltaTimeではなく、
このTimeを使ってAnimationClipを処理している点がポイントです。
引数のFrameDataに入っているdeltaTimeを使えば1Frameずつ進んでいくような処理は実装できるのですが、
特定時間の状態にしたり、タイムラインのシークバーのようなものが実装できません。
そのためTimeを定義して、これを外側から与えられるようにします。
このあたりはTimelineの拡張によく似ています。
Playable Graphを作る
次にPlayable Graphを作って再生を管理するためのクラスを作ります。
#if UNITY_EDITOR #define IS_EDITOR #endif #if IS_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; [RequireComponent(typeof(Animator))] [ExecuteAlways] public class AnimationPlayer : MonoBehaviour { private float _time = default; private bool _isPlaying = false; private PlayableGraph _playableGraph; private ScriptPlayable<AnimationClipPlayableBehaviour> _playable; private AnimationClip _currentClip; #if IS_EDITOR private double _editorTimeSinceStartup; #endif public void Play() { _isPlaying = true; } public void Stop() { _isPlaying = false; } /// <summary> /// 時間を指定して状態を評価する /// </summary> public void SetTime(float time) { CreateGraphIfNeeded(); // 時間が更新されてなかったら処理しない if (_time == time) { return; } _time = time; _playable.GetBehaviour().Time = time; _playableGraph.Evaluate(); } public void SetClip(AnimationClip clip) { CreateGraphIfNeeded(); _playable.GetBehaviour().SetClip(clip); _currentClip = clip; } /// <summary> /// PlayableGraphが生成されてなかったら生成する /// </summary> private void CreateGraphIfNeeded() { if (!_playableGraph.IsValid()) { _playableGraph = PlayableGraph.Create("Animation Playable"); AnimationPlayableOutput.Create(_playableGraph, "Animation", GetComponent<Animator>()); _playable = ScriptPlayable<AnimationClipPlayableBehaviour>.Create(_playableGraph); var behaviour = _playable.GetBehaviour(); behaviour.Setup(_playable, _playableGraph); _playableGraph.GetOutput(0).SetSourcePlayable(_playable); _playableGraph.SetTimeUpdateMode(DirectorUpdateMode.Manual); _playable.Play(); if (_currentClip != null) { SetClip(_currentClip); } } } private void OnEnable() { #if IS_EDITOR EditorApplication.update += EditorUpdate; #endif } private void OnDisable() { #if IS_EDITOR EditorApplication.update -= EditorUpdate; #endif } private void Update() { // 再生時はUpdateで時間を更新 if (Application.isPlaying) { var time = _time; if (_isPlaying) { time = _time + Time.deltaTime; } SetTime(time); } } #if IS_EDITOR private void EditorUpdate() { // DeltaTimeを計算 var deltaTime = EditorApplication.timeSinceStartup - _editorTimeSinceStartup; _editorTimeSinceStartup = EditorApplication.timeSinceStartup; // 非再生時はEditorUpdateで時間を更新 if (!Application.isPlaying) { var time = _time; if (_isPlaying) { time = _time + (float)deltaTime; } SetTime(time); } } #endif public void OnDestroy() { if (_playableGraph.IsValid()) { _playableGraph.Destroy(); } } }
使い方はAnimationClipをセットしてPlay()
やStop()
を呼ぶだけです。
自身で時間を指定して更新したい場合にはそれらを呼ばず、SetTime()
を呼びます。
Unity非再生時でも正常に動くようにしているのがポイントです。
使ってみる
それではこのクラスを下記のようなコンポーネントで適当に操作してみます。
#if UNITY_EDITOR #define IS_EDITOR #endif #if IS_EDITOR using UnityEditor; #endif using UnityEngine; [ExecuteAlways] public class Example : MonoBehaviour { [SerializeField] private AnimationPlayer _player; [SerializeField] private float _time; [SerializeField] private AnimationClip _clip; private AnimationClip _currentClip; private void Update() { if (_player != null && Application.isPlaying) { _player.SetTime(_time); if (_clip != _currentClip) { _player.SetClip(_clip); _currentClip = _clip; } } } private void OnEnable() { #if IS_EDITOR EditorApplication.update += EditorUpdate; #endif } private void OnDisable() { #if IS_EDITOR EditorApplication.update -= EditorUpdate; #endif } #if IS_EDITOR private void EditorUpdate() { if (_player != null && !Application.isPlaying) { _player.SetTime(_time); if (_clip != _currentClip) { _player.SetClip(_clip); _currentClip = _clip; } } } #endif }
今回は指定時間のモーションをプレビューできるものを作ってみました。
正常に動作していることが確認できました。