#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEngine; /// /// 修复 Animator Controller 中动画片段因层级变化导致的路径丢失。 /// public class AnimatorClipPathFixer : EditorWindow { private AnimatorController _controller; private GameObject _referenceRoot; private string _manualPrefix = ""; private string _syncStatus = ""; private MessageType _syncStatusType = MessageType.Info; private Vector2 _scroll; private readonly List _reports = new(); private struct ClipReport { public AnimationClip Clip; public int TotalBindings; public int BrokenBindings; public int FixedBindings; public List Messages; } [MenuItem("Tools/Animation/AnimatorClipPathFixer")] public static void OpenWindow() { var window = GetWindow(); window.titleContent = new GUIContent(AnimatorClipPathFixerLoc.WindowTitle); window.SyncFromSelection(); window.minSize = new Vector2(480, 360); } private void OnGUI() { DrawLanguageSelector(); EditorGUILayout.LabelField(AnimatorClipPathFixerLoc.Title, EditorStyles.boldLabel); EditorGUILayout.HelpBox(AnimatorClipPathFixerLoc.HelpBox, MessageType.Info); using (new EditorGUILayout.VerticalScope("box")) { _controller = EditorGUILayout.ObjectField( AnimatorClipPathFixerLoc.FieldAnimatorController, _controller, typeof(AnimatorController), false) as AnimatorController; _referenceRoot = EditorGUILayout.ObjectField( new GUIContent( AnimatorClipPathFixerLoc.FieldReferenceRoot, AnimatorClipPathFixerLoc.FieldReferenceRootTip), _referenceRoot, typeof(GameObject), true) as GameObject; _manualPrefix = EditorGUILayout.TextField( new GUIContent( AnimatorClipPathFixerLoc.FieldManualPrefix, AnimatorClipPathFixerLoc.FieldManualPrefixTip), _manualPrefix); } if (!string.IsNullOrEmpty(_syncStatus)) EditorGUILayout.HelpBox(_syncStatus, _syncStatusType); EditorGUILayout.Space(4); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button(AnimatorClipPathFixerLoc.BtnDetectFromSelection)) SyncFromSelection(); GUI.enabled = _controller != null && _referenceRoot != null; if (GUILayout.Button(AnimatorClipPathFixerLoc.BtnAnalyze)) Analyze(); if (GUILayout.Button(AnimatorClipPathFixerLoc.BtnApplyFix)) ApplyFix(); GUI.enabled = true; } DrawReports(); } private void DrawLanguageSelector() { using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); EditorGUILayout.LabelField(AnimatorClipPathFixerLoc.LabelLanguage, GUILayout.Width(36)); var index = (int)AnimatorClipPathFixerLoc.Current; var newIndex = EditorGUILayout.Popup(index, AnimatorClipPathFixerLoc.LanguageOptions, GUILayout.Width(100)); if (newIndex != index) { AnimatorClipPathFixerLoc.Current = (AnimatorClipPathFixerLoc.Language)newIndex; titleContent = new GUIContent(AnimatorClipPathFixerLoc.WindowTitle); Repaint(); } } EditorGUILayout.Space(2); } private void SyncFromSelection() { _syncStatus = ""; _syncStatusType = MessageType.Info; foreach (var obj in Selection.objects) { if (obj is AnimatorController projectController) { _controller = projectController; break; } } var animator = FindAnimatorFromSelection(); if (animator != null) { _referenceRoot = animator.gameObject; ApplyControllerFromAnimator(animator); return; } if (Selection.gameObjects.Length > 0 || Selection.activeGameObject != null) { _syncStatus = AnimatorClipPathFixerLoc.SyncNoAnimator; _syncStatusType = MessageType.Warning; return; } if (_controller != null) { _syncStatus = AnimatorClipPathFixerLoc.SyncControllerOnly(_controller.name); _syncStatusType = MessageType.Info; return; } _syncStatus = AnimatorClipPathFixerLoc.SyncSelectObject; _syncStatusType = MessageType.Warning; } private void ApplyControllerFromAnimator(Animator animator) { var rac = animator.runtimeAnimatorController; if (rac is AnimatorController runtimeController) { _controller = runtimeController; _syncStatus = AnimatorClipPathFixerLoc.SyncAnimatorAndController( GetHierarchyPath(animator.transform), runtimeController.name); return; } if (rac is AnimatorOverrideController overrideController) { var baseController = overrideController.runtimeAnimatorController as AnimatorController; if (baseController != null) _controller = baseController; _syncStatus = AnimatorClipPathFixerLoc.SyncOverrideController( GetHierarchyPath(animator.transform), overrideController.name, baseController?.name ?? AnimatorClipPathFixerLoc.None); _syncStatusType = baseController != null ? MessageType.Info : MessageType.Warning; return; } if (rac != null) { _syncStatus = AnimatorClipPathFixerLoc.SyncUnknownControllerType( GetHierarchyPath(animator.transform), rac.GetType().Name, rac.name); _syncStatusType = MessageType.Warning; return; } _syncStatus = AnimatorClipPathFixerLoc.SyncNoRuntimeController( GetHierarchyPath(animator.transform)); _syncStatusType = MessageType.Warning; } private static Animator FindAnimatorFromSelection() { var gameObjects = Selection.gameObjects; if (gameObjects == null || gameObjects.Length == 0) { if (Selection.activeGameObject != null) gameObjects = new[] { Selection.activeGameObject }; else return null; } foreach (var go in gameObjects) { if (go == null) continue; var animator = go.GetComponent(); if (animator != null) return animator; animator = go.GetComponentInChildren(true); if (animator != null) return animator; animator = go.GetComponentInParent(); if (animator != null) return animator; } return null; } private static string GetHierarchyPath(Transform t) { if (t == null) return ""; var names = new List(); while (t != null) { names.Add(t.name); t = t.parent; } names.Reverse(); return string.Join("/", names); } private void Analyze() { _reports.Clear(); var animator = GetAnimator(_referenceRoot); if (animator == null) { EditorUtility.DisplayDialog( AnimatorClipPathFixerLoc.DialogTitle, AnimatorClipPathFixerLoc.DialogNoAnimatorOnReference, AnimatorClipPathFixerLoc.DialogOk); return; } foreach (var clip in GetClips(_controller)) { var resolver = new PathResolver(animator.transform, _manualPrefix); _reports.Add(resolver.AnalyzeClip(clip)); } SortReportsByBrokenBindings(); Repaint(); } private void ApplyFix() { if (_controller == null || _referenceRoot == null) return; var animator = GetAnimator(_referenceRoot); if (animator == null) { EditorUtility.DisplayDialog( AnimatorClipPathFixerLoc.DialogTitle, AnimatorClipPathFixerLoc.DialogNoAnimatorOnReference, AnimatorClipPathFixerLoc.DialogOk); return; } var summary = FixController(_controller, animator.gameObject, _manualPrefix); _reports.Clear(); _reports.AddRange(summary.Reports); SortReportsByBrokenBindings(); Repaint(); ShowFixResultDialog(summary); } /// /// 含失效绑定(或仍有未修复项)的片段靠前;同类按失效数量降序,再按片段名排序。 /// private void SortReportsByBrokenBindings() { _reports.Sort((a, b) => { var aRemaining = Math.Max(0, a.BrokenBindings - a.FixedBindings); var bRemaining = Math.Max(0, b.BrokenBindings - b.FixedBindings); var aHasIssue = aRemaining > 0; var bHasIssue = bRemaining > 0; if (aHasIssue != bHasIssue) return bHasIssue.CompareTo(aHasIssue); if (aRemaining != bRemaining) return bRemaining.CompareTo(aRemaining); if (a.BrokenBindings != b.BrokenBindings) return b.BrokenBindings.CompareTo(a.BrokenBindings); return string.Compare(a.Clip?.name, b.Clip?.name, StringComparison.Ordinal); }); } private void DrawReports() { if (_reports.Count == 0) return; EditorGUILayout.Space(6); EditorGUILayout.LabelField(AnimatorClipPathFixerLoc.ReportSectionTitle, EditorStyles.boldLabel); _scroll = EditorGUILayout.BeginScrollView(_scroll); foreach (var r in _reports) { using (new EditorGUILayout.VerticalScope("box")) { EditorGUILayout.LabelField(r.Clip != null ? r.Clip.name : "(null)", EditorStyles.boldLabel); EditorGUILayout.LabelField(AnimatorClipPathFixerLoc.ReportBindings( r.TotalBindings, r.BrokenBindings, r.FixedBindings)); if (r.Messages != null) { foreach (var msg in r.Messages.Take(30)) EditorGUILayout.LabelField(" " + msg, EditorStyles.miniLabel); if (r.Messages.Count > 30) EditorGUILayout.LabelField( AnimatorClipPathFixerLoc.ReportMoreMessages(r.Messages.Count - 30), EditorStyles.miniLabel); } } } EditorGUILayout.EndScrollView(); } private static FixSummary FixController(AnimatorController controller, GameObject referenceRoot, string manualPrefix) { var summary = new FixSummary(); var animator = GetAnimator(referenceRoot); if (animator == null || controller == null) return summary; var clips = GetClips(controller); var resolver = new PathResolver(animator.transform, manualPrefix); AssetDatabase.StartAssetEditing(); try { foreach (var clip in clips) { if (clip == null) continue; var report = resolver.FixClip(clip); summary.Reports.Add(report); summary.TotalFixed += report.FixedBindings; if (report.FixedBindings > 0) EditorUtility.SetDirty(clip); } } finally { AssetDatabase.StopAssetEditing(); } AssetDatabase.SaveAssets(); return summary; } private static void ShowFixResultDialog(FixSummary summary) { var totalFixed = summary.TotalFixed; var unresolved = summary.Reports.Sum(r => Math.Max(0, r.BrokenBindings - r.FixedBindings)); var message = AnimatorClipPathFixerLoc.DialogClipsProcessed(summary.Reports.Count) + "\n" + AnimatorClipPathFixerLoc.DialogBindingsFixed(totalFixed) + "\n" + AnimatorClipPathFixerLoc.DialogStillBroken(Math.Max(0, unresolved)); if (totalFixed == 0 && unresolved == 0) message += "\n" + AnimatorClipPathFixerLoc.DialogNothingToFix; EditorUtility.DisplayDialog( AnimatorClipPathFixerLoc.DialogFixCompleteTitle, message, AnimatorClipPathFixerLoc.DialogOk); } private static Animator GetAnimator(GameObject root) { if (root == null) return null; return root.GetComponent() ?? root.GetComponentInChildren(); } private static IEnumerable GetClips(AnimatorController controller) { if (controller == null) yield break; var set = new HashSet(); foreach (var clip in controller.animationClips) { if (clip != null && set.Add(clip)) yield return clip; } } private class FixSummary { public readonly List Reports = new(); public int TotalFixed; } private class PathResolver { private readonly Transform _animatorRoot; private readonly string _manualPrefix; private readonly HashSet _validPaths = new(); private readonly List _allPaths = new(); public PathResolver(Transform animatorRoot, string manualPrefix) { _animatorRoot = animatorRoot; _manualPrefix = NormalizePrefix(manualPrefix); BuildHierarchyPaths(); } public ClipReport AnalyzeClip(AnimationClip clip) { var report = new ClipReport { Clip = clip, Messages = new List() }; if (clip == null) return report; foreach (var binding in GetAllBindings(clip)) { report.TotalBindings++; if (IsPathValid(binding.path)) continue; report.BrokenBindings++; if (TryResolvePath(binding.path, out var newPath)) report.Messages.Add(AnimatorClipPathFixerLoc.PathPreview(binding.path, newPath)); else report.Messages.Add(AnimatorClipPathFixerLoc.PathCannotAutoMatch(binding.path)); } return report; } public ClipReport FixClip(AnimationClip clip) { var report = AnalyzeClip(clip); report.FixedBindings = 0; report.Messages.Clear(); if (clip == null || report.BrokenBindings == 0) return report; Undo.RecordObject(clip, "Fix Animation Paths"); RemapFloatCurves(clip, ref report); RemapObjectCurves(clip, ref report); return report; } private void RemapFloatCurves(AnimationClip clip, ref ClipReport report) { var bindings = AnimationUtility.GetCurveBindings(clip); foreach (var binding in bindings) { if (IsPathValid(binding.path)) continue; if (!TryResolvePath(binding.path, out var newPath)) continue; var curve = AnimationUtility.GetEditorCurve(clip, binding); AnimationUtility.SetEditorCurve(clip, binding, null); var newBinding = binding; newBinding.path = newPath; AnimationUtility.SetEditorCurve(clip, newBinding, curve); report.FixedBindings++; report.Messages.Add(AnimatorClipPathFixerLoc.PathFixed(binding.path, newPath)); } } private void RemapObjectCurves(AnimationClip clip, ref ClipReport report) { var bindings = AnimationUtility.GetObjectReferenceCurveBindings(clip); foreach (var binding in bindings) { if (IsPathValid(binding.path)) continue; if (!TryResolvePath(binding.path, out var newPath)) continue; var curve = AnimationUtility.GetObjectReferenceCurve(clip, binding); AnimationUtility.SetObjectReferenceCurve(clip, binding, null); var newBinding = binding; newBinding.path = newPath; AnimationUtility.SetObjectReferenceCurve(clip, newBinding, curve); report.FixedBindings++; report.Messages.Add(AnimatorClipPathFixerLoc.PathFixed(binding.path, newPath)); } } private bool TryResolvePath(string oldPath, out string newPath) { newPath = null; if (string.IsNullOrEmpty(oldPath)) { if (_validPaths.Contains("")) { newPath = ""; return true; } return false; } if (!string.IsNullOrEmpty(_manualPrefix)) { var manual = _manualPrefix + oldPath; if (_validPaths.Contains(manual)) { newPath = manual; return true; } } var autoPrefix = TryDetectParentPrefix(oldPath); if (!string.IsNullOrEmpty(autoPrefix)) { var prefixed = autoPrefix + oldPath; if (_validPaths.Contains(prefixed)) { newPath = prefixed; return true; } } var suffixMatches = _allPaths .Where(p => p == oldPath || p.EndsWith("/" + oldPath, StringComparison.Ordinal)) .ToList(); if (suffixMatches.Count == 1) { newPath = suffixMatches[0]; return true; } if (suffixMatches.Count > 1) { newPath = suffixMatches .OrderBy(p => p.Length) .First(); return true; } var leaf = oldPath.Split('/').LastOrDefault(); if (!string.IsNullOrEmpty(leaf)) { var leafMatches = _allPaths .Where(p => p == leaf || p.EndsWith("/" + leaf, StringComparison.Ordinal)) .ToList(); if (leafMatches.Count == 1) { newPath = leafMatches[0]; return true; } } return false; } private bool IsPathValid(string path) { return _validPaths.Contains(path ?? ""); } private void BuildHierarchyPaths() { _validPaths.Clear(); _allPaths.Clear(); _validPaths.Add(""); foreach (Transform child in _animatorRoot) CollectPaths(child, child.name); } private void CollectPaths(Transform t, string relativePath) { _validPaths.Add(relativePath); _allPaths.Add(relativePath); foreach (Transform child in t) CollectPaths(child, relativePath + "/" + child.name); } /// /// 动画仍从旧根节点名开始,但该节点已变成 Animator 下某个子物体的后代(多包了一层父节点)。 /// private string TryDetectParentPrefix(string oldPath) { var firstSegment = oldPath.Split('/')[0]; if (_animatorRoot.Find(firstSegment) != null) return ""; foreach (Transform child in _animatorRoot) { if (child.Find(firstSegment) == null) continue; var prefix = child.name + "/"; if (_validPaths.Contains(prefix + oldPath)) return prefix; } return ""; } private static string NormalizePrefix(string prefix) { if (string.IsNullOrWhiteSpace(prefix)) return ""; prefix = prefix.Replace('\\', '/').Trim('/'); return string.IsNullOrEmpty(prefix) ? "" : prefix + "/"; } private static IEnumerable GetAllBindings(AnimationClip clip) { foreach (var b in AnimationUtility.GetCurveBindings(clip)) yield return b; foreach (var b in AnimationUtility.GetObjectReferenceCurveBindings(clip)) yield return b; } } } #endif