前言:在写角色控制器的时候,我在写角色的逻辑控制的时候,比如是否按下wasd或者攻击键,都是将其放在一个庞大的update里面去检测,这样会写很多的if-else,并且,我在攻击时希望玩家停止移动,就要去增加一个bool值,而且这种大量的逻辑判断都是放在一个update,状态一切换,会改变很多数据,看着非常丑。参考了一下有限状态机的设计模式以及动画状态机的实现方式,设计一套结构清晰的角色控制器。
效果:这效果图片貌似看不出来啥,就文字描述下吧,待机和移动之间是秒切换,按住移动键再按攻击J,会停止移动,等到攻击结束若还按着移动键会切换为移动动画并进行逻辑移动,攻击结束后若没按任何键会回到待机状态,攻击的后摇时间可以通过代码添加附加系数控制。
简单介绍一下有限状态机的设计模式:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。比如按下攻击键,我就从移动或者待机->进入到攻击模式(不会进行坐标改变)
->攻击结束回调进入下一个阶段。这样很好的把一个状态抽象成一个节点,状态与状态间的耦合性大幅下降。
1.代码:
IState.CS脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//每个状态节点的内部设计
public interface IState
{
public void OnEnter();
public void OnUpdate();
public void OnExit();
}
AllStates.CS脚本用来定义每个状态应该实现的逻辑控制。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//AllStates脚本
/// <summary>
/// 状态枚举类型
/// </summary>
public enum State_Enum
{
idle,
move,
attack
}
/// <summary>
/// Idle状态
/// </summary>
public class Idle : IState
{
private Role baseFSM;
//构造函数获取刚刚创建脚本的属性方法
public Idle(Role manager)
{
this.baseFSM = manager;
}
public void OnEnter()
{
baseFSM.animator.SetBool("Idle", true);
}
public void OnExit()
{
baseFSM.animator.SetBool("Idle", false);
}
public void OnUpdate()
{
if(Input.GetAxisRaw("Horizontal")!=0||Input.GetAxisRaw("Vertical")!=0)
{
baseFSM.TransitionState(State_Enum.move);
}
if (Input.GetKeyDown(KeyCode.J))
{
baseFSM.TransitionState(State_Enum.attack);
}
}
}
/// <summary>
/// Move状态
/// </summary>
public class Move : IState
{
private Role baseFSM;
//构造函数获取刚刚创建脚本的属性方法
public Move(Role manager)
{
this.baseFSM = manager;
}
public void OnEnter()
{
baseFSM.animator.SetBool("Move", true);
}
public void OnExit()
{
baseFSM.animator.SetBool("Move", false);
}
public void OnUpdate()
{
baseFSM.MovePos();//逻辑移动
if (Input.GetAxisRaw("Horizontal") == 0 &&Input.GetAxisRaw("Vertical") == 0)
{
baseFSM.TransitionState(State_Enum.idle);
}
if (Input.GetKeyDown(KeyCode.J))
{
baseFSM.TransitionState(State_Enum.attack);
}
}
}
/// <summary>
/// Attack状态
/// </summary>
public class Attack : IState
{
private Role baseFSM;
AnimationEvent attackEve = new AnimationEvent();
public Attack(Role manager)
{
this.baseFSM = manager;
}
public void OnEnter()
{
attackEve.functionName = "EndAttack";
attackEve.time = baseFSM.allClips["atk"].length;//可以修改后摇时长
baseFSM.allClips["atk"].AddEvent(attackEve);
baseFSM.animator.SetTrigger("Attack");
}
public void OnExit()
{
baseFSM.allClips["atk"].events = default;
}
public void OnUpdate()
{
//Attack状态动态的脚本写在这里
}
}
上面重点讲一下Attack这个攻击类,与Move类和Idle不同,它多加了一个攻击结束后的回调方法,因为本身的设定,攻击动画的切换是通过触发来切换的,攻击动画结束后自动回到待机或者移动,所以不放在OnUpdate里面进行检测,而是给动画添加帧事件自动触发。而帧方法EndAttack是必须放到接下来的Role脚本里面的
Base.CS脚本用来定义玩家自己的基本属性,这个可以自己设定。这边单独抽出来是习惯把数据库的东西单独抽出来方面后续的封装,也可以直接放到地下的Role角色控制脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Base : MonoBehaviour
{
protected int attack;//攻击力
protected int blood;//血量
protected int mana;//法力
protected int moveXSpeed;//X轴移动速度
protected int moveYSpeed;//Y轴移动速度
protected Vector2 moveDir;
protected Vector2 curPos;
protected int lookDir;//设置朝向
protected void SetAttribute()
{
attack = 10;
moveXSpeed = 3;
moveYSpeed = 2;
blood = 100;
mana = 20;
moveDir = Vector2.zero;
curPos = Vector2.zero;
}
}
Role.CS脚本最终用来控制角色的脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Role :Base
{
//一个状态生命周期的三个关键函数
public IState currenState;
[HideInInspector]
public Animator animator;
//存储动画状态机上的所有动画
public Dictionary<string, AnimationClip> allClips = new Dictionary<string, AnimationClip>();
//定义字典通过键值对建立状态与其对象的联系
private Dictionary<State_Enum, IState> states = new Dictionary<State_Enum, IState>();
private void Awake()
{
//获得动画控制器上的所有动画
animator = GetComponent<Animator>();
AnimationClip[] clips= animator.runtimeAnimatorController.animationClips;
for(int i = 0; i < clips.Length; i++)
{
allClips.Add(clips[i].name, clips[i]);
}
//设置人物的基础属性
SetAttribute();
//向字典中添加对应的状态与其对象
states.Add(State_Enum.idle, new Idle(this));
states.Add(State_Enum.move, new Move(this));
states.Add(State_Enum.attack, new Attack(this));
//默认状态为idle
TransitionState(State_Enum.idle);
}
private void Update()
{
currenState.OnUpdate();
}
/// <summary>
/// 有限状态机状态切换函数
/// </summary>
/// <param name="type"></param>
public void TransitionState(State_Enum type)
{
if (currenState != null)
{
currenState.OnExit();
}
currenState = states[type];
currenState.OnEnter();
}
public void SetLookDir()
{
lookDir = (int)Input.GetAxisRaw("Horizontal");
if (lookDir != 0)
{
transform.localScale = new Vector3(lookDir, 1, 1);
}
}
public void MovePos()
{
SetLookDir();
moveDir.x = Input.GetAxisRaw("Horizontal") * moveXSpeed;
moveDir.y = Input.GetAxisRaw("Vertical") * moveYSpeed;
curPos = (Vector2)transform.position;
curPos = curPos + moveDir * Time.deltaTime;
transform.position = curPos;
}
public void EndAttack()
{
if (Input.GetAxisRaw("Horizontal") == 0 && Input.GetAxisRaw("Vertical") == 0)
{
TransitionState(State_Enum.idle);
}
else
{
TransitionState(State_Enum.move);
}
}
}
2.动画状态机切换条件设置:
Idle和Move动画都是循环动画
Idle->Move:取消勾选HasExitTime,可以适当调小idle到move的过度时间,就是相交的小蓝条宽度,Move=true,Idle=false
Move->Idle:取消勾选HasExitTime,Move=false,Idle=true
Move->Attack:取消勾选HasExitTime(因为我们有通过代码添加攻击动画的回调函数,所以不需要勾选),有Attack Trigger参数,Move=false
Attack->Move:取消勾选HasExitTime,无Attack Trigger参数,Move=true
Idle->Attack:取消勾选HasExitTime,有Attack Trigger参数,Idle=false
Attack->Idle:取消勾选HasExitTime,无Attack Trigger参数,Idle=true
参考链接:https://blog.csdn.net/xinzhilinger/article/details/115840911
本篇参考了别人写的FSM的架构,在其基础上增加了逻辑控制的具体内容,实现逻辑控制与动画表现相统一