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が繋がっていることが確認できます。

<img width=512 alt="SimpleAnimation" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/62101f6d-f847-8629-f079-fa7252047ae7.png">

```c#: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が繋がっています。

<img width=512 alt="EditorSimpleAnimationWindow" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/9354a4a8-7b30-028f-72f3-f4a3bccbde86.gif">

```c#: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の重みを変更することで線の色が徐々に変わっているのが確認できます。

<img width=512 alt="EditorMixAnimationWindow" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/f4c1e903-77c1-adeb-b928-f269cc6db457.gif">

```c#: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の実行が重くなったりすることがありますが、この方法であればいつでもサクサクで確認が可能です。
任意のタイミング/ブレンド率でのモーションブレンドの確認なども容易ですのでぜひ活用してみてください。