📗

【Unity】アニメーション統合クリップについて

2024/08/29に公開

はじめに

初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!

今回は
 アニメーション統合クリップ
について紹介します

https://zenn.dev/tmb/articles/1072f8ea010299

アニメーション統合クリップとは

アニメーションは、オブジェクト毎にAnimatorControllerとAnimationClipを使います

例①:ホーム画面にあるバナーのアニメーションの場合
画像①のように名前による管理が必要になります

このような状態だと、オブジェクト毎にファイルが増えていき、管理が大変になる。
そのため、AnimatorControllerに紐づく(オブジェクトに紐づく)AnimationClipは統合して管理します

先ほどの例①のAnimationClipをAnimatorControllerに統合した場合
画像②のように、HomeBannerAnimatorに内包する形で紐づくアニメーションを持たせる

このようにすることで、オブジェクト毎のアニメーションの管理がしやすくなる。

使い方

・AnimatorControllerを置きたいProjectの階層で
 右クリックし、AssetsメニューからAnimationCombineClipを選択

・AnimationCombineClipウィンドウが立ち上がるので
 Target Clipに作成済みのAnimatorControllerを設定するか、
 Animator Controller Nameに作成したいAnimatorControllerの名前を入力し、Createを押す

・作成されたAnimatorControllerはデフォルトで、ShowとHideを持っています。
 また、ShowとHideはAnimatorにも自動で設定されます。

【Clipの追加、リネーム、削除について】
・Clipの追加はAdd New Clipに名前を入力し、Createを押す(名前を入力するとCreateボタンが出ます)
・Clipのリネームは対象のクリップのRenameボタンを押し、入力欄に変更後の名前を入力する
・Clipの削除は対象のクリップのRemoveボタンを押す

スクリプト

AnimationCombineClip.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.Animations;
using System.Collections.Generic;
using System.IO;

namespace Editor
{
    sealed public class AnimationCombineClip : EditorWindow
    {
        private string _controllerName;
        private AnimatorController _controller;
        private string _clipName;
        private HashSet<AnimationClip> _clipSet;
        private List<AnimationClip> _clipsToRemove;
        private Dictionary<AnimationClip, string> _renameClips;
        private List<AnimationClip> _clipsToAdd = new List<AnimationClip>(); // 追加するクリップのリスト
        private Dictionary<AnimationClip, string> _clipNames = new Dictionary<AnimationClip, string>(); // リスト内のクリップの名前管理
        private HashSet<AnimationClip> _clipsBeingRenamed = new HashSet<AnimationClip>(); // リネーム中のクリップ

        private const string AddControllerName = "Controller";

        /// <summary>
        /// メニュー項目を追加し、ウィンドウを作成するメソッド
        /// </summary>
        [MenuItem("Assets/AnimationCombineClip")]
        private static void Create()
        {
            AnimationCombineClip window = GetWindow<AnimationCombineClip>();
            if (Selection.activeObject is AnimatorController)
            {
                window._controller = Selection.activeObject as AnimatorController;
                window.CacheClips();
            }
        }

        /// <summary>
        /// アニメーターコントローラーに含まれるアニメーションクリップをキャッシュするメソッド
        /// </summary>
        private void CacheClips()
        {
            _clipSet = new HashSet<AnimationClip>();
            if (_controller == null)
            {
                return;
            }

            Object[] allAssets = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(_controller));
            foreach (Object asset in allAssets)
            {
                if (asset is AnimationClip clip)
                {
                    _clipSet.Add(clip);
                }
            }

            _clipsToRemove = new List<AnimationClip>();
            _renameClips = new Dictionary<AnimationClip, string>();
        }

        /// <summary>
        /// エディタウィンドウのGUIを描画するメソッド
        /// </summary>
        private void OnGUI()
        {
            // Target Clip の設定
            EditorGUILayout.LabelField("Target Animator Controller");
            AnimatorController newController = EditorGUILayout.ObjectField(_controller, typeof(AnimatorController), false) as AnimatorController;

            // Target Clip が設定されたかどうかで処理を分岐
            if (newController != _controller)
            {
                _controller = newController;
                CacheClips();
                _clipsToAdd.Clear();
            }

            if (_controller == null)
            {
                // Target Clip が設定されていない場合のUI

                EditorGUILayout.LabelField("Animator Controller Name");
                _controllerName = EditorGUILayout.TextField(_controllerName);

                // ドラッグアンドドロップエリアとリストを表示
                DrawClipDragAndDropList();

                // デフォルトクリップを追加するボタン(Show と Hide)
                if (GUILayout.Button("Create Default Clips"))
                {
                    AddDefaultClips();
                }

                // 新規コントローラー作成ボタン
                if (GUILayout.Button("Create New Animator Controller"))
                {
                    CreateNewAnimatorControllerWithClips();
                }
            }
            else
            {
                // Target Clip が設定された場合のUI

                EditorGUILayout.LabelField("Animator Controller: " + _controller.name);

                // ドラッグアンドドロップエリアとリストを表示
                DrawClipDragAndDropList();

                // 新しいクリップ名を入力するフィールドと追加ボタン
                EditorGUILayout.Space();
                EditorGUILayout.LabelField("Add New Clip");
                _clipName = EditorGUILayout.TextField("Clip Name", _clipName);

                // ドラッグアンドドロップで追加されたリストをコントローラーに追加するボタン
                if (GUILayout.Button("Add Clip"))
                {
                    if (!string.IsNullOrEmpty(_clipName))
                    {
                        CreateNewClip(_clipName);
                        _clipName = string.Empty;
                    }
                    if (_clipsToAdd.Count > 0)
                    {
                        foreach (var clip in _clipsToAdd)
                        {
                            AddClipToController(clip);
                        }
                        _clipsToAdd.Clear();
                    }
                }

                // デフォルトクリップを追加するボタン(Show と Hide)
                if (GUILayout.Button("Add Default Clips"))
                {
                    CreateNewClip("Show");
                    CreateNewClip("Hide");
                }

                // 現在コントローラーに内包されているクリップリストを表示
                if (_clipSet != null && _clipSet.Count > 0)
                {
                    EditorGUILayout.Space();
                    EditorGUILayout.LabelField("Clips in Controller");

                    EditorGUILayout.BeginVertical("Box");
                    foreach (AnimationClip clip in _clipSet)
                    {
                        EditorGUILayout.BeginHorizontal();
                        EditorGUILayout.LabelField(clip.name);

                        // 既存クリップのリネームボタン
                        if (_renameClips.ContainsKey(clip))
                        {
                            _renameClips[clip] = EditorGUILayout.TextField(_renameClips[clip], GUILayout.Width(100));
                            if (GUILayout.Button("Apply", GUILayout.Width(100)))
                            {
                                RenameClip(clip, _renameClips[clip]);
                                _renameClips.Remove(clip);
                            }
                        }
                        else
                        {
                            if (GUILayout.Button("Rename", GUILayout.Width(100)))
                            {
                                _renameClips[clip] = clip.name;
                            }
                        }

                        // 既存クリップのリムーブボタン
                        if (GUILayout.Button("Remove", GUILayout.Width(100)))
                        {
                            _clipsToRemove.Add(clip);
                        }
                        EditorGUILayout.EndHorizontal();
                    }
                    EditorGUILayout.EndVertical();

                    // 削除リストにあるクリップを実際に削除
                    foreach (AnimationClip clip in _clipsToRemove)
                    {
                        RemoveClip(clip);
                    }
                    _clipsToRemove.Clear();
                }
            }
        }

        /// <summary>
        /// ドラッグアンドドロップリストの表示とリネーム・リムーブ機能
        /// </summary>
        private void DrawClipDragAndDropList()
        {
            // ドラッグアンドドロップエリア
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Drag and Drop Clip Here to Add to List");
            var dragRect = GUILayoutUtility.GetRect(0, 50, GUILayout.ExpandWidth(true));
            GUI.Box(dragRect, "Drag and Drop Animation Clips");

            // ドラッグアンドドロップでリストに追加
            HandleDragAndDropForList(dragRect);

            // クリップリストを表示
            if (_clipsToAdd.Count > 0)
            {
                EditorGUILayout.Space();
                EditorGUILayout.LabelField("Clips to Add to Controller");

                EditorGUILayout.BeginVertical("Box");
                for (int i = 0; i < _clipsToAdd.Count; i++)
                {
                    var clip = _clipsToAdd[i];
                    EditorGUILayout.BeginHorizontal();

                    // クリップがリネーム中であれば、名前を編集可能に
                    if (_clipsBeingRenamed.Contains(clip))
                    {
                        _clipNames[clip] = EditorGUILayout.TextField(_clipNames[clip], GUILayout.Width(200));
                        if (GUILayout.Button("Apply", GUILayout.Width(100)))
                        {
                            _clipsBeingRenamed.Remove(clip);
                        }
                    }
                    else
                    {
                        EditorGUILayout.LabelField(_clipNames[clip], GUILayout.Width(200));
                        if (GUILayout.Button("Rename", GUILayout.Width(100)))
                        {
                            _clipsBeingRenamed.Add(clip);
                        }
                    }

                    // リムーブボタン
                    if (GUILayout.Button("Remove", GUILayout.Width(100)))
                    {
                        _clipsToAdd.RemoveAt(i);
                        _clipNames.Remove(clip);
                        _clipsBeingRenamed.Remove(clip);
                        i--;
                    }

                    EditorGUILayout.EndHorizontal();
                }
                EditorGUILayout.EndVertical();
            }
        }

        /// <summary>
        /// デフォルトのShowとHideのクリップを追加するメソッド
        /// </summary>
        private void AddDefaultClips()
        {
            var showClip = new AnimationClip { name = "Show" };
            var hideClip = new AnimationClip { name = "Hide" };

            _clipsToAdd.Add(showClip);
            _clipsToAdd.Add(hideClip);

            _clipNames[showClip] = "Show";
            _clipNames[hideClip] = "Hide";
        }

        /// <summary>
        /// ドラッグアンドドロップ処理(リスト用)
        /// </summary>
        private void HandleDragAndDropForList(Rect dropArea)
        {
            Event evt = Event.current;
            switch (evt.type)
            {
                case EventType.DragUpdated:
                case EventType.DragPerform:
                    if (!dropArea.Contains(evt.mousePosition))
                        return;

                    DragAndDrop.visualMode = DragAndDropVisualMode.Copy;

                    if (evt.type == EventType.DragPerform)
                    {
                        DragAndDrop.AcceptDrag();

                        foreach (Object dragged in DragAndDrop.objectReferences)
                        {
                            if (dragged is AnimationClip clip && !_clipsToAdd.Contains(clip))
                            {
                                _clipsToAdd.Add(clip); // リストに追加
                                _clipNames[clip] = clip.name; // 名前管理用
                                Repaint();
                            }
                        }
                    }
                    break;
            }
        }

        /// <summary>
        /// 新しいアニメーターコントローラーを作成し、リスト内のクリップを含ませる
        /// </summary>
        private void CreateNewAnimatorControllerWithClips()
        {
            var selects = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.Assets);
            if (selects.Length == 0)
            {
                EditorUtility.DisplayDialog("Error", "Please select a folder in the Project window.", "OK");
                return;
            }

            if (!_controllerName.EndsWith(AddControllerName))
            {
                _controllerName += AddControllerName;
            }

            string path = AssetDatabase.GetAssetPath(selects[0]);
            if (!Directory.Exists(path))
            {
                path = Path.GetDirectoryName(path);
            }

            path = Path.Combine(path, _controllerName + ".controller");
            path = AssetDatabase.GenerateUniqueAssetPath(path);

            // 新しいアニメーションコントローラーを作成
            _controller = AnimatorController.CreateAnimatorControllerAtPath(path);

            if (_controller == null)
            {
                Debug.LogError("Failed to create Animator Controller.");
                return;
            }

            CacheClips();

            // BaseLayerにステートマシンを取得
            AnimatorStateMachine stateMachine = _controller.layers[0].stateMachine;

            // DoNothing ステートを追加して初期ステートとして設定
            AnimatorState doNothingState = stateMachine.AddState("DoNothing");
            doNothingState.writeDefaultValues = false;
            stateMachine.defaultState = doNothingState;

            // リストに追加したクリップをコントローラーに追加
            foreach (var clip in _clipsToAdd)
            {
                // クリップを複製して新しい名前を付ける
                AnimationClip newClip = new AnimationClip();
                EditorUtility.CopySerialized(clip, newClip);
                newClip.name = _clipNames[clip];

                // アセットとして追加
                AssetDatabase.AddObjectToAsset(newClip, _controller);
                AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
                AssetDatabase.Refresh();

                // 新しいステートを作成し、Motionにクリップを割り当て
                AnimatorState newState = stateMachine.AddState(newClip.name);
                newState.motion = newClip;
                newState.writeDefaultValues = false;

                _clipSet.Add(newClip);
            }

            // リストをクリア
            _clipsToAdd.Clear();
            _clipNames.Clear();
        }

        /// <summary>
        /// ドラッグアンドドロップされたクリップを複製してアニメーションコントローラーに追加
        /// </summary>
        private void AddClipToController(AnimationClip originalClip)
        {
            if (_controller == null || originalClip == null)
            {
                return;
            }

            string name = _clipNames[originalClip];
            if (ContainsClipName(name))
            {
                return;
            }

            // アニメーションクリップを複製
            AnimationClip newClip = new AnimationClip();
            EditorUtility.CopySerialized(originalClip, newClip);
            newClip.name = name;

            // アセットとして追加
            AssetDatabase.AddObjectToAsset(newClip, _controller);
            AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
            AssetDatabase.Refresh();

            // ステートを追加
            AnimatorStateMachine stateMachine = _controller.layers[0].stateMachine;
            AnimatorState newState = stateMachine.AddState(newClip.name);
            newState.motion = newClip;
            newState.writeDefaultValues = false;

            _clipSet.Add(newClip);
        }

        /// <summary>
        /// 新しいアニメーションクリップを作成し、コントローラーに追加する
        /// </summary>
        private void CreateNewClip(string clipName)
        {
            if (ContainsClipName(clipName))
            {
                return;
            }

            AnimationClip newClip = AnimatorController.AllocateAnimatorClip(clipName);

            AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(newClip);
            settings.loopTime = false;
            AnimationUtility.SetAnimationClipSettings(newClip, settings);

            // アセットとして追加
            AssetDatabase.AddObjectToAsset(newClip, _controller);
            AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
            AssetDatabase.Refresh();

            // ステートを追加
            AnimatorStateMachine stateMachine = _controller.layers[0].stateMachine;
            AnimatorState newState = stateMachine.AddState(newClip.name);
            newState.motion = newClip;
            newState.writeDefaultValues = false;

            _clipSet.Add(newClip);
        }

        /// <summary>
        /// 指定されたアニメーションクリップの名前を変更する
        /// </summary>
        private void RenameClip(AnimationClip clip, string newName)
        {
            string oldName = clip.name;

            clip.name = newName;
            EditorUtility.SetDirty(clip);

            foreach (AnimatorControllerLayer layer in _controller.layers)
            {
                UpdateStateMachine(layer.stateMachine, oldName, newName);
            }

            AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
            AssetDatabase.Refresh();
        }

        /// <summary>
        /// 状態マシン内のすべての状態を更新してクリップ名を変更する
        /// </summary>
        private void UpdateStateMachine(AnimatorStateMachine stateMachine, string oldName, string newName)
        {
            foreach (ChildAnimatorState state in stateMachine.states)
            {
                AnimationClip stateClip = state.state.motion as AnimationClip;
                if (stateClip != null && stateClip.name == newName && state.state.name == oldName)
                {
                    state.state.name = newName;
                    EditorUtility.SetDirty(state.state);
                }
            }

            foreach (ChildAnimatorStateMachine childStateMachine in stateMachine.stateMachines)
            {
                UpdateStateMachine(childStateMachine.stateMachine, oldName, newName);
            }
        }

        /// <summary>
        /// 指定されたアニメーションクリップを削除する
        /// </summary>
        private void RemoveClip(AnimationClip clip)
        {
            if (clip == null || _controller == null)
            {
                return;
            }

            // コントローラー内のすべてのレイヤーを確認
            foreach (var layer in _controller.layers)
            {
                AnimatorStateMachine stateMachine = layer.stateMachine;

                // ステートマシン内のすべてのステートを確認し、削除するクリップと一致するステートを削除
                List<ChildAnimatorState> statesToRemove = new List<ChildAnimatorState>();

                foreach (var state in stateMachine.states)
                {
                    if (state.state.motion == clip)
                    {
                        statesToRemove.Add(state);
                    }
                }

                // ステートを削除
                foreach (var stateToRemove in statesToRemove)
                {
                    stateMachine.RemoveState(stateToRemove.state);
                }
            }

            // アセットからクリップを削除
            DestroyImmediate(clip, true);
            AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_controller));
            AssetDatabase.Refresh();

            // クリップをキャッシュからも削除
            _clipSet.Remove(clip);
        }

        /// <summary>
        /// 指定された名前のアニメーションクリップが存在するか
        /// </summary>
        private bool ContainsClipName(string name)
        {
            foreach (AnimationClip clip in _clipSet)
            {
                if (clip.name == name)
                {
                    return true;
                }
            }
            return false;
        }
    }
}

Discussion