12
Help us understand the problem. What are the problem?

posted at

Unityを実行せずにアニメーションを再生する

概要

PlayableAPIを用いて、Unityエディタを実行せずにアニメーションを再生する方法を記載します。

Playables APIとは、UnityのAnimator Controllerなどで内部的に使われている仕組みです。
これを用いることでアニメーションの再生機構を自作することなどができます。
https://docs.unity3d.com/ja/2021.2/Manual/Playables.html

この記事ではPlayable Graphを可視化できるPlayableGraph Visualizerを使用しています。
https://github.com/Unity-Technologies/graph-visualizer.git

PlayableAPIでシンプルにアニメーションを再生する(実行あり)

Unity-chanに自作の再生コンポーネントを付与し、Unityエディタを実行します。
単純に1つのAnimationClipを再生します。
PlayableGraph VisualizerではシンプルにAnimationClipとAnimationOutputが繋がっていることが確認できます。

SimpleAnimation

SimpleAnimation.cs
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

[RequireComponent(typeof(Animator))]
public class SimpleAnimation : MonoBehaviour
{
    PlayableGraph graph;
    [SerializeField] AnimationClip clip;

    void Start()
    {
        graph = PlayableGraph.Create();

        var clipPlayable = AnimationClipPlayable.Create(graph, clip);
        var output = AnimationPlayableOutput.Create(graph, "output", GetComponent<Animator>());
        output.SetSourcePlayable(clipPlayable);

        graph.Play();
    }

    void OnDestroy()
    {
        if (graph.IsValid())
        {
            graph.Destroy();
        }
    }
}

自作のエディタ拡張からアニメーションを再生する

キャラモデルの確認用に、エディタ拡張でアニメーションを流すことがあると思います。
単純に1つのAnimationClipを再生します。(UIは適当です。)
PlayableGraph Visualizerは変わらずAnimationClipとAnimationOutputが繋がっています。

EditorSimpleAnimationWindow

EditorSimpleAnimationWindow.cs
using UnityEngine;
using UnityEditor;
using UnityEngine.Playables;
using UnityEngine.Animations;

public class EditorSimpleAnimationWindow : EditorWindow
{
    [MenuItem("MyAnimation/SimpleAnimation", priority = 1)]
    static void ShowWindow()
    {
        EditorWindow.GetWindow<EditorSimpleAnimationWindow>();
    }

    [SerializeField] SimpleAnimation simpleAnimation;
    [SerializeField] AnimationClip clip;

    private float time = 0;

    void OnGUI()
    {
        EditorGUILayout.LabelField("Target", EditorStyles.boldLabel);
        simpleAnimation = EditorGUILayout.ObjectField("SimpleAnimationController", simpleAnimation, typeof(SimpleAnimation), true) as SimpleAnimation;
        clip = EditorGUILayout.ObjectField("clip", clip, typeof(AnimationClip), true) as AnimationClip;

        if (simpleAnimation == null)
        {
            return;
        }

        EditorGUILayout.LabelField("Animation", EditorStyles.boldLabel);
        if (GUILayout.Button("SetupPlayableGraph"))
        {
            SetupPlayableGraph();
            Debug.Log("SetupPlayableGraph");
        }

        using (new EditorGUILayout.HorizontalScope())
        {
            if (GUILayout.Button("PlayAnimation"))
            {
                PlayAnimation();
                Debug.Log("PlayAnimation");
            }

            if (GUILayout.Button("StopAnimation"))
            {
                StopAnimation();
                Debug.Log("StopAnimation");
            }
        }

        if (GUILayout.Button("DestroyPlayableGraph"))
        {
            DestroyPlayableGraph();
            Debug.Log("DestroyPlayableGraph");
        }

        EditorGUILayout.LabelField("Time", EditorStyles.boldLabel);
        using (new EditorGUILayout.HorizontalScope())
        {
            time = EditorGUILayout.Slider(time, 0, clip.length);
            if (GUILayout.Button("Evaluate"))
            {
                Evaluate(time);
                Debug.Log("Evaluate");
            }
        }
    }


    # region MainCode
    PlayableGraph graph;
    AnimationMixerPlayable mixer;

    public void SetupPlayableGraph()
    {
        graph = PlayableGraph.Create();

        var clipPlayable = AnimationClipPlayable.Create(graph, clip);
        var output = AnimationPlayableOutput.Create(graph, "output", simpleAnimation.gameObject.GetComponent<Animator>());
        output.SetSourcePlayable(clipPlayable);
    }

    public void PlayAnimation()
    {
        graph.Play();
        Evaluate(0.0f);
    }

    public void StopAnimation()
    {
        graph.Stop();
    }

    public void Evaluate(float time)
    {
        if (graph.IsValid())
        {
            graph.Evaluate(time);
        }
    }

    public void DestroyPlayableGraph()
    {
        if (graph.IsValid())
        {
            graph.Destroy();
        }
    }
    # endregion MainCode
}

自作のエディタ拡張からアニメーションを再生してモーションブレンド具合を確認する

AnimationMixerPlayableなどを用いてモーションブレンド具合を確認することもできます。
https://docs.unity3d.com/ScriptReference/Animations.AnimationMixerPlayable.html

PlayableGraph VisualizerではAnimationMixerからAnimationClipに繋がっており、Blendの重みを変更することで線の色が徐々に変わっているのが確認できます。

EditorMixAnimationWindow

EditorMixAnimationWindow.cs
using UnityEngine;
using UnityEditor;
using UnityEngine.Playables;
using UnityEngine.Animations;

public class EditorMixAnimationWindow : EditorWindow
{
    [MenuItem("MyAnimation/MixAnimation", priority = 2)]
    static void ShowWindow()
    {
        EditorWindow.GetWindow<EditorMixAnimationWindow>();
    }

    [SerializeField] SimpleAnimation simpleAnimation;
    [SerializeField] AnimationClip clipA;
    [SerializeField] AnimationClip clipB;

    private float time = 0;
    private float weight = 0;

    void OnGUI()
    {
        EditorGUILayout.LabelField("Target", EditorStyles.boldLabel);
        simpleAnimation = EditorGUILayout.ObjectField("SimpleAnimationComtroller", simpleAnimation, typeof(SimpleAnimation), true) as SimpleAnimation;
        clipA = EditorGUILayout.ObjectField("clipA", clipA, typeof(AnimationClip), true) as AnimationClip;
        clipB = EditorGUILayout.ObjectField("clipB", clipB, typeof(AnimationClip), true) as AnimationClip;

        if (simpleAnimation == null)
        {
            return;
        }

        EditorGUILayout.LabelField("Animation", EditorStyles.boldLabel);
        if (GUILayout.Button("SetupPlayableGraph"))
        {
            SetupPlayableGraph();
            Debug.Log("SetupPlayableGraph");
        }

        using (new EditorGUILayout.HorizontalScope())
        {
            if (GUILayout.Button("PlayAnimation"))
            {
                PlayAnimation();
                Debug.Log("PlayAnimation");
            }

            if (GUILayout.Button("StopAnimation"))
            {
                StopAnimation();
                Debug.Log("StopAnimation");
            }
        }

        if (GUILayout.Button("DestroyPlayableGraph"))
        {
            DestroyPlayableGraph();
            Debug.Log("DestroyPlayableGraph");
        }

        EditorGUILayout.LabelField("Time", EditorStyles.boldLabel);
        using (new EditorGUILayout.HorizontalScope())
        {
            var length = Mathf.Max(clipA.length, clipB.length);
            time = EditorGUILayout.Slider(time, 0, length);
            if (GUILayout.Button("Evaluate"))
            {
                Evaluate(time);
                Debug.Log("Evaluate");
            }
        }

        EditorGUILayout.LabelField("Weight", EditorStyles.boldLabel);
        weight = EditorGUILayout.Slider(weight, 0, 1);
        UpdateMixerWeight(weight);
    }


    # region MainCode
    PlayableGraph graph;
    AnimationMixerPlayable mixer;

    public void SetupPlayableGraph()
    {
        graph = PlayableGraph.Create();

        var clip1Playable = AnimationClipPlayable.Create(graph, clipA);
        var clip2Playable = AnimationClipPlayable.Create(graph, clipB);

        mixer = AnimationMixerPlayable.Create(graph, 2, true);
        mixer.ConnectInput(0, clip1Playable, 0);
        mixer.ConnectInput(1, clip2Playable, 0);

        var output = AnimationPlayableOutput.Create(graph, "output", simpleAnimation.gameObject.GetComponent<Animator>());
        output.SetSourcePlayable(mixer);
    }

    public void PlayAnimation()
    {
        graph.Play();
        UpdateMixerWeight(weight);
        Evaluate(0.0f);
    }

    public void StopAnimation()
    {
        graph.Stop();
    }

    public void UpdateMixerWeight(float mixWeight)
    {
        if (graph.IsValid())
        {
            mixer.SetInputWeight(0, 1 - mixWeight);
            mixer.SetInputWeight(1, mixWeight);
        }
    }

    public void Evaluate(float time)
    {
        if (graph.IsValid())
        {
            graph.Evaluate(time);
        }
    }

    public void DestroyPlayableGraph()
    {
        if (graph.IsValid())
        {
            graph.Destroy();
        }
    }
    # endregion MainCode
}

まとめ

PlayableAPIを用いて、Unityエディタを実行せずにアニメーションを再生する方法を記載しました。

今回は簡単にアニメーションを再生/ブレンドするだけでしたが、AnimationControllerだと複雑化しやすいキャラ制御もコードで記述でき、エンジニア目線だとコードレビューもしやすいかもしれません。
またキャラが増えた場合にもAnimationControllerを量産する必要はなく、キャラごとのアニメーションをマスターデータで管理することも容易です。

ゲーム開発においてキャラ、アニメーションチェックツールを作成することはあると思います。
プロジェクトが肥大化するとUnityの実行が重くなったりすることがありますが、この方法であればいつでもサクサクで確認が可能です。
任意のタイミング/ブレンド率でのモーションブレンドの確認なども容易ですのでぜひ活用してみてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
12
Help us understand the problem. What are the problem?