#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 string headBone,neckBone,frontRightFoot,frontLeftFoot,hindRightFoot,hindLeftFoot,rightFoot,leftFoot; public float height,idleSeconds,idleBreath,attackSeconds,attackWindup,attackImpact,attackRecover; public float attackLeanDegrees,attackArmDegrees,attackLunge,attackReach,idleSourceWeight,attackSourceWeight,wingSpreadDegrees; public float attackHeadReach,attackHeadPitchDegrees,attackSpreadDegrees; } 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"; bool bird=r.profile=="avian_grounded"; bool turtle=r.profile=="quadruped_turtle"; bool heavy=r.profile=="quadruped_heavy"; bool quadruped=turtle || heavy; bool roar=r.attackStyle=="flame_roar"; if(r.profile!="biped_short" && !bird && !quadruped)throw new ArgumentException("Unsupported anatomy"); if(r.attackStyle!="body_slam" && r.attackStyle!="ranged_cast" && r.attackStyle!="wing_cast" && r.attackStyle!="turtle_bubble" && r.attackStyle!="braced_bash" && !roar)throw new ArgumentException("Unsupported attack style"); if(roar && r.profile!="biped_short")throw new ArgumentException("Flame roar requires an upright biped"); if(bird!=(r.attackStyle=="wing_cast") || (bird && !(r.wingSpreadDegrees>0 && r.wingSpreadDegrees<=60))) throw new ArgumentException("Invalid grounded-bird wing cast"); if(turtle!=(r.attackStyle=="turtle_bubble"))throw new ArgumentException("Invalid turtle bubble cast"); if(heavy!=(r.attackStyle=="braced_bash"))throw new ArgumentException("Invalid heavy quadruped bash"); 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 head=quadruped || roar ? bones.First(t=>t.name==r.headBone) : null; var neck=quadruped || roar ? bones.First(t=>t.name==r.neckBone) : null; var feet=(quadruped ? new[]{r.frontRightFoot,r.frontLeftFoot,r.hindRightFoot,r.hindLeftFoot} : roar ? new[]{r.rightFoot,r.leftFoot} : new string[0]).Select(n=>bones.First(t=>t.name==n)).ToArray(); var footPositions=feet.Select(t=>t.position).ToArray();var footRotations=feet.Select(t=>t.rotation).ToArray(); 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(!quadruped && (chest==null || arms.Any(t=>t==null) || arms[0]==arms[1])) throw new Exception("Missing/distinct profile bones required"); if((heavy || roar) && (chest==null || chest==head))throw new Exception("Independent chest and head required"); var bind=Pose.Read(bones);var bindRootPosition=root.position;float bindBottom=Bottom(model); var bindRootRotation=root.rotation; var wingSides=bird || roar ? arms.Select(a=>Mathf.Sign(a.position.x-root.position.x)).ToArray() : new float[0]; if((bird || roar) && wingSides[0]==wingSides[1])throw new Exception("Arm/wing roots must be on opposite sides"); 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