【Unity】AnimationClip のリネームサポートツール

Animator を使用している GameObject の子の命名を変えると,
AnimationClip で Missing が発生してしまい, 修正が手間なため, ツールを作成しました。

完成形

存在するパスが入力されると緑色のアイコンが表示されます。

ソースコード

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;

namespace Test_AnimationMissing
{
    public class AnimationFixer : EditorWindow
    {
        private Animator m_Animator;
        private List<AnimationClip> m_MissingClips = new();
        private List<string> m_MissingPathList = new();
        private List<string> m_ReplaceList = new();
        private List<string> m_ExistsPathList = new();
        private Vector2 m_ScrollPosition;
        private Texture2D m_SuccessIcon;
        private Texture2D m_FailedIcon;

        [MenuItem("EditorWindow/AnimationFixer")]
        private static void Init()
        {
            var window = GetWindow(typeof(AnimationFixer));
            window.titleContent = new GUIContent("AnimationFixer");
            window.Show();
            window.minSize = new Vector2(330, 400);
        }

        private void OnEnable()
        {
            m_SuccessIcon = (Texture2D)EditorGUIUtility.Load("d_greenLight");
            m_FailedIcon = (Texture2D)EditorGUIUtility.Load("d_redLight");
        }

        private void OnGUI()
        {
            using (var check = new EditorGUI.ChangeCheckScope())
            {
                m_Animator = EditorGUILayout.ObjectField("animator", m_Animator, typeof(Animator), true) as Animator;

                if (check.changed)
                {
                    Reload();
                }
            }

            if (m_Animator == null)
            {
                EditorGUILayout.HelpBox("animator を設定してください", MessageType.Info);
                return;
            }

            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();
                if (GUILayout.Button("リロード", GUILayout.Width(80)))
                {
                    Reload();
                }
            }

            using (var scrollview = new EditorGUILayout.ScrollViewScope(m_ScrollPosition))
            {
                m_ScrollPosition = scrollview.scrollPosition;
                if (m_MissingClips.Count == 0)
                {
                    EditorGUILayout.HelpBox("Missing がありません", MessageType.Info);
                    return;
                }

                using (new EditorGUILayout.HorizontalScope())
                {
                    GUILayout.Label("■ Missing パス");
                    GUILayout.Space(10);
                    GUILayout.Label("■ 置換後 パス");
                }
                using (new EditorGUILayout.VerticalScope("box"))
                {
                    for (int i = 0; i < m_MissingPathList.Count; i++)
                    {
                        using (new EditorGUILayout.HorizontalScope())
                        {
                            EditorGUILayout.SelectableLabel(m_MissingPathList[i], GUILayout.Height(15));
                            GUILayout.Label("->", GUILayout.Width(20));
                            m_ReplaceList[i] = EditorGUILayout.TextField(m_ReplaceList[i]);
                            var icon = m_ExistsPathList.Contains(m_ReplaceList[i]) ? m_SuccessIcon : m_FailedIcon;
                            if (icon != null)
                            {
                                GUILayout.Label(new GUIContent(icon), GUILayout.Height(20), GUILayout.Width(20));
                            }
                        }
                    }
                }

                GUILayout.Space(15);

                GUILayout.Label("■ 修正対象のAnimationClip");
                foreach (var clip in m_MissingClips)
                {
                    EditorGUILayout.ObjectField(clip, typeof(AnimationClip), false);
                }

                if (GUILayout.Button("Apply"))
                {
                    for (int i = 0; i < m_MissingPathList.Count; i++)
                    {
                        var missingPath = m_MissingPathList[i];
                        var replacePath = m_ReplaceList[i];
                        Undo.RecordObjects(m_MissingClips.ToArray(), "replace animationclip paths");
                        ReplacePath(m_MissingClips.ToArray(), missingPath, replacePath);
                    }

                    Reload();
                }
            }
        }


        private void Reload()
        {
            GUI.FocusControl(null);
            m_ReplaceList.Clear();
            m_MissingClips.Clear();
            m_MissingPathList.Clear();

            var result = GetMissingInfo(m_Animator);
            m_MissingClips.AddRange(result.Item2);
            m_MissingPathList.AddRange(result.Item1);
            m_ReplaceList.AddRange(m_MissingPathList);
            m_ExistsPathList = GetAllPaths(m_Animator.transform);
        }

        // List<missing path>, List<missing clip>
        private static (List<string>, List<AnimationClip>) GetMissingInfo(Animator m_Animator)
        {
            var result = (new List<string>(), new List<AnimationClip>());
            var controller = m_Animator.runtimeAnimatorController as UnityEditor.Animations.AnimatorController;
            var clips = controller.animationClips.Distinct().ToList();

            foreach (var clip in clips)
            {
                var hasMissing = false;
                var bindings = AnimationUtility.GetCurveBindings(clip);
                foreach (var binding in bindings)
                {
                    if (m_Animator.transform.Find(binding.path) == null)
                    {
                        result.Item1.Add(binding.path);
                        hasMissing = true;
                    }
                }

                if (hasMissing)
                {
                    result.Item2.Add(clip);
                }
            }

            result.Item1 = result.Item1.Distinct().ToList();
            result.Item2 = result.Item2.Distinct().ToList();

            return result;
        }

        private static void ReplacePath(AnimationClip[] clips, string oldPath, string newPath)
        {
            foreach (var clip in clips)
            {
                var bindings = AnimationUtility.GetCurveBindings(clip);
                var removeBindings = bindings.Where(c => c.path.Contains(oldPath));

                foreach (var binding in removeBindings)
                {
                    var curve = AnimationUtility.GetEditorCurve(clip, binding);
                    var newBinding = binding;
                    newBinding.path = newBinding.path.Replace(oldPath, newPath);
                    AnimationUtility.SetEditorCurve(clip, binding, null);
                    AnimationUtility.SetEditorCurve(clip, newBinding, curve);
                }
            }
        }

        private static List<string> GetAllPaths(Transform root)
        {
            List<string> paths = new List<string>();
            // Bindings の path に root は含まれない
            foreach (Transform child in root)
            {
                AddPathsRecursive(child, null, paths);
            }
            return paths;
        }

        private static void AddPathsRecursive(Transform current, string path, List<string> paths)
        {
            path = string.IsNullOrEmpty(path) ? current.name : path + "/" + current.name;
            paths.Add(path);
            foreach (Transform child in current)
            {
                AddPathsRecursive(child, path, paths);
            }
        }
    }
}

参考

以下リンク先をかなり参考にさせていただいていますm
tsubakit1.hateblo.jp