#if UNITY_EDITOR using System; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEngine; using Object=UnityEngine.Object; /// Offline source-motion editing; saves assets, never adds runtime correction. public static class FinalizeEnemyMotions { [Serializable] public sealed class Recipe { public string id,assetRoot,rootBone,chestBone,rightArm,leftArm,outputDir,profile,attackStyle,attackEvent,usage; public float height,idleSeconds,idleBreath,attackSeconds,attackWindup,attackImpact,attackRecover; public float attackLeanDegrees,attackArmDegrees,attackLunge,attackReach,idleSourceWeight,attackSourceWeight; } sealed class Pose { public Vector3[] p,s; public Quaternion[] q; public static Pose Read(Transform[] b)=>new Pose { p=b.Select(t=>t.localPosition).ToArray(),s=b.Select(t=>t.localScale).ToArray(),q=b.Select(t=>t.localRotation).ToArray() }; public void Write(Transform[] b) { for(int i=0;iPath.GetFullPath(Path.Combine(Application.dataPath,"../..")); static float Smooth(float t)=>Mathf.SmoothStep(0,1,Mathf.Clamp01(t)); static float Strike(Recipe r,float t) { if(tb.localToWorldMatrix*bind[i]).ToArray(); return mesh.vertices.Select((v,i)=> { var w=weights[i];return matrices[w.boneIndex0].MultiplyPoint3x4(v)*w.weight0 +matrices[w.boneIndex1].MultiplyPoint3x4(v)*w.weight1+matrices[w.boneIndex2].MultiplyPoint3x4(v)*w.weight2 +matrices[w.boneIndex3].MultiplyPoint3x4(v)*w.weight3; }).ToArray(); } static float Bottom(GameObject model)=>model.GetComponentsInChildren().SelectMany(Vertices).Min(v=>v.y); static void Save(AnimationClip clip,Transform model,Transform[] bones,Pose[] poses,float seconds,bool loop) { Undo.RecordObject(clip,"Author character motion"); clip.ClearCurves();clip.frameRate=30; int count=poses.Length; for(int f=1;f(File.ReadAllText(Path.Combine(Repo,recipePath))); if(string.IsNullOrEmpty(r.attackStyle))r.attackStyle="body_slam"; if(string.IsNullOrEmpty(r.attackEvent))r.attackEvent=r.usage=="companion_visual" ? "OnCompanionAttackImpact" : "OnEnemyAttackImpact"; if(r.attackStyle!="body_slam" && r.attackStyle!="ranged_cast")throw new ArgumentException("Unsupported attack style"); if(r.height<=0 || !(0(r.assetRoot+"/Rigged/"+r.id+"_Rigged.fbx")); try { var anim=model.GetComponent();if(anim!=null) anim.enabled=false; var bones=model.GetComponentsInChildren().Where(t=>t!=model.transform && (t.name=="Armature" || t.name.StartsWith("bone_"))).ToArray(); var root=bones.First(t=>t.name==r.rootBone); var chest=bones.FirstOrDefault(t=>t.name==r.chestBone); var arms=new[] {bones.FirstOrDefault(t=>t.name==r.rightArm),bones.FirstOrDefault(t=>t.name==r.leftArm)}; if(r.profile=="biped_short" && (chest==null || arms.Any(t=>t==null))) throw new Exception("Missing profile bones"); var bind=Pose.Read(bones);var bindRootPosition=root.position;float bindBottom=Bottom(model); var skins=model.GetComponentsInChildren(); float unitScale=(skins.SelectMany(Vertices).Max(v=>v.y)-bindBottom)/r.height; var source=AssetDatabase.LoadAllAssetsAtPath(r.assetRoot+"/Anims/"+r.id+"_"+name+".fbx").OfType().First(c=>!c.name.StartsWith("__preview__")); source.SampleAnimation(model,0);var reference=Pose.Read(bones); bool idle=name=="Idle";float seconds=idle ? r.idleSeconds : r.attackSeconds; int count=Mathf.RoundToInt(seconds*30)+1;var poses=new Pose[count]; for(int f=0;f0 ? r.idleSourceWeight : .2f; float attackWeight=r.attackSourceWeight>0 ? r.attackSourceWeight : .35f; float weight=Mathf.Pow(Mathf.Sin(Mathf.PI*t),2)*(idle ? idleWeight : attackWeight); bind.Write(bones);source.SampleAnimation(model,t*source.length);var raw=Pose.Read(bones); for(int b=0;b(r.assetRoot+"/Clips/"+name+".anim"); Save(clip,model.transform,bones,poses,seconds,idle); if(!idle) { AnimationUtility.SetAnimationEvents(clip,new[] {new AnimationEvent { time=r.attackImpact*seconds,functionName=r.attackEvent,messageOptions=SendMessageOptions.DontRequireReceiver }}); } sb.AppendLine($"{name}: {seconds:F3}s loop={idle} start/end=bind generated source + offline {r.profile}/{r.attackStyle} authoring"); } finally {Object.DestroyImmediate(model);} } AssetDatabase.SaveAssets(); string report=Path.Combine(Repo,r.outputDir,"motion_processing.txt");Directory.CreateDirectory(Path.GetDirectoryName(report));File.WriteAllText(report,sb.ToString()); return sb.ToString(); } } #endif