// WL882_Anim.cs — PD 지시 #882 무기 기술 이동용 애니메이터 도구 (에디터 · Play 아님) // unity command run_script --file AgentScripts/WL882_Anim.cs --entry WL882_Anim.Describe --args '["Assets/Res_Addr/Animations/pcanim_onehand.controller","fatalblow"]' // unity command run_script --file AgentScripts/WL882_Anim.cs --entry WL882_Anim.CopyState --args '[src컨트롤러, src상태, dst컨트롤러, 새 상태, 전이 본보기 상태]' using System.Linq; using System.Text; using UnityEditor; using UnityEditor.Animations; using UnityEngine; public static class WL882_Anim { static AnimatorState Find(UnityEditor.Animations.AnimatorController c, string name, out AnimatorStateMachine owner, out int layer) { owner = null; layer = -1; for (int li = 0; li < c.layers.Length; li++) { var st = FindIn(c.layers[li].stateMachine, name, out owner); if (st != null) { layer = li; return st; } } return null; } static AnimatorState FindIn(AnimatorStateMachine sm, string name, out AnimatorStateMachine owner) { foreach (var cs in sm.states) if (cs.state.name == name) { owner = sm; return cs.state; } foreach (var sub in sm.stateMachines) { var r = FindIn(sub.stateMachine, name, out owner); if (r != null) return r; } owner = null; return null; } public static object Describe(string ctrlPath, string stateName) { var c = AssetDatabase.LoadAssetAtPath(ctrlPath); if (c == null) return "컨트롤러 없음 " + ctrlPath; var sb = new StringBuilder(); sb.Append("layers=").Append(c.layers.Length).Append(' '); foreach (var l in c.layers) sb.Append('[').Append(l.name).Append(" states=").Append(l.stateMachine.states.Length).Append(" any=").Append(l.stateMachine.anyStateTransitions.Length).Append("] "); AnimatorStateMachine owner; int layer; var s = Find(c, stateName, out owner, out layer); if (s == null) return sb.Append("\n상태 없음 ").Append(stateName).ToString(); var clip = s.motion as AnimationClip; sb.Append("\n").Append(stateName).Append(" layer=").Append(layer).Append(" sm=").Append(owner.name).Append(" motion=").Append(s.motion != null ? s.motion.name + "(" + AssetDatabase.GetAssetPath(s.motion) + ")" : "null") .Append(" len=").Append(clip != null ? clip.length.ToString("F2") : "-").Append(" speed=").Append(s.speed).Append(" speedParam=").Append(s.speedParameterActive ? s.speedParameter : "-") .Append(" tag=").Append(s.tag).Append(" wd=").Append(s.writeDefaultValues).Append(" mirror=").Append(s.mirror).Append(" iks=").Append(s.iKOnFeet) .Append(" behaviours=").Append(s.behaviours.Length); if (clip != null) foreach (var e in AnimationUtility.GetAnimationEvents(clip)) sb.Append(" ev[").Append(e.functionName).Append('@').Append(e.time.ToString("F2")).Append(']'); foreach (var t in s.transitions) sb.Append("\n → ").Append(t.destinationState != null ? t.destinationState.name : (t.isExit ? "EXIT" : "?")).Append(" exit=").Append(t.hasExitTime).Append('@').Append(t.exitTime.ToString("F2")) .Append(" dur=").Append(t.duration.ToString("F2")).Append(t.hasFixedDuration ? "s" : "n").Append(" off=").Append(t.offset).Append(" cond=").Append(string.Join(",", t.conditions.Select(x => x.parameter + " " + x.mode + " " + x.threshold).ToArray())); return sb.ToString(); } /// src 컨트롤러의 상태 모션을 dst 컨트롤러에 새 상태로 넣는다 — 속도 · 태그 · 전이는 dst 의 본보기 상태(like)를 따른다. 이미 있으면 모션만 맞춘다. public static object CopyState(string srcCtrl, string srcState, string dstCtrl, string newState, string likeState) { var src = AssetDatabase.LoadAssetAtPath(srcCtrl); var dst = AssetDatabase.LoadAssetAtPath(dstCtrl); if (src == null || dst == null) return "컨트롤러 없음"; AnimatorStateMachine so, lo; int sl, ll; var s = Find(src, srcState, out so, out sl); var like = Find(dst, likeState, out lo, out ll); if (s == null || s.motion == null) return "원본 상태/모션 없음 " + srcState; if (like == null) return "본보기 상태 없음 " + likeState; AnimatorStateMachine eo; int el; var exist = Find(dst, newState, out eo, out el); if (exist != null) { exist.motion = s.motion; EditorUtility.SetDirty(dst); AssetDatabase.SaveAssets(); return "이미 있음 → 모션만 " + s.motion.name; } var pos = lo.states.First(x => x.state == like).position + new Vector3(0f, 60f, 0f); var ns = lo.AddState(newState, pos); ns.motion = s.motion; ns.speed = like.speed; ns.tag = like.tag; ns.writeDefaultValues = like.writeDefaultValues; ns.iKOnFeet = like.iKOnFeet; ns.speedParameterActive = like.speedParameterActive; ns.speedParameter = like.speedParameter; foreach (var t in like.transitions) { AnimatorStateTransition nt = t.isExit ? ns.AddExitTransition() : ns.AddTransition(t.destinationState); nt.hasExitTime = t.hasExitTime; nt.exitTime = t.exitTime; nt.duration = t.duration; nt.hasFixedDuration = t.hasFixedDuration; nt.offset = t.offset; nt.interruptionSource = t.interruptionSource; nt.orderedInterruption = t.orderedInterruption; nt.canTransitionToSelf = t.canTransitionToSelf; foreach (var cnd in t.conditions) nt.AddCondition(cnd.mode, cnd.threshold, cnd.parameter); } EditorUtility.SetDirty(dst); AssetDatabase.SaveAssets(); return "추가 " + newState + " ← " + s.motion.name + " (전이 " + like.transitions.Length + " · 본보기 " + likeState + ")"; } }