#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEngine; public static class Animator_NoBlendTrees { [MenuItem("Tools/Animator/Convert 1D BlendTrees → States (Copy Controller)")] public static void ConvertSelected() { var src = Selection.activeObject as AnimatorController; if (!src) { EditorUtility.DisplayDialog("Select AnimatorController", "Select an AnimatorController in the Project window and run this again.", "OK"); return; } // Duplicate controller var srcPath = AssetDatabase.GetAssetPath(src); var dir = Path.GetDirectoryName(srcPath).Replace('\\','/'); var name = Path.GetFileNameWithoutExtension(srcPath); var dstPath = AssetDatabase.GenerateUniqueAssetPath($"{dir}/{name}_NoBT.controller"); if (!AssetDatabase.CopyAsset(srcPath, dstPath)) { EditorUtility.DisplayDialog("Error", "Could not duplicate controller.", "OK"); return; } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); var ctrl = AssetDatabase.LoadAssetAtPath(dstPath); if (!ctrl) { Debug.LogError("Failed to load duplicated controller."); return; } int totalTrees = 0, converted = 0, skipped2D = 0; // Process every layer & sub-state machine foreach (var layer in ctrl.layers) { converted += ConvertBlendTreesInStateMachine(layer.stateMachine, ref totalTrees, ref skipped2D); } EditorUtility.SetDirty(ctrl); AssetDatabase.SaveAssets(); Debug.Log($"[NoBT] Done → {dstPath}\n" + $"Total BT found: {totalTrees}, Converted: {converted}, Skipped 2D: {skipped2D}"); EditorGUIUtility.PingObject(ctrl); } // Recursively scan a state machine static int ConvertBlendTreesInStateMachine(AnimatorStateMachine sm, ref int totalTrees, ref int skipped2D) { int convertedHere = 0; // First handle child state machines recursively foreach (var csm in sm.stateMachines) convertedHere += ConvertBlendTreesInStateMachine(csm.stateMachine, ref totalTrees, ref skipped2D); // Collect BT states first (don’t mutate while iterating) var btStates = new List(); foreach (var cs in sm.states) { if (cs.state.motion is BlendTree) btStates.Add(cs.state); } foreach (var btState in btStates) { var tree = btState.motion as BlendTree; totalTrees++; // Only support 1D trees if (!Is1DBlendTree(tree)) { Debug.LogWarning($"[NoBT] Skipping 2D or complex tree in state '{btState.name}'."); skipped2D++; continue; } // Gather leaf clips with thresholds (flatten nested 1D trees) var leaves = new List(); CollectLeaves1D(tree, leaves); if (leaves.Count == 0) { Debug.LogWarning($"[NoBT] No leaf clips found in tree '{btState.name}'. Skipping."); continue; } // Build thresholds/ranges leaves.Sort((a,b) => a.threshold.CompareTo(b.threshold)); var ranges = BuildRanges(leaves); // Create new states for each leaf var newStates = new List(); for (int i = 0; i < leaves.Count; i++) { var leaf = leaves[i]; string stateName = UniqueStateName(sm, $"{btState.name}_{San(leaf.clip.name)}"); var s = sm.AddState(stateName); s.motion = leaf.clip; s.writeDefaultValues = btState.writeDefaultValues; s.speed = btState.speed; newStates.Add(s); } // Copy transitions that pointed TO the BT state, // and add gating by the blend parameter to select the right leaf state. var incoming = FindIncomingTransitions(sm, btState); foreach (var inc in incoming) { foreach (var (state, range) in newStates.Zip(ranges, (st, rg) => (st, rg))) { var nt = CloneTransition(inc.fromState, inc.transition, state); // Add range conditions based on blend parameter AddRangeConditions(nt, tree.blendParameter, range); } // Remove original incoming transition RemoveTransition(inc.fromState, inc.transition); } // Copy AnyState transitions to the BT state (same gating) var anyIncoming = sm.anyStateTransitions.Where(t => t.destinationState == btState).ToArray(); foreach (var t in anyIncoming) { foreach (var (state, range) in newStates.Zip(ranges, (st, rg) => (st, rg))) { var nt = sm.AddAnyStateTransition(state); CopyTransitionSettings(t, nt); AddRangeConditions(nt, tree.blendParameter, range); } RemoveAnyStateTransition(sm, t); } // Copy transitions OUT of the BT state to each new state foreach (var t in btState.transitions.ToArray()) { foreach (var state in newStates) { var nt = state.AddTransition(t.destinationState); CopyTransitionSettings(t, nt); } RemoveTransition(btState, t); } // If BT state was default state, set a reasonable default (first leaf) if (sm.defaultState == btState && newStates.Count > 0) sm.defaultState = newStates[0]; // Finally remove the original BT state sm.RemoveState(btState); convertedHere++; } return convertedHere; } // ---------- Helpers ---------- struct Leaf { public AnimationClip clip; public float threshold; public Leaf(AnimationClip c, float t){clip=c; threshold=t;} } static bool Is1DBlendTree(BlendTree bt) { if (bt == null) return false; if (bt.blendType == BlendTreeType.Simple1D) return true; // Also allow nested 1D trees (we’ll recurse) foreach (var ch in bt.children) { if (ch.motion is BlendTree childBT && !Is1DBlendTree(childBT)) return false; } return true; } static void CollectLeaves1D(BlendTree bt, List outLeaves) { foreach (var ch in bt.children) { if (ch.motion is AnimationClip ac) outLeaves.Add(new Leaf(ac, ch.threshold)); else if (ch.motion is BlendTree childBT) CollectLeaves1D(childBT, outLeaves); } } // Build open/closed ranges around thresholds using midpoints // For N clips with thresholds t0 <= t1 <= ... <= tN-1: // state i range: (ti-1+ti)/2 < P < (ti+ti+1)/2; ends are open-ended struct Range { public float? min; public float? max; } static List BuildRanges(List leaves) { var t = leaves.Select(l => l.threshold).ToArray(); var ranges = new List(leaves.Count); for (int i = 0; i < leaves.Count; i++) { float? min = i == 0 ? (float?)null : 0.5f * (t[i-1] + t[i]); float? max = i == leaves.Count - 1 ? (float?)null : 0.5f * (t[i] + t[i+1]); ranges.Add(new Range { min = min, max = max }); } return ranges; } static string San(string s) { foreach (var c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_'); return s.Replace(' ', '_'); } static string UniqueStateName(AnimatorStateMachine sm, string baseName) { string name = baseName; int i = 1; while (sm.states.Any(cs => cs.state.name == name)) name = baseName + "_" + i++; return name; } // Find transitions whose destination is the target state (in the SAME state machine) struct Incoming { public AnimatorState fromState; public AnimatorStateTransition transition; } static List FindIncomingTransitions(AnimatorStateMachine sm, AnimatorState target) { var list = new List(); foreach (var cs in sm.states) { var from = cs.state; foreach (var t in from.transitions) if (t.destinationState == target) list.Add(new Incoming { fromState = from, transition = t }); } return list; } static AnimatorStateTransition CloneTransition(AnimatorState fromState, AnimatorStateTransition src, AnimatorState dstState) { var nt = fromState.AddTransition(dstState); CopyTransitionSettings(src, nt); return nt; } 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; // Copy conditions foreach (var c in src.conditions) dst.AddCondition(c.mode, c.threshold, c.parameter); } static void AddRangeConditions(AnimatorStateTransition t, string param, Range r) { if (!string.IsNullOrEmpty(param)) { if (r.min.HasValue) t.AddCondition(AnimatorConditionMode.Greater, r.min.Value, param); if (r.max.HasValue) t.AddCondition(AnimatorConditionMode.Less, r.max.Value, param); } } static void RemoveTransition(AnimatorState from, AnimatorStateTransition t) { var list = from.transitions.ToList(); list.Remove(t); #if UNITY_2020_1_OR_NEWER from.transitions = list.ToArray(); #else // older Unity auto-updates the array when using RemoveTransition; keeping fallback // (no-op) #endif } static void RemoveAnyStateTransition(AnimatorStateMachine sm, AnimatorStateTransition t) { var list = sm.anyStateTransitions.ToList(); list.Remove(t); sm.anyStateTransitions = list.ToArray(); } } #endif