MiniGames/Assets/Editor/Animator_NoBlendTrees.cs
2025-08-14 20:29:09 +05:00

291 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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