291 lines
10 KiB
C#
291 lines
10 KiB
C#
#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<AnimatorController>(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<AnimatorState>();
|
||
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<Leaf>();
|
||
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<AnimatorState>();
|
||
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<Leaf> 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<Range> BuildRanges(List<Leaf> leaves)
|
||
{
|
||
var t = leaves.Select(l => l.threshold).ToArray();
|
||
var ranges = new List<Range>(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<Incoming> FindIncomingTransitions(AnimatorStateMachine sm, AnimatorState target)
|
||
{
|
||
var list = new List<Incoming>();
|
||
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
|