人物动画状态机
- 介绍
- FSM
- 角色模型的设置
- 角色动作的设置
- 角色动画控制器的设置
- 书写角色动画的具体状态,实现缓动起步的FSM
- 总结
介绍
摇杆我就不介绍了,之前我在这里面讲过怎么用摇杆,摇杆连接。
这里我先说下什么是FSM人物动画状态机,说白了就是一个集中管理控制角色动画状态的一个管理器。可以切换操作对象的不同动作状态。下面我放一个实例Gif这个是人物FSM动画控制+人物动作匹配缓动起步的实例。
FSM
RoleState是角色的所有状态列举,后面切换状态是通过RoleFSMMgr切换玩家的RoleState来改变玩家的状态。
RoleState.cs
/// <summary>
/// 角色状态
/// </summary>
public enum RoleState
{
None,
Idle,
Run,
RunToIdle,
}
这里还有一个角色状态的抽象基类RoleStateAbstract,这里主要存储了动画状态片段以及信息,还有就是这里有三个方法分别是动作状态的进入OnEnter、动作状态的持续中OnStay、动作状态的离开OnExit,这里主要是为了具体的动作状态提供统一管理的方法,也方便了上层的RoleFSMMgr的调用管理整个状态的执行和切换状态。
RoleStateAbstract.cs
using UnityEngine;
/// <summary>
/// 角色状态的抽象基类
/// </summary>
public abstract class RoleStateAbstract
{
/// <summary>
/// 当前角色有限状态机管理器
/// </summary>
public RoleFSMMgr CurrRoleFSMMgr { get; private set; }
/// <summary>
/// 当前动画状态信息
/// </summary>
public AnimatorStateInfo CurrRoleAnimatorStateInfo { get; set; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="roleFSMMgr"></param>
public RoleStateAbstract(RoleFSMMgr roleFSMMgr)
{
CurrRoleFSMMgr = roleFSMMgr;
}
/// <summary>
/// 进入状态
/// </summary>
public virtual void OnEnter() { }
/// <summary>
/// 执行状态
/// </summary>
public virtual void OnStay() { }
/// <summary>
/// 离开状态
/// </summary>
public virtual void OnExit() { }
}
在RoleFSMMgr管理器中的Dictionary<RoleState, RoleStateAbstract>中存储了当前玩家的状态枚举跟具体的状态信息RoleStateAbstrac动作状态基类一一对应。并且还存储了玩家的角色控制器RoleCtrl、当前状态信息、当前状态的枚举等信息。
RoleFSMMgr除了切换玩家状态以外,还有一个作用就是要驱动玩家的当前状态,这里会提供了一个OnUpdate的方法来驱动玩家的当前状态。
因为当前管理器是角色对象需要实例化的,所以这里提供了一个构造函数,我们在构造函数中实例化我们所有的角色状态以及继承自RoleStateAbstract的所有具体角色状态。
RoleFSMMgr.cs
using System.Collections.Generic;
/// <summary>
/// 角色有限状态机管理器
/// </summary>
public class RoleFSMMgr
{
/// <summary>
/// 当前角色控制器
/// </summary>
public RoleCtrl CurrRoleCtrl { get; private set; }
/// <summary>
/// 当前角色状态枚举
/// </summary>
public RoleState CurrRoleStateEnum { get; private set; }
/// <summary>
/// 当前角色状态
/// </summary>
private RoleStateAbstract m_CurrRoleState = null;
/// <summary>
/// 状态基类字典
/// </summary>
private Dictionary<RoleState, RoleStateAbstract> m_RoleStateDic;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="currRoleCtrl"></param>
public RoleFSMMgr(RoleCtrl currRoleCtrl)
{
CurrRoleCtrl = currRoleCtrl;
m_RoleStateDic = new Dictionary<RoleState, RoleStateAbstract>();
m_RoleStateDic[RoleState.Idle] = new RoleStateIdle(this);
m_RoleStateDic[RoleState.Run] = new RoleStateRun(this);
m_RoleStateDic[RoleState.RunToIdle] = new RoleStateRunToIdle(this);
}
#region OnUpdate 每帧执行
/// <summary>
/// 每帧执行
/// </summary>
public void OnUpdate()
{
if (m_CurrRoleState != null)
{
m_CurrRoleState.OnStay();
}
}
#endregion
/// <summary>
/// 切换状态
/// </summary>
/// <param name="newState">新状态</param>
public void ChangeState(RoleState newState)
{
if (CurrRoleStateEnum == newState) return;
//调用以前状态的离开方法
if (m_CurrRoleState != null)
m_CurrRoleState.OnExit();
//更改当前状态枚举
CurrRoleStateEnum = newState;
//更改当前状态
m_CurrRoleState = m_RoleStateDic[newState];
///调用新状态的进入方法
m_CurrRoleState.OnEnter();
}
}
RoleCtrl人物控制器我就不过多解释了,这里其实只是一个对象的载体脚本,只是为了获取到角色对象上面的Animator动画、CharacterController人形碰撞器、JoyStickBar摇杆、RoleFSMMgr实例化的角色fsm管理器。
这里的Update的方法中有一段代码如下,这个就是检测是否在地面如果不在地面上那么就向下位移瞬间到达平面的意思,如果你是想要做跳跃的话可以修改这里,自己写向下的作用力。
///非落地情况 使其快速落地
if (!m_char.isGrounded)
{
m_char.Move(new Vector3 (0,-100,0));
}
RoleCtrl是实体对象,所以需要在Start的时候创建我们得fsm状态管理器,然后设置自身的初始状态。
整体代码
RoleCtrl.cs
using UnityEngine;
public class RoleCtrl : MonoBehaviour {
/// <summary>
/// 摇杆
/// </summary>
public JoyStickBar m_Joy;
/// <summary>
/// 当前人物触发器
/// </summary>
public CharacterController m_char;
/// <summary>
/// 动画状态机
/// </summary>
public RoleFSMMgr m_fsm;
/// <summary>
/// 动画控制器
/// </summary>
public Animator m_anim;
private void Start()
{
m_Joy.m_ctrl = this;
m_char = GetComponent<CharacterController>();
m_anim = GetComponent<Animator>();
m_fsm = new RoleFSMMgr(this);
m_fsm.ChangeState(RoleState.Idle);
}
// Update is called once per frame
void Update () {
if (!m_char) return;
if (!m_Joy) return;
if (m_fsm == null) return;
///非落地情况 使其快速落地
if (!m_char.isGrounded)
{
m_char.Move(new Vector3 (0,-100,0));
}
m_fsm.OnUpdate();
}
}
虽然我之前讲过摇杆,但是这里我把摇杆的脚本也贴出来,这里还是稍微有点变化的
摇杆继承了UnityEngine.EventSystems中的三个事件分别是IBeginDragHandler、IDragHandler、IEndDragHandler
JoyStickBar.cs
using UnityEngine;
using UnityEngine.EventSystems;
public class JoyStickBar : MonoBehaviour,IBeginDragHandler,IDragHandler,IEndDragHandler {
/// <summary>
/// 角色控制器
/// </summary>
public RoleCtrl m_ctrl;
/// <summary>
/// 最大半径
/// </summary>
public float maxRadius;
/// <summary>
/// 计算中的半径
/// </summary>
public float radius;
/// <summary>
/// 原始位置
/// </summary>
private Vector2 originalPos;
/// <summary>
/// 遥杆中心位置
/// </summary>
public RectTransform joystickradius;
/// <summary>
/// 箭头指针方向
/// </summary>
public Transform joystickpointer;
#region 方向控制访问器
/// <summary>
/// 水平方向
/// </summary>
private float horizontal = 0;
/// <summary>
/// 垂直方向
/// </summary>
private float vertical = 0;
/// <summary>
/// 水平方向属性访问器
/// </summary>
public float Horizontal
{
get { return horizontal; }
}
/// <summary>
/// 垂直方向属性访问器
/// </summary>
public float Vertical
{
get { return vertical; }
}
#endregion
private void Start()
{
if (!joystickradius) return;
originalPos = transform.position;
maxRadius = - joystickradius.anchoredPosition.x;
Debug.LogError(maxRadius);
ShowPointer(false);
}
#region 方向受力
/// <summary>
/// 各个方向上的受力
/// </summary>
private void DirPotency()
{
//horizontal = transform.localPosition.x;
//vertical = transform.localPosition.y;
horizontal = GetComponent<RectTransform>().anchoredPosition.x;
vertical = GetComponent<RectTransform>().anchoredPosition.y;
}
#endregion
#region 继承接口事件逻辑处理
/// <summary>
/// 开始拖拽
/// </summary>
/// <param name="eventData"></param>
public void OnBeginDrag(PointerEventData eventData)
{
ShowPointer(true);
}
/// <summary>
/// 拖拽中
/// </summary>
/// <param name="eventData"></param>
public void OnDrag(PointerEventData eventData)
{
//偏移量
Vector2 dir = eventData.position - originalPos;
//Vector2 dir = new Vector2 (Input.mousePosition.x, Input.mousePosition.y) - originalPos;
//获取向量长度
float distance = Vector3.Magnitude(dir);
//获取当前
radius = Mathf.Clamp(distance,0,maxRadius);
//状态切换
m_ctrl.m_fsm.ChangeState(RoleState.Run);
//位置赋值
transform.position = dir.normalized * radius + originalPos;
//方向受力度量
DirPotency();
//角度转换
CalculateAngle(dir.normalized);
}
/// <summary>
/// 结束拖拽
/// </summary>
/// <param name="eventData"></param>
public void OnEndDrag(PointerEventData eventData)
{
transform.position = originalPos;
//当前半径
radius = 0;
//方向受力度量
DirPotency();
ShowPointer(false);
//m_ctrl.m_fsm.ChangeState(RoleState.Idle);
m_ctrl.m_fsm.ChangeState(RoleState.RunToIdle);
}
#endregion
#region 指针逻辑
/// <summary>
/// 角度转换
/// </summary>
public void CalculateAngle(Vector2 dir)
{
if (!joystickpointer) return;
float angle = Vector2.Angle(Vector2.up,dir);
joystickpointer.rotation = Quaternion.Euler(new Vector3(0, 0, -(dir.x>0?angle:-angle)));
}
/// <summary>
/// 显示隐藏指针
/// </summary>
/// <param name="isshow"></param>
public void ShowPointer(bool isshow)
{
joystickpointer.gameObject.SetActive(isshow);
}
#endregion
}
角色模型的设置
角色的模型这里使用的是通用骨骼Generic,当然这里使用的是人形或者其他形也没有问题,只要模型跟动作是同意的骨骼就没有问题。
根骨骼节点记得也要跟动作选择一致,Root node这里选择的是骨骼的根节点Bip001这个根节点,动画对应的也需要选择这个。
下面是动画的设置
角色动作的设置
动作的设置这里详细的说一下,因为这里有些动画是包含位移的,有些动作又是不包含位移的,这里需要看你具体要做的功能是什么,比如你要做帧同步的动画状态,那么你动画肯定是不能自己带位移的,也不能用动画的位移去应用到实际运动,因为这个动作底层使用的还是float是不一致的物理,除非这里只用做显示层。至于只要不是需要同步的位移的那么你动作带有位移或者使用这个也没有关系。
待机Idle动画
Anim.Compression这个时动画的压缩我通常直接设置Off,不启用动画的压缩
这里是动画的名字和帧数区间,如果是做了一个完整的动画需要截取动画的的,可以自己根据动作或者策划提供的配置文件进行截取动画这里我就不多说了,名字最好是要改一下并且跟RoleState中你创建的角色状态的枚举是一致的,这样方便在动画后面持续过程中方便操作,获取动画的信息等。
再看一下下面这几项Loop Time是是否需要循环播放动画,这里我们要做的是Idle和Run所以肯定是都要选择Loop Time。
Root Transform Rotation中
Bake Into Pose不够选代表的是使用动作的旋转来映射到真实人物上,勾选代表产生的旋转不会映射到实际人物上。
Base Upon(at Start)代表的是旋转映射到哪根骨骼上。
Offset代表的是偏移量
Root Transfrom Position(Y)
Bake Into Pose不够选代表的是使用动作的Y轴位移来映射到真实人物Y轴上,勾选代表产生位移不会映射到实际人物Y轴上。
Base Upon代表的是旋转映射到哪根骨骼上。
Offset代表的是偏移量
Root Transform Position(XZ)
Bake Into Pose不够选代表的是使用动作的XZ轴位移来映射到真实人物上,勾选代表产生位移不会映射到实际人物XZ轴上。
Base Upon代表的是旋转映射到哪根骨骼上。
Offset代表的是偏移量
Curvse动画曲线这里简单说一下,可以获取动画的曲线值来方便调式动作。
Events动画事件一般人物的脚步声音,动作特效事件等需要插事件。
Mask骨骼遮罩这个就不说了,这个是做分层动画
跑步Run动画
这个动画我就不单独设置了,跟Idle的原理是一样的,设置也是完全相同的。记住设置玩动画之后要点击Apply。
角色动画控制器的设置
创建两个参数Idle是一个bool类型的
Run是一个float类型的参数为后面使用
单层动画控制器,这里单层动画即可,将之前设置好的Idle拖上来,并且从Any State分别连线到Idle。
连线设置如下
Transition Duration这个融合度根据自己的动作合适的程度设置(融合度)
Has Exit Time取消勾选(无退出动作时间)
Can Transition To Self取消勾选(不能自己切换自己)
Conditions切换条件选择Idle,true
创建一个动画融合树,并命名为Run
动画融合树设置如下,将Idle和Run都拖拽上来,Parameter设置为刚才创建的参数Run,此时拖动BlendTree的值动画可以看到动画的变化
动画变化如下
调节Run的参数0-1,上面会得到一个从Idle到Run的不同程度的融合动画,这样后面就可以做融合缓动的起步的动画了。
拖动AnyState连线到Run并且设置如下
书写角色动画的具体状态,实现缓动起步的FSM
RoleStateIdle状态这里是常规动画的脚本书写
/// <summary>
/// 待机状态
/// </summary>
public class RoleStateIdle : RoleStateAbstract
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="roleFSMMgr">有限状态机管理器</param>
public RoleStateIdle(RoleFSMMgr roleFSMMgr) : base(roleFSMMgr)
{
}
/// <summary>
/// 实现基类 进入状态
/// </summary>
public override void OnEnter()
{
base.OnEnter();
CurrRoleFSMMgr.CurrRoleCtrl.m_anim.SetBool(RoleState.Idle.ToString(), true);
}
/// <summary>
/// 实现基类 执行状态
/// </summary>
public override void OnStay()
{
base.OnStay();
//CurrRoleAnimatorStateInfo = CurrRoleFSMMgr.CurrRoleCtrl.m_anim.GetCurrentAnimatorStateInfo(0);
}
/// <summary>
/// 实现基类 离开状态
/// </summary>
public override void OnExit()
{
base.OnExit();
CurrRoleFSMMgr.CurrRoleCtrl.m_anim.SetBool(RoleState.Idle.ToString(), false);
}
}
RoleStateRun状态这个就变成了缓动起步
下面我提供了两种缓动的方案,一种是根据摇杆拖拽的程度来控制跑动的幅度,另一种是设置了一个时间来做增速,相当于进行缓冲起步。
这里看的仔细的会发现我OnExit的方法并没有写逻辑,因为我这里如果停下的话还需要另一个状态来操作逻辑,从当前跑动的幅度在回到Idle的这么一个状态。当然这里还考虑了一点就是OnEnter的时候我提取了一个time来接收是否有初始速度,如果有初始速度那么按照初始速度来进行计算。
using UnityEngine;
/// <summary>
/// 跑状态
/// </summary>
public class RoleStateRun : RoleStateAbstract
{
/// <summary>
/// 旋转速度
/// </summary>
public float m_rotateSpeed = 3f;
/// <summary>
/// 移动速度
/// </summary>
public float m_moveSpeed = 3f;
/// <summary>
/// 移动速度
/// </summary>
private float m_MoveRatio = 0f;
/// <summary>
/// 转身的目标方向
/// </summary>
private Quaternion m_TargetQuaternion;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="roleFSMMgr">有限状态机管理器</param>
public RoleStateRun(RoleFSMMgr roleFSMMgr) : base(roleFSMMgr)
{
}
/// <summary>
/// 实现基类 进入状态
/// </summary>
public override void OnEnter()
{
base.OnEnter();
time = CurrRoleFSMMgr.CurrRoleCtrl.m_anim.GetFloat(RoleState.Run.ToString());
}
/// <summary>
/// 缓冲计时
/// </summary>
float time = 0;
/// <summary>
/// 缓冲速度
/// </summary>
float speed = 0;
/// <summary>
/// 实现基类 执行状态
/// </summary>
public override void OnStay()
{
base.OnStay();
#region 移动方案一 根据方向按键拖动的距离来增加移动速度
获取方向键拖动比例
//float radius = CurrRoleFSMMgr.CurrRoleCtrl.m_Joy.radius;
//float maxradius = CurrRoleFSMMgr.CurrRoleCtrl.m_Joy.maxRadius;
//m_MoveRatio = radius / maxradius;
//CurrRoleFSMMgr.CurrRoleCtrl.m_anim.SetFloat(RoleState.Run.ToString(), m_MoveRatio);
//float hor = CurrRoleFSMMgr.CurrRoleCtrl.m_Joy.Horizontal;
//float ver = CurrRoleFSMMgr.CurrRoleCtrl.m_Joy.Vertical;
//Vector3 dir = new Vector3(hor, 0, ver);
//if (dir != Vector3.zero)
//{
// CurrRoleFSMMgr.CurrRoleCtrl.transform.rotation = Quaternion.Lerp(CurrRoleFSMMgr.CurrRoleCtrl.transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime * m_rotateSpeed);
// CurrRoleFSMMgr.CurrRoleCtrl.m_char.Move(CurrRoleFSMMgr.CurrRoleCtrl.transform.forward * Time.deltaTime * m_moveSpeed * m_MoveRatio);
//}
#endregion
#region 方案二 拖动按钮有加速度
if (time <= 1)
{
time += Time.deltaTime;
speed = m_moveSpeed * time;
CurrRoleFSMMgr.CurrRoleCtrl.m_anim.SetFloat(RoleState.Run.ToString(), time);
}
float hor = CurrRoleFSMMgr.CurrRoleCtrl.m_Joy.Horizontal;
float ver = CurrRoleFSMMgr.CurrRoleCtrl.m_Joy.Vertical;
Vector3 dir = new Vector3(hor, 0, ver);
if (dir != Vector3.zero)
{
CurrRoleFSMMgr.CurrRoleCtrl.transform.rotation = Quaternion.Lerp(CurrRoleFSMMgr.CurrRoleCtrl.transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime * m_rotateSpeed);
CurrRoleFSMMgr.CurrRoleCtrl.m_char.Move(CurrRoleFSMMgr.CurrRoleCtrl.transform.forward * Time.deltaTime * speed);
}
#endregion
//CurrRoleAnimatorStateInfo = CurrRoleFSMMgr.CurrRoleCtrl.m_anim.GetCurrentAnimatorStateInfo(0);
}
/// <summary>
/// 实现基类 离开状态
/// </summary>
public override void OnExit()
{
base.OnExit();
}
}
RoleStateRunToIdle状态是从当前跑动的幅度切换回Idle的状态
using UnityEngine;
public class RoleStateRunToIdle : RoleStateAbstract
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="roleFSMMgr">有限状态机管理器</param>
public RoleStateRunToIdle(RoleFSMMgr roleFSMMgr) : base(roleFSMMgr)
{
}
/// <summary>
/// 实现基类 进入状态
/// </summary>
public override void OnEnter()
{
base.OnEnter();
time = CurrRoleFSMMgr.CurrRoleCtrl.m_anim.GetFloat(RoleState.Run.ToString());
CurrRoleFSMMgr.CurrRoleCtrl.m_anim.SetFloat(RoleState.Run.ToString(), time);
}
float time;
float speed = 0;
/// <summary>
/// 实现基类 执行状态
/// </summary>
public override void OnStay()
{
base.OnStay();
if (time >= 0)
{
time -= Time.deltaTime;
speed = 3 * time;
CurrRoleFSMMgr.CurrRoleCtrl.m_anim.SetFloat(RoleState.Run.ToString(), time);
}
CurrRoleFSMMgr.CurrRoleCtrl.m_char.Move(CurrRoleFSMMgr.CurrRoleCtrl.transform.forward * Time.deltaTime * speed);
}
/// <summary>
/// 实现基类 离开状态
/// </summary>
public override void OnExit()
{
base.OnExit();
//CurrRoleFSMMgr.CurrRoleCtrl.m_anim.SetFloat(RoleState.Run.ToString(), 0f);
}
}
这样就实现了这一套完整的缓动的FSM动画状态机。
总结
这个FSM其实还是很常用的,包括这里我其实也没有讲的特比深入,只是一个简单的FSM入门。这个还可以考虑动作的播放到什么程度和暂定动画,加速动画等,甚至后面还有HFSM分层管理器,有兴趣的话可以多了解了解,小主后面有时间的话可以在做一个HFSM,上面文章如果有看不懂的可以找到我的资源FSMRoleCtrl.unitypackage。(设置了10积分下载,希望各位可以理解)