关于 unity-animation
面向 Unity 开发者的动画技能,涵盖 Mecanim、Animator Controller、Blend Tree、Timeline 和动画脚本。内容通过设置说明与代码示例介绍系统组件和工作流程。
主要功能
- 01Mecanim 组件与动画类型
- 02Animator 状态、转换与参数
- 03Blend Tree 配置
- 04Avatar 设置与重定向
- 05Root Motion 与动画事件
- 06Timeline 与动画脚本
适用场景
- 01构建角色移动与状态流程
- 02在 Humanoid 骨架间重定向动画
- 03在动画播放期间触发事件
- 04使用 Timeline 编排影片序列
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-animation安装后在 Claude Code 中输入/unity-animation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/animation-clips.mdanimator-controller.mdtimeline.md详细介绍
Unity 动画系统
动画系统概述
Unity 的 Mecanim 动画系统由三个相互关联的组件构成:
- 动画片段 -- 独立的动作片段(Idle、Walk、Run)
- Animator Controller -- 状态机,将动画片段组织成包含状态和过渡的流程图
- Avatar 系统 -- 将人形角色骨架映射到通用的内部格式,以便重定向动画
Animator 组件附加到 GameObject 上,并引用播放所需的 Animator Controller 和 Avatar 资源。
动画类型
- Humanoid -- 需要配置 Avatar;支持在不同角色骨架之间重定向动画;CPU 开销比 Generic 高 15–20%
- Generic -- 对特定层级中的 Transform 或 MonoBehaviour 属性进行动画处理;无法在不同层级之间迁移
- Legacy -- 较旧的 Animation 组件;适用于简单的单次播放或 UI 动画
Animator Controller
Animator Controller 资源会为角色或带动画的 GameObject 安排 Animation Clips 和 Transitions。
创建: 在 Project 窗口中右键 > Create > Animator Controller
关键组件
- 状态 -- 每个状态播放关联的 Animation Clip 或 Blend Tree
- 过渡 -- 定义状态机如何以及何时在状态之间切换
- 参数 -- 脚本可设置以控制过渡的变量(Float、Int、Bool、Trigger)
- 层 -- 用于不同身体部位或动画用途的独立状态机
- 子状态机 -- 用于分层组织的嵌套状态机
参数
共有四种类型:
| 类型 | 描述 | 脚本方法 |
|---|---|---|
| Float | 小数 | SetFloat() / GetFloat() |
| Int | 整数 | SetInteger() / GetInteger() |
| Bool | 真/假 | SetBool() / GetBool() |
| Trigger | 自动重置的布尔值 | SetTrigger() / ResetTrigger() |
using UnityEngine;
public class PlayerAnimController : MonoBehaviour
{
Animator animator;
void Start()
{
animator = GetComponent<Animator>();
}
void Update()
{
// Note: Uses legacy Input Manager for simplicity. See unity-input for the new Input System.
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
bool fire = Input.GetButtonDown("Fire1");
animator.SetFloat("Forward", v);
animator.SetFloat("Strafe", h);
animator.SetBool("Fire", fire);
}
void OnCollisionEnter(Collision col)
{
if (col.gameObject.CompareTag("Enemy"))
{
animator.SetTrigger("Die");
}
}
}
Animator Override Controller
替换 Animator Controller 中的动画片段,同时保留其结构、参数和逻辑。适用于多个角色共用同一状态机但使用不同动画片段的情况。
重要: 使用 Override Controller 时,请将过渡退出时间设为标准化时间(而非秒数),否则,如果覆盖动画片段的时长不同,退出时间可能会被忽略。
层
- Override 模式 -- 替换之前各层中的动画
- Additive 模式 -- 在之前各层的动画基础上叠加动画
- Avatar Mask -- 限制某一层作用于特定身体部位(例如,仅上半身)
- 同步层 -- 使用不同动画片段复用状态机结构
状态机和过渡
状态
Animator Controller 中的每个状态都代表一个独立动作。特殊状态包括:
- Entry -- 默认入口
- Any State -- 从任何当前状态进行过渡
- Exit -- 退出当前状态机或子状态机
过渡
过渡定义状态之间如何融合。
| 设置 | 描述 |
|---|---|
| Has Exit Time | 在标准化时间点触发过渡(例如,0.75 = 已完成 75%) |
| Transition Duration | 融合时长;以秒为单位(Fixed Duration),或为源状态时长的一部分 |
| Transition Offset | 目标状态开始播放的位置(0.5 = 中点) |
| Conditions | 基于参数的规则;必须同时满足所有规则 |
| Interruption Source | 可中断过渡的来源:None、Current State、Next State,或它们的组合 |
| Ordered Interruption | 决定过渡解析是在当前过渡处停止,还是继续检查任何有效过渡 |
同时设置 Has Exit Time 和 Conditions 时,Unity 只会在退出时间之后检查条件。
状态机行为
继承 StateMachineBehaviour 的脚本会附加到状态上。回调包括:OnStateEnter、OnStateUpdate、OnStateExit、OnStateMove、OnStateIK。它们都会接收 (Animator animator, AnimatorStateInfo stateInfo, int layerIndex)。
public class AttackState : StateMachineBehaviour
{
public AudioClip attackSound;
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
AudioSource.PlayClipAtPoint(attackSound, animator.transform.position);
}
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// Cleanup when leaving state
}
}
混合树
混合树根据参数值,在多个相似动画之间平滑混合;与之不同,过渡会在一段时间内从一个独立状态切换到另一个状态。
关键要求: 动画必须性质相似且时序一致。脚部接触点应在归一化时间上对齐(例如,左脚位于 0.0,右脚位于 0.5)。
混合类型
- 1D -- 单个参数控制混合(例如,用速度控制走路/跑步)
- 2D Simple Directional -- 两个参数,每个方向对应一个动作
- 2D Freeform Directional -- 两个参数,每个方向对应多个动作
- 2D Freeform Cartesian -- 两个参数,动作不代表方向
- Direct -- 每个动作都有自己的权重参数(面部动画)
创建: 在 Animator Controller 中右键单击 > Create State > From New Blend Tree。双击进入图表。通过 Inspector 添加子动作。
// Driving a locomotion blend tree from script
void Update()
{
float speed = new Vector3(rb.velocity.x, 0, rb.velocity.z).magnitude;
float direction = Vector3.SignedAngle(transform.forward,
rb.velocity.normalized, Vector3.up);
animator.SetFloat("Speed", speed, 0.1f, Time.deltaTime);
animator.SetFloat("Direction", direction, 0.1f, Time.deltaTime);
}
Avatar 和 Humanoid 骨架
Avatar 系统会识别模型是否为人形,并映射身体部位以进行动画重定向。
核心概念
- Avatar -- 将骨骼结构映射到 Unity 的内部人形格式
- 重定向 -- 共享相同 Avatar 映射的不同人形骨架之间可以转移动画
- 肌肉定义 -- 在肌肉空间而非骨骼空间中直观地控制
设置
- 在 Project 窗口中选择模型
- 在 Import Settings 的 Rig 选项卡中,将 Animation Type 设置为 Humanoid
- 配置 Avatar 映射(Unity 会在可能的情况下自动映射)
- 在 Avatar Configuration 窗口中验证并调整骨骼分配
根运动
根运动会将动画驱动的移动传递到 GameObject 的 Transform。
架构
- 身体 Transform -- 角色的重心;存储世界空间曲线
- 根 Transform -- 身体 Transform 在 Y 平面上的投影;每帧在运行时计算
动画片段检查器设置
| 设置 | 用途 |
|---|---|
| Bake Into Pose (Rotation) | 朝向保留在身体上;GameObject 不会接收旋转 |
| Bake Into Pose (Y) | 垂直运动保留在身体上;除跳跃动画外,均应启用 |
| Bake Into Pose (XZ) | 水平运动保留在身体上;应为待机动画启用,以防止漂移 |
| Based Upon | Body Orientation(动作捕捉)、Original(关键帧动画)、Feet(防止浮空) |
Animator.gravityWeight 由 Bake Into Pose Position Y 控制:启用时为 1,禁用时为 0。
// Custom root motion handling
using UnityEngine;
[RequireComponent(typeof(Animator))]
public class RootMotionController : MonoBehaviour
{
Animator animator;
CharacterController controller;
void Start()
{
animator = GetComponent<Animator>();
controller = GetComponent<CharacterController>();
animator.applyRootMotion = false; // We handle it manually
}
void OnAnimatorMove()
{
// Apply root motion through CharacterController
Vector3 deltaPosition = animator.deltaPosition;
deltaPosition.y -= 9.81f * Time.deltaTime; // Add gravity
controller.Move(deltaPosition);
transform.rotation *= animator.deltaRotation;
}
}
动画事件
动画事件会在动画时间轴上的指定位置触发函数。
参数类型
- Float -- 数值(例如,音量)
- Int -- 整数值
- String -- 文本数据
- Object -- GameObject 或 Prefab 引用
设置
- 在 Animation 选项卡中展开 Events 部分
- 将播放头定位到所需帧
- 单击 Add Event
- 设置与已附加脚本上的方法匹配的函数名称
using UnityEngine;
public class FootstepHandler : MonoBehaviour
{
public AudioClip[] footstepSounds;
public GameObject dustPrefab;
// Called by animation event -- function name must match event
public void PlayFootstep(int footIndex)
{
if (footstepSounds.Length > 0)
{
int clipIndex = Random.Range(0, footstepSounds.Length);
AudioSource.PlayClipAtPoint(footstepSounds[clipIndex], transform.position);
}
}
// Called by animation event passing an Object parameter
public void SpawnEffect(Object effectPrefab)
{
Instantiate((GameObject)effectPrefab, transform.position, Quaternion.identity);
}
}
Timeline
Timeline 可用于创建电影内容、游戏玩法序列、音频序列和粒子效果。包:com.unity.timeline(v1.8.11,与 Unity 6.3 兼容)。
核心组件
- Timeline Asset -- 定义轨道、片段及其排列方式
- Timeline Instance -- 绑定到特定场景对象的运行时实例
- Playable Director -- 播放 Timeline 资源并将轨道绑定到场景对象的组件
轨道类型
| 轨道 | 用途 |
|---|---|
| Animation | 控制绑定的 GameObject 上的 Animator |
| Audio | 在绑定的 AudioSource 上播放 AudioClip |
| Activation | 启用/禁用绑定的 GameObject |
| Signal | 通过 SignalReceiver 在特定时间触发事件 |
| Control | 触发子 Timeline、粒子系统或其他 Playable Director |
| Playable | 使用 Playables API 的自定义轨道 |
功能
- 直接在 Timeline 中录制动画
- 支持 Humanoid 动画
- Animation Override 轨道
- 用于模块化组合的子 Timeline
动画脚本编写
反向运动学(IK)
要求:Humanoid Avatar,且已在层上启用 IK Pass。
using UnityEngine;
public class IKController : MonoBehaviour
{
Animator animator;
public bool ikActive = true;
public Transform rightHandTarget;
public Transform lookTarget;
void Start()
{
animator = GetComponent<Animator>();
}
void OnAnimatorIK()
{
if (animator == null || !ikActive) return;
// Look at target
animator.SetLookAtWeight(1f);
animator.SetLookAtPosition(lookTarget.position);
// Right hand IK
animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f);
animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandTarget.position);
animator.SetIKRotation(AvatarIKGoal.RightHand, rightHandTarget.rotation);
}
}
直接播放状态
// Play state immediately
animator.Play("Attack");
// Play with normalized time offset (start at 50%)
animator.Play("Attack", 0, 0.5f);
// Crossfade into state over 0.25 seconds
animator.CrossFadeInFixedTime("Run", 0.25f);
// Crossfade using normalized time
animator.CrossFade("Run", 0.1f);
查询状态信息
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
if (stateInfo.IsName("Attack"))
{
Debug.Log("Currently attacking");
}
if (stateInfo.normalizedTime >= 1.0f && !animator.IsInTransition(0))
{
Debug.Log("Animation finished");
}
// Use hash for performance
int runHash = Animator.StringToHash("Run");
if (animator.HasState(0, runHash))
{
animator.Play(runHash);
}
目标匹配
// Match character's hand to a ledge position during climb animation
animator.MatchTarget(ledgePosition, Quaternion.identity, AvatarTarget.RightHand,
new MatchTargetWeightMask(Vector3.one, 0f), 0.1f, 0.4f);
常见模式
角色移动控制器
using UnityEngine;
[RequireComponent(typeof(Animator))]
public class LocomotionController : MonoBehaviour
{
Animator animator;
static readonly int SpeedHash = Animator.StringToHash("Speed");
static readonly int GroundedHash = Animator.StringToHash("Grounded");
static readonly int JumpHash = Animator.StringToHash("Jump");
void Start()
{
animator = GetComponent<Animator>();
}
void Update()
{
float speed = Input.GetAxis("Vertical");
animator.SetFloat(SpeedHash, Mathf.Abs(speed), 0.1f, Time.deltaTime);
animator.SetBool(GroundedHash, IsGrounded());
if (Input.GetButtonDown("Jump") && IsGrounded())
{
animator.SetTrigger(JumpHash);
}
}
bool IsGrounded()
{
return Physics.Raycast(transform.position + Vector3.up * 0.1f, Vector3.down, 0.2f);
}
}
运行时替换 Override Controller
using UnityEngine;
public class WeaponAnimSwap : MonoBehaviour
{
public AnimatorOverrideController swordOverride;
public AnimatorOverrideController bowOverride;
Animator animator;
void Start()
{
animator = GetComponent<Animator>();
}
public void EquipSword()
{
animator.runtimeAnimatorController = swordOverride;
}
public void EquipBow()
{
animator.runtimeAnimatorController = bowOverride;
}
}
反模式
| 反模式 | 问题 | 解决方案 |
|---|---|---|
| 每帧使用字符串参数 | GC 分配、查找速度慢 | 使用 Animator.StringToHash() 缓存哈希值 |
| 将 Humanoid 骨架用于非人形对象 | 造成 15-20% 不必要的 CPU 开销 | 对道具、动物和特效使用 Generic 动画类型 |
| 在共享根对象下放置多个 Animator | 每个层级只能进行单线程写入,阻碍并行处理 | 为每个 Animator 分配独立的根 GameObject |
| 过度使用 StateMachineBehaviour | 会创建同步点,破坏并行求值 | 尽量减少 SMB 回调;优先使用脚本端逻辑 |
| 通过 Animator 为 RectTransform 设置动画 | 会导致确定性写入问题 | 使用旧版 Animation 组件为 UI 设置动画 |
| 为单次播放动画创建复杂状态机 | 即使在空闲状态下也会持续评估转换 | 改用 Animation 组件或 Playables API |
手动调用 Animator.Update() | 会绕过并行执行 | 使用 PlayableGraph 进行手动控制,同时保留并行处理 |
| 未将转换退出时间设为 normalized | Override Controller 片段可能会跳过转换 | 始终使用 normalized time 设置退出时间 |
更换控制器后忘记调用 Rebind() | 绑定信息过时会导致播放错误 | 在运行时更换控制器时调用 Rebind() |
| 空闲片段未启用 Bake Into Pose(XZ) | 空闲动画会累积漂移 | 为静止片段启用 Bake Into Pose XZ |
核心 API 速查
Animator —— 核心属性
applyRootMotion、speed、updateMode(Normal/AnimatePhysics/UnscaledTime)、cullingMode、deltaPosition、deltaRotation、velocity、isHuman、layerCount、bodyPosition、gravityWeight
Animator —— 核心方法
- 播放:
Play(state, layer, time)、CrossFade(state, duration)、CrossFadeInFixedTime(state, duration) - 参数:
SetFloat/Int/Bool/Trigger(name, value)、GetFloat/Int/Bool(name)、ResetTrigger(name) - 状态查询:
GetCurrentAnimatorStateInfo(layer)、GetNextAnimatorStateInfo(layer)、IsInTransition(layer) - IK:
SetIKPosition/Rotation(goal, value)、SetIKPositionWeight/RotationWeight(goal, weight)、SetLookAtPosition(pos) - 实用工具:
MatchTarget(...)、GetBoneTransform(bone)、Rebind()、StringToHash(name)、SetLayerWeight(layer, weight)
相关技能
- unity-foundations —— Unity 核心概念、GameObject、组件、项目结构
- unity-scripting —— MonoBehaviour 生命周期、C# 模式、事件系统
- unity-graphics —— 渲染、材质、着色器(与动画材质属性相关)