Files
AIGame/AIClient/AgentScripts/BuildYBotController.cs
T

169 lines
11 KiB
C#

#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 BuildYBotController
{
const string Common = "Assets/Bundles/Characters/Human/Common";
const string Model = Common + "/YBot.fbx";
const string Output = Common + "/YBot.controller";
static readonly string[] States = { "Idle", "Walk", "Attack01", "Attack02" };
static readonly string[] Files = { "Y Bot@BreathingIdle.fbx", "Y Bot@Walking.fbx", "Y Bot@Atk01.fbx", "Y Bot@atk02.fbx" };
static Avatar AvatarAt(string path)
{
var avatar = AssetDatabase.LoadAllAssetsAtPath(path).OfType<Avatar>().SingleOrDefault();
if (!avatar || !avatar.isValid || !avatar.isHuman) throw new Exception("Invalid Humanoid Avatar: " + path);
return avatar;
}
static Dictionary<string, string> Bones(GameObject model)
{
return model.GetComponentsInChildren<Transform>(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 + "/AnimY/" + 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<GameObject>(Model));
if (bones.Count != 65) throw new Exception("Expected YBot's 65 bones, got " + bones.Count);
var clips = new AnimationClip[Files.Length];
for (int i = 0; i < Files.Length; i++)
{
string path = Common + "/AnimY/" + Files[i];
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
var source = AssetDatabase.LoadAssetAtPath<GameObject>(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 YBot: " + path);
if (source.GetComponentsInChildren<Renderer>(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<AnimationClip>().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<AnimatorController>(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<object>(); var checks = new List<string>();
try
{
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Model), scene);
var animator = go.GetComponent<Animator>();
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<Transform>().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_ybot_controller");
Directory.CreateDirectory(folder); File.WriteAllText(Path.Combine(folder, "validation.json"), json);
return json;
}
finally { EditorSceneManager.ClosePreviewScene(scene); }
}
}
#endif