Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
PyTorch系列文章目录
Python系列文章目录
C#系列文章目录
01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互
15-C#入门 Day15:彻底搞懂数组!从基础到游戏子弹管理实战
16-C# List 从入门到实战:掌握动态数组,轻松管理游戏敌人列表 (含代码示例)
17-C# 字典 (Dictionary) 完全指南:从入门到游戏属性表实战 (Day 17)
18-C#游戏开发【第18天】 | 深入理解队列(Queue)与栈(Stack):从基础到任务队列实战
19-【C# 进阶】深入理解枚举 Flags 属性:游戏开发中多状态组合的利器
20-C#结构体(Struct)深度解析:轻量数据容器与游戏开发应用 (Day 20)
21-Unity数据持久化进阶:告别硬编码,用ScriptableObject优雅管理游戏配置!(Day 21)
22-Unity C# 健壮性编程:告别崩溃!掌握异常处理与调试的 4 大核心技巧 (Day 22)
23-C#代码解耦利器:委托与事件(Delegate & Event)从入门到实践 (Day 23)
24-Unity脚本通信终极指南:从0到1精通UnityEvent与事件解耦(Day 24)
25-精通C# Lambda与LINQ:Unity数据处理效率提升10倍的秘诀! (Day 25)
26-# Unity C#进阶:掌握泛型编程,告别重复代码,编写优雅复用的通用组件!(Day26)
27-Unity协程从入门到精通:告别卡顿,用Coroutine优雅处理异步与时序任务 (Day 27)
28-搞定玩家控制!Unity输入系统、物理引擎、碰撞检测实战指南 (Day 28)
29-# Unity动画控制核心:Animator状态机与C#脚本实战指南 (Day 29)
文章目录
- Langchain系列文章目录
- PyTorch系列文章目录
- Python系列文章目录
- C#系列文章目录
- 前言
- 一、初识动画双雄:Animation与Animator窗口
- 1.1 Animation窗口:动画片段的诞生地
- 1.2 Animator窗口:动画逻辑的指挥中心
- 二、核心引擎:动画状态机(State Machine)详解
- 2.1 状态(State):角色行为的快照
- 2.2 过渡(Transition):状态切换的桥梁
- 2.2.1 设置过渡条件
- 2.2.2 过渡参数详解
- 2.3 参数(Parameter):驱动状态变化的钥匙
- 2.3.1 参数类型(Float, Int, Bool, Trigger)
- 2.3.2 参数的作用与选择
- 三、代码驱动:C#脚本与Animator的联动
- 3.1 获取Animator组件
- 3.2 通过脚本设置参数值
- 3.2.1 控制布尔/触发器
- 3.2.2 控制浮点/整数
- 3.3 监听动画状态(进阶)
- 四、精准卡点:动画事件(Animation Event)的应用
- 4.1 什么是动画事件?
- 4.2 如何添加动画事件
- 4.3 编写响应事件的脚本函数
- 4.4 应用场景举例
- 五、实战演练:打造动态角色
- 5.1 准备工作:导入动画片段与设置Animator
- 5.2 构建状态机:连接行走、跑步、跳跃、攻击
- 5.3 编写控制脚本:响应玩家输入
- 5.4 添加动画事件:实现攻击判定或音效
- 六、常见问题与排错
- 七、总结
前言
欢迎来到《C# for Unity开发者50天掌握》专栏的第29天!在前几周的学习中,我们已经掌握了C#的基础与面向对象编程,了解了Unity的核心机制如输入、物理与碰撞。今天,我们将进入一个让游戏角色栩栩如生的关键领域——动画控制。
静态的模型固然是游戏的基础,但真正赋予角色生命力的是流畅自然的动画。Unity提供了强大的Animator
组件和配套的Animation
与Animator
窗口,让开发者能够构建复杂且响应迅速的动画逻辑。想象一下,你的角色能够根据玩家的输入平滑地从站立切换到行走、奔跑,在恰当的时机跃起或挥出武器,这正是本节课我们要实现的目标。
本节将重点介绍:
Animation
窗口与Animator
窗口的核心功能与区别。- 动画状态机(State Machine)的构成:状态(State)、过渡(Transition)、参数(Parameter)。
- 如何使用C#脚本与Animator组件交互,通过改变参数来驱动动画状态的切换。
- 动画事件(Animation Event)的概念及其在特定动画帧执行代码的应用。
- 最后,我们将通过一个实践案例,整合所学知识,为角色添加并控制行走、跑步、跳跃、攻击等基础动画。
准备好了吗?让我们一起揭开Unity动画系统的神秘面纱,让你的角色动起来!
一、初识动画双雄:Animation与Animator窗口
在Unity中处理动画,我们主要会接触到两个重要的窗口:Animation
窗口和Animator
窗口。理解它们各自的职责至关重要。
1.1 Animation窗口:动画片段的诞生地
Animation
窗口(通常通过 Window > Animation > Animation
或快捷键 Ctrl+6
打开)是你创建和编辑**动画片段(Animation Clip)**的地方。
- 时间轴(Timeline): 它提供了一个时间轴界面,你可以为选定游戏对象的属性(如位置、旋转、缩放、组件属性等)设置关键帧(Keyframe)。
- 关键帧编辑: 你可以在不同的时间点记录下属性的值,Unity会自动在关键帧之间进行插值计算,形成连续的动画效果。
- 动画片段创建: 当你为一个没有动画的游戏对象首次添加动画时,Unity会提示你创建一个
.anim
文件,这就是一个动画片段资源。你可以创建多个动画片段,例如Idle.anim
,Walk.anim
,Run.anim
,Jump.anim
,Attack.anim
等。 - 动画事件添加: 在这个窗口中,你还可以在动画片段的特定时间点添加动画事件标记(稍后详述)。
类比: 如果把整个动画系统比作一部电影,那么Animation
窗口就是剪辑师的工作台,用来制作独立的镜头片段(动画片段)。
1.2 Animator窗口:动画逻辑的指挥中心
Animator
窗口(通常通过 Window > Animation > Animator
打开,或者双击项目中的Animator Controller
资源)是你组织和控制动画逻辑的地方。它基于**动画状态机(Animation State Machine)**的概念工作。
- 状态机视图: 这是
Animator
窗口的核心。你可以在这里创建不同的动画状态(State),并将你的动画片段(.anim
文件)拖拽到这些状态上。 - 状态: 代表角色可能处于的某种动画表现,例如“站立状态”、“行走状态”、“跳跃状态”。每个状态通常关联一个动画片段。
- 过渡(Transition): 定义了状态之间如何切换的规则和条件。例如,从“站立”到“行走”需要满足什么条件(比如速度参数大于某个值)。
- 参数(Parameter): 这些是你在状态机中定义的变量(如浮点数、整数、布尔值、触发器),用于驱动过渡条件的判断。C#脚本主要通过修改这些参数来控制动画状态的切换。
- 层(Layer)与子状态机(Sub-State Machine): 对于更复杂的动画逻辑,可以使用层来叠加或覆盖动画(如上半身攻击,下半身移动),使用子状态机来组织相关的状态集合(如将所有战斗相关的状态放入一个子状态机)。
类比: Animator
窗口就像是电影的导演,他决定了哪个镜头片段(状态/动画片段)在什么时候播放,以及片段之间如何衔接(过渡),依据的是剧本/指令(参数)。
要使用Animator
窗口,你需要先创建一个Animator Controller资源(Create > Animator Controller
),然后将其赋给需要播放动画的游戏对象上的Animator
组件。Animator
组件是连接游戏对象、Animator Controller和脚本的桥梁。
二、核心引擎:动画状态机(State Machine)详解
动画状态机是Animator
组件的核心,理解其工作原理是掌握Unity动画控制的关键。它由三个主要部分组成:状态、过渡和参数。
2.1 状态(State):角色行为的快照
一个状态代表了角色在某个时间点的一种特定行为或姿势,并且通常会循环播放或播放一次关联的动画片段。
- 创建状态: 在Animator窗口空白处右键
Create State > Empty
来创建一个新状态。 - 关联动画片段: 选中状态,在Inspector面板的
Motion
字段中,拖入一个.anim
动画片段资源。 - 默认状态 (Default State): 状态机有且只有一个默认状态(橙色显示),这是状态机启动时进入的第一个状态。通常设置为角色的“Idle”(站立)状态。可以通过右键点击状态选择
Set as Layer Default State
来设置。 - Any State: 这是一个特殊的状态节点,表示可以从任何其他状态转换到目标状态,只要满足过渡条件。常用于全局性的动作,如受伤、死亡等,无论角色当前在做什么,只要满足条件(如受到攻击),就应该立即切换到“受击”或“死亡”状态。
- Exit State: 这是一个特殊的退出节点,用于从子状态机返回到上一层状态机。
2.2 过渡(Transition):状态切换的桥梁
过渡定义了从一个状态切换到另一个状态的规则和方式。
- 创建过渡: 在源状态上右键
Make Transition
,然后将箭头指向目标状态。 - 过渡属性: 选中过渡箭头,可以在Inspector面板中设置其属性:
Has Exit Time
: 如果勾选,表示当前状态的动画必须播放完(或达到指定的退出时间Exit Time
)才能开始过渡。如果不勾选,则只要满足Conditions
,过渡会立即发生(常用于响应迅速的操作,如从Idle到Walk)。Settings
:Exit Time
: (仅当Has Exit Time
勾选时)动画播放到哪个标准化时间点(0到1)可以退出。Fixed Duration
: 如果勾选,过渡时间是固定的秒数;如果不勾选,过渡时间是目标动画长度的一个百分比。Transition Duration
: 过渡的持续时间(秒或百分比),表示两个动画混合的平滑程度。值越小,切换越生硬;值越大,混合越平滑。Transition Offset
: 目标动画从哪个标准化时间点开始播放。Interruption Source
: 定义了当前过渡在何种情况下可以被其他过渡打断。
2.2.1 设置过渡条件
这是过渡的核心部分。Conditions
列表决定了何时触发这个过渡。
- 添加条件: 点击
Conditions
列表下方的+
号。 - 选择参数: 从下拉菜单中选择一个之前在Animator中定义的参数。
- 设置条件: 根据参数类型设置条件(例如,
Speed
> 0.1,IsJumping
is true,Attack
trigger)。 - 多条件: 可以添加多个条件,所有条件必须同时满足,过渡才会发生。
示例: 从 “Idle” 到 “Walk” 的过渡,可以不勾选 Has Exit Time
,并添加条件 Speed > 0.1
。这意味着只要速度参数大于0.1,角色就会立即开始向行走动画过渡。
2.2.2 过渡参数详解
理解Has Exit Time
和Transition Duration
对于创建流畅的动画至关重要。
- 何时使用
Has Exit Time
?:- 勾选:适用于需要完整播放的动画,或者需要等待动画自然结束才能转换的情况(如攻击动画的前摇和后摇)。
- 不勾选:适用于需要快速响应输入的动作(如移动开始/停止、跳跃)。
Transition Duration
的影响:- 较小值 (e.g., 0.1): 快速切换,可能显得突兀。
- 较大值 (e.g., 0.25): 平滑混合,但响应可能稍慢。需要根据具体动画和手感进行调整。
2.3 参数(Parameter):驱动状态变化的钥匙
参数是状态机外部(主要是C#脚本)与内部逻辑(过渡条件)沟通的桥梁。脚本通过修改参数的值,来告知状态机应该进行何种状态切换。
- 创建参数: 在Animator窗口左侧的
Parameters
标签页中,点击+
号选择要创建的参数类型。
2.3.1 参数类型(Float, Int, Bool, Trigger)
Unity Animator提供四种参数类型:
- Float (浮点数): 用于表示连续变化的值,如移动速度、方向、生命值百分比等。
- 条件:
Greater
(大于),Less
(小于)
- 条件:
- Int (整数): 用于表示离散的数值状态,如武器类型、连击次数等。
- 条件:
Greater
(大于),Less
(小于),Equals
(等于),Not Equal
(不等于)
- 条件:
- Bool (布尔值): 用于表示开关状态,如是否在空中 (
IsJumping
)、是否在防御 (IsBlocking
) 等。- 条件:
true
,false
- 条件:
- Trigger (触发器): 用于表示一次性的事件,如攻击、跳跃、受击等。触发后会自动消耗(变为false)。特别适合那些只需要瞬间信号来启动过渡的动作。
- 条件: 无需设置值,只要被触发(SetTrigger),条件即满足。
2.3.2 参数的作用与选择
选择合适的参数类型对于设计清晰的状态机逻辑非常重要。
- 速度控制移动: 使用
Float
类型的参数(如Speed
)。Idle (Speed < 0.1), Walk (Speed >= 0.1 and Speed < 2.0), Run (Speed >= 2.0)。 - 跳跃状态: 使用
Bool
类型的参数(如IsGrounded
)。从地面状态到跳跃状态的过渡条件是IsGrounded
isfalse
。从跳跃状态回到地面状态的过渡条件是IsGrounded
istrue
。或者使用Trigger
(如Jump
)来启动跳跃动画,配合脚本检测落地后设置回IsGrounded
为true
。 - 攻击动作: 使用
Trigger
类型的参数(如Attack
)。当玩家按下攻击键时,脚本设置Attack
触发器,启动攻击动画。动画播放完后(或通过Has Exit Time
控制),通常会自动返回Idle或其他状态。
三、代码驱动:C#脚本与Animator的联动
仅仅设置好状态机是不够的,我们需要通过C#脚本来动态地改变Animator中的参数值,从而根据游戏逻辑(如玩家输入、AI状态)来控制角色动画。
3.1 获取Animator组件
首先,在你的C#脚本(通常是附加在角色模型上的控制脚本)中,需要获取对Animator
组件的引用。
using UnityEngine;
public class PlayerAnimationController : MonoBehaviour
{
private Animator animator; // 声明一个私有变量来存储Animator组件的引用
void Awake() // 或者 Start()
{
// 在脚本初始化时获取Animator组件
animator = GetComponent<Animator>();
if (animator == null)
{
Debug.LogError("未能找到Animator组件!请确保游戏对象上已添加Animator组件。");
}
}
// ... 后续控制代码将写在这里 ...
}
最佳实践: 在Awake
或Start
方法中获取并缓存组件引用,避免在Update
中反复调用GetComponent
,以提高性能。
3.2 通过脚本设置参数值
获取到Animator
组件后,就可以使用其提供的方法来设置参数值了。这些方法通常在Update
方法中根据当前游戏状态或输入来调用。
3.2.1 控制布尔/触发器
void Update()
{
if (animator == null) return; // 如果没有获取到Animator,则不执行后续代码
// --- 控制跳跃 (示例:使用Bool) ---
bool isGrounded = CheckIfGrounded(); // 假设有一个检测是否在地面的方法
animator.SetBool("IsGrounded", isGrounded);
if (Input.GetButtonDown("Jump") && isGrounded)
{
// 如果按下跳跃键且在地面,则可能触发跳跃逻辑(物理上的跳跃)
// 动画状态的切换由IsGrounded参数的变化驱动
// 或者,如果使用Trigger:
// animator.SetTrigger("JumpTrigger");
}
// --- 控制攻击 (示例:使用Trigger) ---
if (Input.GetButtonDown("Fire1")) // 假设Fire1是攻击键
{
animator.SetTrigger("Attack");
// 注意:Trigger参数设置后会自动重置,适合一次性事件
}
}
bool CheckIfGrounded()
{
// 这里需要实现真实的地面检测逻辑,例如使用Physics.Raycast或Physics.CheckSphere
// 为简化,这里仅返回true
return true;
}
SetBool(string name, bool value)
: 设置指定名称的布尔参数的值。SetTrigger(string name)
: 触发指定名称的触发器参数。
3.2.2 控制浮点/整数
void Update()
{
if (animator == null) return;
// --- 控制移动速度 (示例:使用Float) ---
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
// 计算移动速度(这里简化为输入向量的模长)
Vector2 moveInput = new Vector2(moveHorizontal, moveVertical);
float currentSpeed = moveInput.magnitude;
// 将速度值传递给Animator的Speed参数
// 可以考虑增加平滑处理,让速度变化更自然
animator.SetFloat("Speed", currentSpeed, 0.1f, Time.deltaTime); // 使用带阻尼的SetFloat
// --- 控制武器类型 (示例:使用Int) ---
if (Input.GetKeyDown(KeyCode.Alpha1)) // 按下数字键1切换武器
{
animator.SetInteger("WeaponType", 1);
}
else if (Input.GetKeyDown(KeyCode.Alpha2)) // 按下数字键2切换武器
{
animator.SetInteger("WeaponType", 2);
}
}
SetFloat(string name, float value)
: 设置浮点参数的值。SetFloat(string name, float value, float dampTime, float deltaTime)
: 带阻尼地设置浮点参数值,可以在dampTime
秒内平滑过渡到value
,常用于速度等需要平滑变化的参数。SetInteger(string name, int value)
: 设置整数参数的值。
重要提示: 脚本中使用的参数名称(字符串)必须与Animator窗口中定义的参数名称完全一致(包括大小写)。
3.3 监听动画状态(进阶)
虽然主要通过参数控制状态切换,但有时可能需要知道当前动画状态机正处于哪个状态或正在播放哪个动画片段。
void Update()
{
if (animator == null) return;
// 获取当前基础层(Layer 0)的状态信息
AnimatorStateInfo currentStateInfo = animator.GetCurrentAnimatorStateInfo(0);
// 检查当前状态是否是某个特定状态(通过状态名称哈希值比较更高效)
if (currentStateInfo.IsName("YourStateName")) // 或者使用 IsTag("YourTag")
{
// 正在播放名为 "YourStateName" 的状态
}
// 获取当前状态的标准化时间 (0到1表示动画播放进度)
float normalizedTime = currentStateInfo.normalizedTime;
if (currentStateInfo.IsName("Attack") && normalizedTime >= 0.9f)
{
// 攻击动画快要播放完了
}
// 获取下一个将要过渡到的状态信息 (如果正在过渡中)
AnimatorStateInfo nextStateInfo = animator.GetNextAnimatorStateInfo(0);
if (animator.IsInTransition(0))
{
// 正在进行过渡
}
}
GetCurrentAnimatorStateInfo(int layerIndex)
: 获取指定层当前状态的信息。GetNextAnimatorStateInfo(int layerIndex)
: 获取指定层下一个状态的信息(如果正在过渡)。IsInTransition(int layerIndex)
: 判断指定层是否正在进行过渡。AnimatorStateInfo.IsName(string name)
: 判断状态是否是指定名称。AnimatorStateInfo.IsTag(string tag)
: 判断状态是否带有指定标签(可以在Animator窗口中为状态添加Tag)。AnimatorStateInfo.normalizedTime
: 获取动画播放的标准化时间(整数部分表示循环次数,小数部分表示当前循环的进度)。
注意: IsName
比较的是状态的名称。为了性能,Unity内部使用哈希值进行比较。可以直接使用 Animator.StringToHash("YourStateName")
预先计算哈希值进行比较。
四、精准卡点:动画事件(Animation Event)的应用
有时,我们希望在动画播放到特定帧时执行某些代码逻辑,例如:
- 走路动画中,脚接触地面时播放脚步声。
- 攻击动画中,武器挥到特定位置时才真正产生伤害判定。
- 动画播放结束时通知某个系统。
这时,**动画事件(Animation Event)**就派上用场了。它允许你在动画片段的时间轴上标记一个点,并在动画播放到该点时调用指定的游戏对象上的脚本函数。
4.1 什么是动画事件?
动画事件是附加在动画片段(.anim
文件)上的标记,它指定了在动画播放到该时间点时,需要调用哪个脚本中的哪个公共方法。
4.2 如何添加动画事件
- 选中游戏对象: 在Hierarchy中选中包含
Animator
组件和你想要添加事件的动画片段所对应的游戏对象。 - 打开Animation窗口: 确保Animation窗口 (
Ctrl+6
) 是打开的。 - 选择动画片段: 从Animation窗口左上角的下拉菜单中,选择你想要添加事件的那个动画片段(如
Attack.anim
)。 - 定位时间点: 在时间轴上拖动白色的时间线,或者直接在时间输入框中输入精确的时间,定位到你希望触发事件的那一帧。
- 添加事件标记: 点击时间轴上方的 “Add Event” 按钮(看起来像一个带加号的小书签图标)。这会在当前时间点添加一个蓝色的事件标记。
- 选择函数: 选中新添加的事件标记,在Inspector面板中会出现一个
Function
下拉菜单。这个菜单会列出附加到同一个游戏对象上的所有脚本中的公共方法 (public)。选择你想要调用的那个方法。 - 传递参数 (可选): 你还可以为调用的函数传递一个参数(Float, Int, String, Object, 或 AnimationEvent 类型)。在Inspector中选择参数类型并填入值。
4.3 编写响应事件的脚本函数
在需要响应动画事件的脚本(必须附加在同一个游戏对象上)中,定义一个公共 (public) 方法,其名称必须与你在Animation窗口中选择的函数名称一致。
using UnityEngine;
public class CharacterActions : MonoBehaviour
{
// 这个公共方法可以被动画事件调用
public void PlayFootstepSound()
{
// 在这里添加播放脚步声的代码
Debug.Log("播放脚步声!");
// 例如: AudioManager.Instance.PlaySFX("Footstep");
}
// 这个公共方法也可以被动画事件调用,并且可以接收参数
public void ApplyAttackDamage()
{
// 在这里添加攻击伤害判定的代码
Debug.Log("攻击命中,计算伤害!");
// 例如: PerformDamageCheck();
}
// 示例:接收一个字符串参数
public void PrintMessage(string message)
{
Debug.Log("动画事件传递的消息: " + message);
}
// 示例:接收一个 AnimationEvent 参数,可以获取更多事件信息
public void HandleGenericEvent(AnimationEvent eventInfo)
{
Debug.Log($"事件触发于动画: {eventInfo.animatorClipInfo.clip.name}, " +
$"时间点: {eventInfo.time}, " +
$"函数名: {eventInfo.functionName}, " +
$"字符串参数: {eventInfo.stringParameter}");
}
}
关键点:
- 响应函数必须是
public
。 - 脚本必须附加在拥有
Animator
组件和播放该动画的游戏对象上。 - 函数名称必须与Animation窗口中设置的完全匹配。
- 如果函数需要参数,确保Animation窗口中也设置了对应类型的参数。
4.4 应用场景举例
- 脚步声: 在行走或跑步动画中,当脚落地的那一帧添加事件,调用
PlayFootstepSound
。 - 攻击判定: 在攻击动画中,当武器挥动到最有效的位置时添加事件,调用
ApplyAttackDamage
来检测是否击中敌人并造成伤害。 - 特效播放: 在技能动画的特定帧,添加事件来触发粒子特效或音效播放。
- 动画结束回调: 在某些一次性动画(如死亡动画)的最后一帧添加事件,调用一个
OnDeathAnimationComplete
函数来处理后续逻辑(如显示游戏结束画面)。
五、实战演练:打造动态角色
现在,让我们将前面学到的知识整合起来,为一个简单的角色添加行走、跑步、跳跃和攻击动画,并通过脚本控制它们。
假设: 你已经有一个带有人形骨骼(Rigged Humanoid)的角色模型,并且已经准备好了以下动画片段(.anim
文件):Idle
, Walk
, Run
, JumpStart
, JumpLoop
, JumpLand
, Attack
。
5.1 准备工作:导入动画片段与设置Animator
- 创建Animator Controller: 在Project窗口中右键
Create > Animator Controller
,命名为PlayerAnimatorController
。 - 附加Animator组件: 选中你的角色游戏对象,在Inspector中点击
Add Component
,搜索并添加Animator
组件。 - 分配Controller: 将刚刚创建的
PlayerAnimatorController
拖拽到Animator
组件的Controller
字段中。 - 设置Avatar: 如果是人形模型,确保
Animator
组件的Avatar
字段已正确分配了模型的Avatar定义(通常在模型导入设置的Rig标签页生成)。
5.2 构建状态机:连接行走、跑步、跳跃、攻击
- 打开Animator窗口: 双击
PlayerAnimatorController
资源。 - 创建参数: 在
Parameters
标签页,添加以下参数:Float
:Speed
(用于控制移动)Bool
:IsGrounded
(用于判断是否在地面)Trigger
:Jump
(用于触发跳跃)Trigger
:Attack
(用于触发攻击)
- 添加状态:
- 将
Idle
,Walk
,Run
动画片段拖入Animator窗口,创建对应状态。设置Idle
为默认状态。 - 创建
JumpStartState
,JumpLoopState
,JumpLandState
。 - 创建
AttackState
。
- 将
- 设置移动过渡:
Idle
->Walk
: 创建过渡,取消Has Exit Time
,条件为Speed > 0.1
。Walk
->Idle
: 创建过渡,取消Has Exit Time
,条件为Speed < 0.1
。Walk
->Run
: 创建过渡,取消Has Exit Time
,条件为Speed > 1.5
(假设1.5是跑步阈值)。Run
->Walk
: 创建过渡,取消Has Exit Time
,条件为Speed < 1.5
。Run
->Idle
: (可选,直接从跑到停) 创建过渡,取消Has Exit Time
,条件为Speed < 0.1
。- 调整
Transition Duration
以获得平滑的移动混合效果(例如 0.1 到 0.25)。
- 设置跳跃过渡:
Any State
->JumpStartState
: 创建过渡,条件为Jump
trigger。取消Has Exit Time
。设置Interruption Source
为NextState
可能更好,允许立即跳跃。JumpStartState
->JumpLoopState
: 创建过渡,勾选Has Exit Time
(让起跳动画播完),Exit Time
设置为 1。条件为IsGrounded
isfalse
。JumpLoopState
->JumpLandState
: 创建过渡,取消Has Exit Time
,条件为IsGrounded
istrue
。JumpLandState
->Idle
(或根据Speed
到Walk
/Run
): 创建过渡,勾选Has Exit Time
(让落地动画播完),Exit Time
设为 1。
- 设置攻击过渡:
Any State
->AttackState
: 创建过渡,条件为Attack
trigger。取消Has Exit Time
。调整Interruption Source
,决定攻击是否能打断其他动作。AttackState
->Idle
(或其他基础状态): 创建过渡,勾选Has Exit Time
(让攻击动画播完),Exit Time
设为 1。
状态机构建图示 (Mermaid):
graph LR
subgraph GroundMovement [地面移动]
Idle -- Speed > 0.1 --> Walk
Walk -- Speed < 0.1 --> Idle
Walk -- Speed > 1.5 --> Run
Run -- Speed < 1.5 --> Walk
end
subgraph JumpSequence [跳跃序列]
JumpStart -- IsGrounded == false (After Exit) --> JumpLoop
JumpLoop -- IsGrounded == true --> JumpLand
JumpLand -- Animation End --> GroundMovement
end
subgraph AttackSequence [攻击序列]
AttackState -- Animation End --> GroundMovement
end
AnyState -- Jump Trigger --> JumpStart
AnyState -- Attack Trigger --> AttackState
%% 设置默认状态
classDef default fill:#f9f,stroke:#333,stroke-width:2px;
class Idle default;
(注意: Mermaid 图仅为示意,实际Animator窗口连接会更复杂,特别是Any State和返回逻辑)
5.3 编写控制脚本:响应玩家输入
创建一个新的C#脚本,例如 PlayerController.cs
,并将其附加到你的角色游戏对象上。
using UnityEngine;
[RequireComponent(typeof(Animator))] // 确保总是有Animator组件
[RequireComponent(typeof(CharacterController))] // 假设使用CharacterController移动
public class PlayerController : MonoBehaviour
{
private Animator animator;
private CharacterController controller;
public float walkSpeed = 3.0f;
public float runSpeed = 6.0f;
public float jumpForce = 8.0f;
public float gravity = 20.0f;
private Vector3 moveDirection = Vector3.zero;
private bool isJumping = false; // 辅助判断跳跃状态
void Awake()
{
animator = GetComponent<Animator>();
controller = GetComponent<CharacterController>();
}
void Update()
{
// --- 地面检测 ---
bool isGrounded = controller.isGrounded;
animator.SetBool("IsGrounded", isGrounded);
// --- 移动输入 ---
float horizontalInput = Input.GetAxis("Horizontal");
float verticalInput = Input.GetAxis("Vertical");
Vector3 inputDirection = new Vector3(horizontalInput, 0, verticalInput);
inputDirection = transform.TransformDirection(inputDirection); // 转换为世界坐标
// --- 计算速度与设置动画参数 ---
float currentSpeed = Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed; // 按Shift跑步
if (inputDirection.magnitude < 0.1f) // 没有输入则速度为0
{
currentSpeed = 0f;
}
moveDirection.x = inputDirection.x * currentSpeed;
moveDirection.z = inputDirection.z * currentSpeed;
// 将速度传递给Animator (只传递期望的水平速度大小,不含垂直速度)
float animationSpeed = new Vector2(moveDirection.x, moveDirection.z).magnitude;
animator.SetFloat("Speed", animationSpeed, 0.1f, Time.deltaTime);
// --- 跳跃输入 ---
if (isGrounded)
{
moveDirection.y = -gravity * Time.deltaTime; // 在地面时施加少量重力防止浮空
if (isJumping) // 如果是从跳跃状态刚落地
{
isJumping = false;
// 可以在这里触发落地动画相关的逻辑(如果需要的话)
}
if (Input.GetButtonDown("Jump"))
{
animator.SetTrigger("Jump"); // 触发跳跃动画
moveDirection.y = jumpForce; // 施加向上的力
isJumping = true;
}
}
else // 在空中
{
// 持续施加重力
moveDirection.y -= gravity * Time.deltaTime;
}
// --- 攻击输入 ---
if (Input.GetButtonDown("Fire1") && isGrounded) // 假设地面才能攻击
{
animator.SetTrigger("Attack");
// 可以在这里添加攻击不能移动的逻辑
}
// --- 应用移动 ---
// 需要检查是否正在播放某些不能移动的动画(例如攻击)
// bool canMove = !animator.GetCurrentAnimatorStateInfo(0).IsTag("Attack"); // 假设攻击状态有"Attack"标签
// if (canMove)
// {
controller.Move(moveDirection * Time.deltaTime);
// }
// else
// {
// 如果不能移动,只应用重力
// controller.Move(new Vector3(0, moveDirection.y, 0) * Time.deltaTime);
// }
controller.Move(moveDirection * Time.deltaTime); // 简化处理,允许攻击时移动
// --- 角色朝向 (可选) ---
if (inputDirection != Vector3.zero)
{
// 可以让角色朝向移动方向
// transform.rotation = Quaternion.LookRotation(new Vector3(moveDirection.x, 0, moveDirection.z));
}
}
}
5.4 添加动画事件:实现攻击判定或音效
- 打开
Animation
窗口,选择Attack
动画片段。 - 找到攻击动画中武器挥出的关键帧。
- 在该帧添加一个动画事件,
Function
选择你在PlayerController
或其他附加脚本中定义的public void ApplyAttackDamage()
方法(你需要先创建这个方法)。 - 同样,可以在
Walk
或Run
动画的脚落地帧添加事件,调用PlayFootstepSound()
方法(也需要先创建)。
现在,运行游戏,你的角色应该能够根据你的键盘(WASD移动,Shift跑步,空格跳跃)和鼠标(鼠标左键攻击)输入,流畅地切换相应的动画了!
六、常见问题与排错
- 动画不播放/切换异常:
- 检查
Animator
组件是否已添加到游戏对象,并且Controller
已正确分配。 - 检查
Animator
窗口中的参数名称与脚本中使用的字符串是否完全一致(大小写敏感)。 - 检查过渡条件设置是否正确,逻辑是否符合预期。
- 检查
Has Exit Time
的勾选状态是否符合你的需求。 - 使用
Debug.Log
在脚本中打印参数值,确认它们是否按预期变化。 - 检查动画片段本身是否正常(可以在
Animation
窗口预览)。
- 检查
- 动画事件不触发:
- 确认响应事件的脚本已附加到同一个游戏对象上。
- 确认响应事件的方法是
public
的。 - 确认
Animation
窗口中事件设置的函数名称与脚本中的方法名称完全一致。 - 如果方法有参数,确认参数类型和值在
Animation
窗口中已正确设置。
- 动画混合效果不佳:
- 调整过渡的
Transition Duration
,找到合适的混合时间。 - 检查动画片段之间的姿势差异,差异过大可能需要更长的过渡时间或更复杂的过渡设置。
- 调整过渡的
- 人形角色动画问题 (Humanoid Rig):
- 确保模型导入设置中
Rig
标签页的Animation Type
为Humanoid
,并正确配置或创建了Avatar
。 - 确保所有动画片段也使用了兼容的Humanoid Rig。
- 检查
Root Motion
设置(在Animator
组件和动画片段导入设置中),它会影响角色移动是由动画驱动还是脚本驱动。
- 确保模型导入设置中
七、总结
恭喜你完成了第29天的学习!今天我们深入探索了Unity强大的动画系统核心——Animator
组件。通过本节的学习,你应该掌握了:
- Animation窗口 用于创建和编辑单个动画片段(
.anim
文件)。 - Animator窗口 用于构建动画状态机,管理动画之间的逻辑关系。
- 动画状态机 (State Machine) 的三大要素:
- 状态 (State):代表角色的不同动画表现,关联动画片段。
- 过渡 (Transition):定义状态切换的规则,由条件驱动。
- 参数 (Parameter):(Float, Int, Bool, Trigger) 作为脚本与状态机沟通的桥梁,用于触发过渡。
- C#脚本控制Animator: 通过
GetComponent<Animator>()
获取引用,使用SetFloat
,SetInteger
,SetBool
,SetTrigger
等方法修改参数值来驱动动画状态切换。 - 动画事件 (Animation Event):允许在动画片段的特定帧调用脚本中的
public
方法,实现如播放音效、伤害判定等精准卡点操作。 - 实践应用: 成功地为一个角色构建了包含行走、跑步、跳跃、攻击的状态机,并通过脚本响应玩家输入来控制动画播放。
掌握Animator
是让你的游戏角色生动起来的关键一步。虽然初看可能有些复杂,但理解了状态机的基本原理和脚本交互方式后,你就能创造出丰富多样的角色行为。在后续的游戏开发中,你会不断地与Animator
打交道,不断优化你的动画逻辑。
继续努力,下一天我们将学习Unity中同样重要的UI开发与交互!