笔记42 FSM有限状态机
传统编码
拖放模型。设定动画。
代码:Player1Controller+出现的小bug
//拿到动画控制器
private Animator ani;
void Start()
{
ani = GetComponent<Animator>();
}
void Update()
{
//拿到轴
float vertical = Input.GetAxis("Vertical");
float horizontal = Input.GetAxis("Horizontal");
//做成向量
Vector3 dir = new Vector3(horizontal,0, vertical);
//移动
if (dir != Vector3.zero)
{
//旋转
transform.rotation = Quaternion.LookRotation(dir);
//前进
transform.Translate(Vector3.forward * 3 * Time.deltaTime);
//播放跑步动画
ani.SetBool("isRun", true);
}
else
{
ani.SetBool("isRun", false);
}
//攻击
if (Input.GetKeyDown(KeyCode.Space))
{
//SetTrigger触发
ani.SetTrigger("attack");
}
//再有类似的,就继续添加if(){}
}
缺点多于优点
再有类似的,就继续添加if(){}。
整个Update里写了一堆if。
一个if就代表一个状态。
如果写了二十多个if,一是,修改起来麻烦;
二是,它是按照顺序来的,有时修改了中间的代码,下面的代码可能出现不知解的bug。
三是,用这种方式写连击,很麻烦。
四是,比如攻击动画没做完,按跳跃是可以跳跃的,这是不允许的。
有个优点,就是与玩家相关的代码全写在这一个文件里了。
有限状态机有两种简单方式:方法、类
法一:方法+代码:Player2Controller
//枚举。玩家的状态
public enum PlayerState
{
idle,
run,
attack
}
public class Player2Controller : MonoBehaviour
{
//动画
private Animator ani;
//当前的状态。默认是站立
private PlayerState state = PlayerState.idle;
void Start()
{
//获取ani
ani = GetComponent<Animator>();
}
void Update()
{
//switch:切换小括号里内容的取值。点击“补齐代码”。
switch (state)
{
case PlayerState.idle:
//补充:如果是站立,就调用站立的方法。
idle();
break;
case PlayerState.run:
run();
break;
case PlayerState.attack:
attack();
break;
}
}
//有几个状态,就写几个方法。此处有三个。
//每个里面,写每个要做的事。
void idle()
{
//站立动画
ani.SetBool("isRun", false);
//得到轴,形成向量
float vertical = Input.GetAxis("Vertical");
float horizontal = Input.GetAxis("Horizontal");
Vector3 dir = new Vector3(horizontal, 0, vertical);
//判断向量不为零,说明按了AWSD某个按键了,跑步。
if (dir != Vector3.zero)
{
//切换到跑步状态
state = PlayerState.run;
}
//攻击
if (Input.GetKeyDown(KeyCode.Space))
{
//切换到攻击状态
state = PlayerState.attack;
}
}
void run()
{
//跑步动画
ani.SetBool("isRun", true);
//得到轴,形成向量
float vertical = Input.GetAxis("Vertical");
float horizontal = Input.GetAxis("Horizontal");
Vector3 dir = new Vector3(horizontal, 0, vertical);
//判断向量不为零,说明按了AWSD某个按键了,跑步。
if (dir != Vector3.zero)
{
//朝向向量
transform.rotation = Quaternion.LookRotation(dir);
//前进
transform.Translate(Vector3.forward * 3 * Time.deltaTime);
}
else
{
//切换动画状态
state = PlayerState.idle;
}
}
void attack()
{
//触发攻击
ani.SetTrigger("attack");
//如果当前状态名称(方块上的名称)不是攻击,说明攻击的动画播放完了。
//if如果 (!不是ani动画.GetCurrentAnimatorStateInfo状态(0).IsName名称("attack1"攻击))
if (!ani.GetCurrentAnimatorStateInfo(0).IsName("attack1"))
{
//切换站立状态
state = PlayerState.idle;
}
}
}
法二:类+代码
需要写几个类
代码:FSMState
//abstract抽象类(不能被实例化。必须有子类继承,对应关键词override)
public abstract class FSMState
{
//当前状态ID。它是int类型,因为玩家的状态我们是写在枚举里的。
public int stateID;
//状态拥有者。需要把角色控制类传过来
public MonoBehaviour mono;
//状态所属管理器
public FSMManager fsmManager;
//初始化方法
public FSMState(int stateID,MonoBehaviour mono,FSMManager manager)
{
//赋值
this.stateID = stateID;
this.mono = mono;
this.fsmManager = manager;
}
//抽象方法(子类必须实现的方法)只能包含在抽象类当中。
//用虚方法(允许子类重写方法)也可以,类就不用写成抽象类了。
public abstract void OnEnter();
public abstract void OnUpdate();
}
代码:FSMManager
public class FSMManager
{
//状态列表
public List<FSMState> stateList = new List<FSMState>();
//当前的状态(索引)。-1表示没给状态。
public int currentIndex = -1;
//改变状态
public void ChangeState(int StateID)
{
//当前状态=传进来的状态。
currentIndex = StateID;
//stateList[currentIndex]从数组里取出对应的状态
//.OnEnter()调用一次OnEnter方法,确保切换到新状态的第一时间执行OnEnter方法
stateList[currentIndex].OnEnter();
}
//更新方法。这个Update是不会调用的,需要玩家里的Update协助调用。
public void Update()
{
//如果当前有状态
if (currentIndex != -1)
{
//stateList[currentIndex]从列表里拿到当前状态
//.OnUpdate()调这个状态里的OnUpdate方法,确保OnUpdate会被一直调用。
stateList[currentIndex].OnUpdate();
}
}
}
代码:IdleState : FSMState
/继承于抽象类FSMState。点击“生成构造函数”“实现抽象类”
public class IdleState : FSMState
{
//必须实现构造方法。为了new的时候更方便。 new IdleState(小括号里提示写三个参数)
public IdleState(int stateID, MonoBehaviour mono, FSMManager manager) : base(stateID, mono, manager)
{
}
//状态的初始化方法,只调用一次
public override void OnEnter()
{
//mono状态拥有者,其实就是PlayerController
mono.GetComponent<Animator>().SetBool("isRun", false);
}
//状态的刷新方法,循环调用
public override void OnUpdate()
{
//监听跑步状态,切换。
//得到轴,组成向量,判断
float vertical = Input.GetAxis("Vertical");
float horizontal = Input.GetAxis("Horizontal");
Vector3 dir = new Vector3(horizontal, 0, vertical);
if (dir != Vector3.zero)
{
//fsmManager状态所属于的管理类。(int)PlayerState.run把枚举转成int类型
fsmManager.ChangeState((int)PlayerState.run);
}
//监听攻击状态,切换。
if (Input.GetKeyDown(KeyCode.Space))
{
fsmManager.ChangeState((int)PlayerState.attack);
}
}
}
代码:RunState : FSMState
public class RunState : FSMState
{
public RunState(int stateID, MonoBehaviour mono, FSMManager manager) : base(stateID, mono, manager)
{
}
public override void OnEnter()
{
//跑步动画状态
mono.GetComponent<Animator>().SetBool("isRun", true);
}
public override void OnUpdate()
{
//得到轴,形成向量,判断,前进
float vertical = Input.GetAxis("Vertical");
float horizontal = Input.GetAxis("Horizontal");
Vector3 dir = new Vector3(horizontal, 0, vertical);
if (dir != Vector3.zero)
{
//朝向向量
mono.transform.rotation = Quaternion.LookRotation(dir);
//前进
mono.transform.Translate(Vector3.forward * 3 * Time.deltaTime);
}
else
{
//切换到站立状态
fsmManager.ChangeState((int)PlayerState.idle);
}
}
}
代码:AttackState : FSMState
public class AttackState : FSMState
{
public AttackState(int stateID, MonoBehaviour mono, FSMManager manager) : base(stateID, mono, manager)
{
}
public override void OnEnter()
{
mono.GetComponent<Animator>().SetTrigger("attack");
}
public override void OnUpdate()
{
//判断当前状态如果不是攻击1,就切换到站立
if (!mono.GetComponent<Animator>().GetCurrentAnimatorStateInfo(0).IsName("attack1"))
{
fsmManager.ChangeState((int)PlayerState.idle);
}
}
}
代码:PlayerState3
//枚举。玩家的状态。(因为写第二个恐龙时,写了枚举PlayerState,所以此处改名为PlayerState3)
//这个枚举不妨在这的话,在下面的那行代码无用。
public enum PlayerState3
{
idle,
run,
attack
}
public class player3Controller : MonoBehaviour
{
//状态管理类
private FSMManager fsmManager;
void Start()
{
//new一个
fsmManager = new FSMManager();
//实例化各种状态。(0索引与枚举里的一致, this传进来的所属管理类, fsmManager属于哪个管理器类,刚刚new的)
IdleState idle = new IdleState(0, this, fsmManager);
RunState run = new RunState(1, this, fsmManager);
AttackState attack = new AttackState(2, this, fsmManager);
//把三种状态添加到管理类列表
fsmManager.stateList.Add(idle);
fsmManager.stateList.Add(run);
fsmManager.stateList.Add(attack);
//管理类默认切换成站立状态
fsmManager.ChangeState((int)PlayerState.idle);
}
void Update()
{
//调用管理类里的Update
//因为FSMManager里的Update方法不会自己运行,此处,帮助它运行。
//进而,它就会调用当前状态里的OnUpdate。
fsmManager.Update();
}
}