#if UNITY_EDITOR using System; using System.IO; using System.Linq; using System.Collections.Generic; using Newtonsoft.Json; using UnityEditor; using UnityEditor.Animations; using UnityEditor.SceneManagement; using UnityEngine; public static class BuildXBotController { const string Common = "Assets/Bundles/Characters/Human/Common"; const string Model = Common + "/XBot.fbx"; const string Output = Common + "/XBot.controller"; static readonly string[] States = { "Idle", "Walk", "Attack01", "Attack02" }; static readonly string[] Files = { "X Bot@BreathingIdle.fbx", "X Bot@Walking.fbx", "X Bot@atk01.fbx", "X Bot@Atk02.fbx" }; static Avatar AvatarAt(string path) { var avatar = AssetDatabase.LoadAllAssetsAtPath(path).OfType().SingleOrDefault(); if (!avatar || !avatar.isValid || !avatar.isHuman) throw new Exception("Invalid Humanoid Avatar: " + path); return avatar; } static Dictionary Bones(GameObject model) { return model.GetComponentsInChildren(true).Where(t => t.name.StartsWith("mixamorig:")) .ToDictionary(t => t.name, t => t.parent && t.parent.name.StartsWith("mixamorig:") ? t.parent.name : ""); } static void Transition(AnimatorState from, AnimatorState to, string parameter, AnimatorConditionMode mode, float value, bool exit = false) { var t = from.AddTransition(to); t.hasExitTime = exit; t.exitTime = .95f; t.hasFixedDuration = true; t.duration = .15f; t.AddCondition(mode, value, parameter); } public static string Build() { if (EditorApplication.isPlayingOrWillChangePlaymode) throw new Exception("Edit Mode required."); if (File.Exists(Output)) throw new Exception("Controller already exists; inspect it before replacing: " + Output); foreach (string f in Files) if (!File.Exists(Common + "/AnimX/" + f)) throw new FileNotFoundException(f); AssetDatabase.ImportAsset(Model, ImportAssetOptions.ForceSynchronousImport); var modelImporter = (ModelImporter)AssetImporter.GetAtPath(Model); modelImporter.animationType = ModelImporterAnimationType.Human; modelImporter.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel; modelImporter.SaveAndReimport(); var avatar = AvatarAt(Model); var bones = Bones(AssetDatabase.LoadAssetAtPath(Model)); if (bones.Count != 65) throw new Exception("Expected XBot's 65 bones, got " + bones.Count); var clips = new AnimationClip[Files.Length]; for (int i = 0; i < Files.Length; i++) { string path = Common + "/AnimX/" + Files[i]; AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport); var source = AssetDatabase.LoadAssetAtPath(path); var sourceBones = Bones(source); if (sourceBones.Count != 65 || sourceBones.Any(b => !bones.ContainsKey(b.Key) || bones[b.Key] != b.Value)) throw new Exception("Source skeleton differs from XBot: " + path); if (source.GetComponentsInChildren(true).Length != 0) throw new Exception("Expected animation without mesh: " + path); var importer = (ModelImporter)AssetImporter.GetAtPath(path); importer.animationType = ModelImporterAnimationType.Human; importer.avatarSetup = ModelImporterAvatarSetup.CopyFromOther; importer.sourceAvatar = avatar; importer.importAnimation = true; importer.importCameras = false; importer.importLights = false; var settings = importer.clipAnimations; if (settings.Length == 0) settings = importer.defaultClipAnimations; if (settings.Length != 1) throw new Exception("Expected one clip: " + path); settings[0].name = States[i]; settings[0].loopTime = i < 2; settings[0].loopPose = i < 2; settings[0].lockRootRotation = true; settings[0].lockRootHeightY = true; settings[0].lockRootPositionXZ = true; settings[0].keepOriginalOrientation = true; settings[0].keepOriginalPositionY = true; settings[0].keepOriginalPositionXZ = true; importer.clipAnimations = settings; importer.SaveAndReimport(); clips[i] = AssetDatabase.LoadAllAssetsAtPath(path).OfType().Single(c => !c.name.StartsWith("__preview__")); if (!clips[i].humanMotion || clips[i].length <= 0) throw new Exception("Invalid Humanoid clip: " + path); } var controller = AnimatorController.CreateAnimatorControllerAtPath(Output); controller.AddParameter("Motion", AnimatorControllerParameterType.Int); controller.AddParameter("Attack01", AnimatorControllerParameterType.Trigger); controller.AddParameter("Attack02", AnimatorControllerParameterType.Trigger); var sm = controller.layers[0].stateMachine; sm.entryPosition = new Vector3(0, 0); sm.anyStatePosition = new Vector3(0, 220); var states = new AnimatorState[4]; for (int i = 0; i < states.Length; i++) { states[i] = sm.AddState(States[i], new Vector3(260 + (i % 2) * 280, (i / 2) * 220)); states[i].motion = clips[i]; states[i].iKOnFeet = i < 2; states[i].writeDefaultValues = false; } sm.defaultState = states[0]; Transition(states[0], states[1], "Motion", AnimatorConditionMode.Equals, 1); Transition(states[1], states[0], "Motion", AnimatorConditionMode.NotEqual, 1); for (int i = 2; i < 4; i++) { var attack = sm.AddAnyStateTransition(states[i]); attack.hasExitTime = false; attack.hasFixedDuration = true; attack.duration = .1f; attack.canTransitionToSelf = false; attack.AddCondition(AnimatorConditionMode.If, 0, States[i]); Transition(states[i], states[1], "Motion", AnimatorConditionMode.Equals, 1, true); Transition(states[i], states[0], "Motion", AnimatorConditionMode.NotEqual, 1, true); } EditorUtility.SetDirty(controller); AssetDatabase.SaveAssetIfDirty(controller); return Verify(); } static void Advance(Animator animator, float duration) { int count = Mathf.CeilToInt(duration * 60); for (int i = 0; i < count; i++) animator.Update(1f / 60); } static void Expect(Animator animator, string state) { if (!animator.GetCurrentAnimatorStateInfo(0).IsName("Base Layer." + state) || animator.IsInTransition(0)) throw new Exception("Expected settled state " + state); } public static string Verify() { var controller = AssetDatabase.LoadAssetAtPath(Output); if (!controller || controller.layers.Length != 1) throw new Exception("Missing controller/layer"); var sm = controller.layers[0].stateMachine; if (sm.states.Length != 4 || sm.defaultState.name != "Idle") throw new Exception("Wrong states/default"); var scene = EditorSceneManager.NewPreviewScene(); var samples = new List(); var checks = new List(); try { var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Model), scene); var animator = go.GetComponent(); animator.avatar = AvatarAt(Model); animator.runtimeAnimatorController = controller; animator.applyRootMotion = false; animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; animator.Rebind(); animator.Update(0); foreach (var state in sm.states.Select(s => s.state)) { var clip = (AnimationClip)state.motion; Vector3 first = Vector3.zero; float movement = 0; for (int i = 0; i < 25; i++) { animator.Play("Base Layer." + state.name, 0, i / 25f); animator.Update(0); var hip = animator.GetBoneTransform(HumanBodyBones.Hips); var hand = animator.GetBoneTransform(HumanBodyBones.LeftHand).position - hip.position; var foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot).position - hip.position; if (i == 0) first = hand + foot; movement = Mathf.Max(movement, Vector3.Distance(first, hand + foot)); if (go.GetComponentsInChildren().Any(t => float.IsNaN(t.position.sqrMagnitude) || float.IsInfinity(t.position.sqrMagnitude))) throw new Exception("Invalid bone pose in " + state.name); } if (movement < .00001f) throw new Exception("No sampled movement in " + state.name); samples.Add(new { state = state.name, source = AssetDatabase.GetAssetPath(clip), clip.length, clip.frameRate, clip.humanMotion, clip.isLooping, samples = 25, sampledMovement = movement }); } animator.SetInteger("Motion", 0); animator.Play("Base Layer.Idle", 0, 0); animator.Update(0); animator.SetInteger("Motion", 1); Advance(animator, .4f); Expect(animator, "Walk"); checks.Add("Idle -> Walk"); animator.SetInteger("Motion", 0); Advance(animator, .4f); Expect(animator, "Idle"); checks.Add("Walk -> Idle"); foreach (int motion in new[] { 0, 1 }) foreach (string attack in States.Skip(2)) { animator.SetInteger("Motion", motion); animator.Play("Base Layer." + (motion == 1 ? "Walk" : "Idle"), 0, 0); animator.Update(0); animator.SetTrigger(attack); Advance(animator, .2f); Expect(animator, attack); float length = ((AnimationClip)sm.states.Single(s => s.state.name == attack).state.motion).length; Advance(animator, length + .4f); Expect(animator, motion == 1 ? "Walk" : "Idle"); checks.Add((motion == 1 ? "Walk" : "Idle") + " -> " + attack + " -> " + (motion == 1 ? "Walk" : "Idle")); } var report = new { controller = Output, model = Model, bones = Bones(go).Count, avatarValid = animator.avatar.isValid, avatarHuman = animator.avatar.isHuman, defaultState = "Idle", parameters = controller.parameters.Select(p => new { p.name, type = p.type.ToString() }), states = samples, transitions = checks, rootMotion = "Baked rotation/Y/XZ; consumer Animator.applyRootMotion=false", verification = "Isolated Editor Animator; no active scene edits" }; string json = JsonConvert.SerializeObject(report, Formatting.Indented); string folder = Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName, "Tools/output/common_bot_controller"); Directory.CreateDirectory(folder); File.WriteAllText(Path.Combine(folder, "validation.json"), json); return json; } finally { EditorSceneManager.ClosePreviewScene(scene); } } } #endif