Unity有限状态机(第三版)

有限状态机实现方式不止一种,这里标题为第三版,不是说前面几版不好,而是又提供了一种新的有限状态机写法。

我之前对有限状态机的想法是,用来做敌人AI,现在再次开阔视野,可以用代码的形式来替代Animator窗口复杂的连线(之前也有人推荐我用Animancer插件,等我学到了我再学),简洁清晰地转换状态。

这个思路主要借鉴自Joker老师的RPG进阶教程,有能力的话大家可以去支持一下Joker老师。


思路分析

再开始写代码前,我们需要先分析自己的需求:
首先是状态机,大概分为敌人状态机、玩家状态机,那么就声明个基类状态机最好。状态机怎么写?首先我们需要一个字典容器来存储所有的状态,然后我们需要一个转换状态的方法,然后我们还需要存储下当前状态,就写一个GetState的方法。
状态怎么写?首先我们有个状态基类StateBase,记录所有状态都要有的方法;再分别写PlayerStateBase和EnemyStateBase继承StateBase,最后我们具体的状态再次分门别类的继承,这样就很有条理了。

实践过程

在设计有限状态机前,我们先设置好我们的状态基类StateBase,记录下我们所有要执行的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class StateBase
{
//初始化
public virtual void Init(IStateMachineOwner owner)
{

}

//卸载资源
public virtual void UnInit()
{

}

public virtual void Enter()
{

}
public virtual void Exit()
{

}
public virtual void Update()
{

}
public virtual void LateUpdate()
{

}
public virtual void FixedUpdate()
{

}
}

接下来我们去写我们的有限状态机,我们状态机大概分为两类,一类是敌人状态机,一类是玩家状态机,那就声明一个共同的接口或者抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//首先我们状态及可能分为敌人状态机、玩家状态机,我们声明个基类、接口、宿主
public interface IStateMachineOwner { }
//这里我采用不继承MonoBehaviour的方式
public class StateMachine
{

//存储有限状态机
private IStateMachineOwner _owner;
public void Init(IStateMachineOwner owner)
{

//保存我们的状态机
_owner = owner;
}

//我们用Type进行状态存储
private Dictionary<Type,StateBase> StateDic = new Dictionary<Type,StateBase>();

//当前状态
private StateBase currentState;

public void ChangeState<T>()where T : StateBase, new()
{

//执行完退出函数再卸载所有方法
if (currentState != null)
{

//如果改变状态与当前状态重复就return;
if (typeof(T) == currentState.GetType()) return;
currentState.Exit();
//Mono管理器启动!
MonoManager.GetInstance().RemoveUpdate(currentState.Update);
MonoManager.GetInstance().RemoveFixedUpdate(currentState.FixedUpdate);
MonoManager.GetInstance().RemoveLateUpdate(currentState.LateUpdate);
}
currentState = GetState<T>();
currentState.Enter();
MonoManager.GetInstance().AddUpdate(currentState.Update);
MonoManager.GetInstance().AddFixedUpdate(currentState.FixedUpdate);
MonoManager.GetInstance().AddLateUpdate(currentState.LateUpdate);
}

//这样写的好处是不用在一开始就注册很多状态了
private StateBase GetState<T>() where T : StateBase,new()
{

//先检查字典有没有这个状态
if(!StateDic.ContainsKey(typeof(T)))
{
StateDic.Add(typeof(T), new T());

//早在注册状态时我们就执行Init方法
StateDic[typeof(T)].Init(_owner);
}
return StateDic[typeof(T)];
}

//停止状态机
public void Stop()
{
if (currentState != null)
{
currentState.Exit();
MonoManager.GetInstance().RemoveUpdate(currentState.Update);
MonoManager.GetInstance().RemoveFixedUpdate(currentState.FixedUpdate);
MonoManager.GetInstance().RemoveLateUpdate(currentState.LateUpdate);
}
foreach (var items in StateDic.Values)
{
items.UnInit();
}
StateDic.Clear();
}
}

巧用有限状态机制作角色控制器

如何使用我们的有限状态机呢?这里Joker老师采用了类似MVC思想的套路,先写一个角色模型层Player_Model,里面包含我们的动画机。
1
2
3
4
5
6
7
8
9
10
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player_Model : MonoBehaviour
{
[SerializeField,Header("拖入动画状态机")]private Animator _animator;
public Animator Animator =>_animator;
}

之后写一个角色控制器脚本Player_Controller,我们分析,需要写播放动画的方法、状态转换的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player_Controller : MonoBehaviour,IStateMachineOwner
{
[SerializeField] private Player_Model player_Model;
private StateMachine stateMachine;

//初始化状态机
private void Start()
{
stateMachine = new StateMachine();
stateMachine.Init(this);

//注册默认状态
stateMachine.ChangeState<Player_IdleState>();
}
//提供播放动画的方法
public void PlayAnimation(string animation,float fixedtime = 0.25f)
{
player_Model.Animator.CrossFadeInFixedTime(animation,fixedtime);
}

//提供改变状态的方法,利用枚举
public void ChangeState(PlayerState state)
{
switch (state)
{
case(PlayerState.Idle):
stateMachine.ChangeState<Player_IdleState>();
break;
}
}
}

接下来针对不同状态不同的写,但是我们状态转换多是需要播放动画的,必然需要PlayerAnimaton方法,那就想办法得到我们的Player_Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class PlayerStateBase : StateBase
{
//所有状态都需要先得到我们的控制器
protected Player_Controller player_Controller;

public override void Init(IStateMachineOwner owner)
{
base.Init(owner);
player_Controller = (Player_Controller)owner;
}

}

//进入时默认播放待机动画
public class Player_IdleState : PlayerStateBase
{
public override void Enter()
{
//播放角色待机动画
player_Controller.PlayAnimation("Idle");
}
public override void Update()
{
base.Update();
//检测攻击

//检测跳跃

//检测玩家移动
}
}

最后捋一捋我们状态机运行的流程:开始执行Player_Controller的Start方法注册状态——调用了StateMachine里的ChangeState方法——执行了GetState方法——执行了PlayerStateBase的Init方法得到了Player_Contorrler脚本——执行了Enter方法——调用了Player_Contoller的PlayerAnimation方法和ChangeState方法——播放动画和状态转换。