MiniGames/Assets/Editor/Animator_NoBlendTrees.cs

291 lines
10 KiB
C#
Raw Permalink Normal View History

2025-08-14 20:29:09 +05:00
#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 (dont 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 (well 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