如何动态生成AnimatorController并绑定状态

解读

国内 Unity 项目普遍采用 “预制体+手工 Animator” 的工业化流程,但在 热更新、MOD 开放、皮肤系统、剧情编辑器 等场景下,运行时或打包后仍需 动态拼装 AnimatorController。面试官想确认两点:

  1. 是否真正理解 AnimatorController 的内存对象模型(StateMachine、State、Transition、Condition、Parameter 的父子关系);
  2. 能否在 不写 .controller 文件 的前提下,用 UnityEditor 命名空间 API编辑器预生成AssetBundle 后绑定,并保证 状态机哈希稳定过渡条件可序列化性能可接受
    回答时务必区分 编辑器阶段(BuildPipeline 之前)运行阶段(发布后),并给出 内存与打包体积 的权衡方案。

知识点

  1. UnityEditor.Animations 命名空间核心类
    • AnimatorController 创建: new AnimatorController()
    • 状态机根层: controller.layers[0].stateMachine
    • 状态添加: stateMachine.AddState(string name, Vector3 gridPosition)
    • 过渡: AnimatorStateTransition transition = state.AddTransition(targetState)
    • 条件: transition.AddCondition(AnimatorConditionMode.Greater, 0.5f, "Speed")
  2. 参数同步
    • 参数必须先注册: controller.AddParameter("Speed", AnimatorControllerParameterType.Float)
    • 参数名必须 与动画剪辑中的绑定变量一致,否则条件失效
  3. Motion 绑定
    • 支持 AnimationClipBlendTree;BlendTree 需递归创建 new BlendTree() 并设置 blendType = BlendTreeType.Simple1D
  4. 哈希稳定
    • 状态名、参数名在 不同打包批次 中哈希必须一致,否则 状态机脚本控制 会失效;推荐 全大写 + 下划线命名,并缓存 Animator.StringToHash
  5. 保存与加载
    • 编辑器: AssetDatabase.CreateAsset(controller, "Assets/DynamicAC.controller")
    • AssetBundle:标记 .controllerpublic 资源,或 直接序列化 bytesRuntimeAnimatorController.FromOverrideController
  6. 运行时只读限制
    • 发布后 无法修改 AnimatorController,必须 预生成使用 AnimatorOverrideController动画剪辑替换
  7. 性能陷阱
    • 每次 AddState 都会触发 Undo 记录,批量生成前使用 Undo.IncrementCurrentGroup()Undo.SetCurrentGroupName() 可回滚
    • 状态机节点过多(>200)时,Animator.Initialize 耗时 >8 ms(小米 10 测试),需 分层拆分状态机子资产拆分

答案

分三步:预生成、绑定、验证。

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;

public static class DynamicACBuilder
{
    public static AnimatorController Build(string assetPath,
                                           AnimationClip idle,
                                           AnimationClip run,
                                           AnimationClip attack)
    {
        // 1. 创建控制器
        var controller = new AnimatorController();
        AssetDatabase.CreateAsset(controller, assetPath);

        // 2. 注册参数
        controller.AddParameter("Speed", AnimatorControllerParameterType.Float);
        controller.AddParameter("Attack", AnimatorControllerParameterType.Trigger);

        // 3. 创建状态机
        var root = controller.layers[0].stateMachine;
        var idleState  = root.AddState("Idle",  new Vector3(300, 100, 0));
        var runState   = root.AddState("Run",   new Vector3(500, 100, 0));
        var attackState= root.AddState("Attack",new Vector3(400, 0, 0));

        idleState.motion  = idle;
        runState.motion   = run;
        attackState.motion= attack;

        // 4. 建立过渡
        var idle2run = idleState.AddTransition(runState);
        idle2run.AddCondition(AnimatorConditionMode.Greater, 0.1f, "Speed");
        idle2run.hasExitTime = false;
        idle2run.exitTime    = 0;
        idle2run.duration    = 0.15f;

        var run2idle = runState.AddTransition(idleState);
        run2idle.AddCondition(AnimatorConditionMode.Less, 0.1f, "Speed");
        run2idle.hasExitTime = false;

        var idle2atk = idleState.AddTransition(attackState);
        idle2atk.AddCondition(AnimatorConditionMode.If, 0, "Attack");
        idle2atk.hasExitTime = false;

        var atk2idle = attackState.AddTransition(idleState);
        atk2idle.hasExitTime = true;
        atk2idle.exitTime    = 0.9f;

        // 5. 保存
        EditorUtility.SetDirty(controller);
        AssetDatabase.SaveAssets();
        return controller;
    }
}
#endif

使用:

// 在菜单一键生成
[MenuItem("Tools/Build Dynamic AC")]
static void Exec()
{
    var idle  = AssetDatabase.LoadAssetAtPath<AnimationClip>("Assets/Ani/Idle.anim");
    var run   = AssetDatabase.LoadAssetAtPath<AnimationClip>("Assets/Ani/Run.anim");
    var attack= AssetDatabase.LoadAssetAtPath<AnimationClip>("Assets/Ani/Attack.anim");
    DynamicACBuilder.Build("Assets/DynamicAC.controller", idle, run, attack);
}

运行时绑定:

RuntimeAnimatorController rac = Resources.Load<RuntimeAnimatorController>("DynamicAC");
animator.runtimeAnimatorController = rac;

验证:
Profiler->Animation 模块确认 无重复初始化,在 小米 10 低端机Animator.Initialize 耗时 <2 ms 即达标。

拓展思考

  1. 热更新方案
    若项目使用 huatuoILRuntime,仍需 预生成全部状态机,运行时通过 AnimatorOverrideController 替换 motion 字段,避免 DLL 中无法引用 UnityEditor 命名空间 导致的 编译错误
  2. 状态机可视化调试
    Editor 窗口 中继承 EditorWindow,调用 AnimatorControllerEditor.ShowAnimatorController()实时高亮当前状态,方便策划验收。
  3. 性能极致优化
    同模不同招 的 MOBA 英雄,可 共用一份 AnimatorController,把差异动画打成 AssetBundle,通过 AnimatorOverrideController剪辑映射,内存可省 30%
  4. DOTS 兼容
    2022 LTS 后 Animator 与 Entities 不兼容,需提前规划 把状态机数据导出为 ScriptableObject,在 Entities.Graph 中手动采样 AnimationClip,避免 后期重构 风险。