如何动态生成AnimatorController并绑定状态
解读
国内 Unity 项目普遍采用 “预制体+手工 Animator” 的工业化流程,但在 热更新、MOD 开放、皮肤系统、剧情编辑器 等场景下,运行时或打包后仍需 动态拼装 AnimatorController。面试官想确认两点:
- 是否真正理解 AnimatorController 的内存对象模型(StateMachine、State、Transition、Condition、Parameter 的父子关系);
- 能否在 不写 .controller 文件 的前提下,用 UnityEditor 命名空间 API 在 编辑器预生成 或 AssetBundle 后绑定,并保证 状态机哈希稳定、过渡条件可序列化、性能可接受。
回答时务必区分 编辑器阶段(BuildPipeline 之前) 与 运行阶段(发布后),并给出 内存与打包体积 的权衡方案。
知识点
- 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")
- AnimatorController 创建:
- 参数同步
- 参数必须先注册:
controller.AddParameter("Speed", AnimatorControllerParameterType.Float) - 参数名必须 与动画剪辑中的绑定变量一致,否则条件失效
- 参数必须先注册:
- Motion 绑定
- 支持
AnimationClip、BlendTree;BlendTree 需递归创建new BlendTree()并设置blendType = BlendTreeType.Simple1D
- 支持
- 哈希稳定
- 状态名、参数名在 不同打包批次 中哈希必须一致,否则 状态机脚本控制 会失效;推荐 全大写 + 下划线命名,并缓存
Animator.StringToHash
- 状态名、参数名在 不同打包批次 中哈希必须一致,否则 状态机脚本控制 会失效;推荐 全大写 + 下划线命名,并缓存
- 保存与加载
- 编辑器:
AssetDatabase.CreateAsset(controller, "Assets/DynamicAC.controller") - AssetBundle:标记
.controller为public资源,或 直接序列化 bytes 后RuntimeAnimatorController.FromOverrideController
- 编辑器:
- 运行时只读限制
- 发布后 无法修改 AnimatorController,必须 预生成 或 使用 AnimatorOverrideController 做 动画剪辑替换
- 性能陷阱
- 每次
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 即达标。
拓展思考
- 热更新方案
若项目使用 huatuo 或 ILRuntime,仍需 预生成全部状态机,运行时通过 AnimatorOverrideController 替换motion字段,避免 DLL 中无法引用 UnityEditor 命名空间 导致的 编译错误。 - 状态机可视化调试
在 Editor 窗口 中继承EditorWindow,调用AnimatorControllerEditor.ShowAnimatorController()可 实时高亮当前状态,方便策划验收。 - 性能极致优化
对 同模不同招 的 MOBA 英雄,可 共用一份 AnimatorController,把差异动画打成 AssetBundle,通过 AnimatorOverrideController 做 剪辑映射,内存可省 30%。 - DOTS 兼容
2022 LTS 后 Animator 与 Entities 不兼容,需提前规划 把状态机数据导出为 ScriptableObject,在 Entities.Graph 中手动采样 AnimationClip,避免 后期重构 风险。