#if UNITY_EDITOR using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEngine; public static class AnimatorSingleLayerizer { [MenuItem("Tools/Animator/Make Single-Layer Copy (Selected Controller)")] public static void MakeSingleLayerCopy() { var srcCtrl = Selection.activeObject as AnimatorController; if (!srcCtrl) { EditorUtility.DisplayDialog("Select AnimatorController", "Select an AnimatorController in the Project view first.", "OK"); return; } var srcPath = AssetDatabase.GetAssetPath(srcCtrl); var dir = Path.GetDirectoryName(srcPath).Replace('\\','/'); var nameNoExt = Path.GetFileNameWithoutExtension(srcPath); var dstPath = AssetDatabase.GenerateUniqueAssetPath($"{dir}/{nameNoExt}_SingleLayer.controller"); if (!AssetDatabase.CopyAsset(srcPath, dstPath)) { EditorUtility.DisplayDialog("Copy failed", "Could not duplicate controller.", "OK"); return; } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); var dstCtrl = AssetDatabase.LoadAssetAtPath(dstPath); if (!dstCtrl) { EditorUtility.DisplayDialog("Load failed", "Could not load duplicated controller.", "OK"); return; } // Base layer to keep var baseLayer = dstCtrl.layers[0]; var baseSM = baseLayer.stateMachine; // Migrate all extra layers into Base as sub state machines var layers = dstCtrl.layers; var migrated = 0; for (int i = 1; i < layers.Length; i++) { var srcLayer = layers[i]; var sub = baseSM.AddStateMachine($"L_{srcLayer.name}"); CopyStateMachineRecursively(srcLayer.stateMachine, sub); migrated++; } // Remove extra layers (only Base remains) dstCtrl.layers = new[] { baseLayer }; EditorUtility.SetDirty(dstCtrl); AssetDatabase.SaveAssets(); Debug.Log($"[SingleLayerizer] Created: {dstPath}\nMigrated layers: {migrated}\nBase layer kept: {baseLayer.name}"); EditorGUIUtility.PingObject(dstCtrl); } // --------- Copy helpers --------- static void CopyStateMachineRecursively(AnimatorStateMachine src, AnimatorStateMachine dst) { // Maps to reconnect transitions var stateMap = new Dictionary(); var smMap = new Dictionary { { src, dst } }; // Copy child states foreach (var cs in src.states) { var s = cs.state; var newS = dst.AddState(s.name); newS.motion = s.motion; newS.speed = s.speed; newS.speedParameter = s.speedParameter; newS.speedParameterActive = s.speedParameterActive; newS.writeDefaultValues = s.writeDefaultValues; newS.mirror = s.mirror; newS.mirrorParameter = s.mirrorParameter; newS.mirrorParameterActive = s.mirrorParameterActive; #if UNITY_2021_2_OR_NEWER newS.timeParameter = s.timeParameter; newS.timeParameterActive = s.timeParameterActive; #endif stateMap[s] = newS; } // Copy child state machines (recursive) foreach (var csm in src.stateMachines) { var newSM = dst.AddStateMachine(csm.stateMachine.name); smMap[csm.stateMachine] = newSM; CopyStateMachineRecursively(csm.stateMachine, newSM); } // Default state if (src.defaultState && stateMap.TryGetValue(src.defaultState, out var mappedDefault)) dst.defaultState = mappedDefault; // AnyState transitions (only to states; ignore to sub-SMs) foreach (var t in src.anyStateTransitions) { if (t.destinationState && stateMap.TryGetValue(t.destinationState, out var to)) { var nt = dst.AddAnyStateTransition(to); CopyTransitionSettings(t, nt); } } // State → State transitions foreach (var cs in src.states) { var from = cs.state; if (!stateMap.TryGetValue(from, out var mappedFrom)) continue; foreach (var t in from.transitions) { if (t.destinationState && stateMap.TryGetValue(t.destinationState, out var to)) { var nt = mappedFrom.AddTransition(to); CopyTransitionSettings(t, nt); } } } // NOTE: Entry transitions and StateMachine → StateMachine transitions are skipped // because AnimatorTransition doesn't expose the same API surface. // DefaultState should cover standard entry behavior. } static void CopyTransitionSettings(AnimatorStateTransition src, AnimatorStateTransition dst) { dst.hasExitTime = src.hasExitTime; dst.exitTime = src.exitTime; dst.hasFixedDuration = src.hasFixedDuration; dst.duration = src.duration; dst.offset = src.offset; dst.interruptionSource = src.interruptionSource; dst.orderedInterruption = src.orderedInterruption; dst.canTransitionToSelf = src.canTransitionToSelf; // Conditions foreach (var c in src.conditions) dst.AddCondition(c.mode, c.threshold, c.parameter); } } #endif