老实说,很久前学习了多种有限状态机写法,有学习打工人小祺的、有跟着鬼鬼鬼ii的,有学习Joker老师的…学得越多反而越来越不知道怎么做。但是分析这次需求,我们决定玩家和敌人都用有限状态机,我觉得Joker老师写得最好。既然Joker老师造好了轮子,我们直接拿来用最方便了。


实践过程

在设计有限状态机前,我们先设置好我们的状态基类StateBase,记录下我们所有要执行的函数。
plaintext
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 OnUpdate()
{

}
public virtual void OnLateUpdate()
{

}
public virtual void OnFixedUpdate()
{

}
}

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

plaintext
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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//状态机基类
public interface StateMachineOwner
{

}
public class StateMachine
{
private StateMachineOwner owner;
private StateBase currentState;
//存储状态
private Dictionary<Type, StateBase> stateDic = new Dictionary<Type, StateBase>();
public void Init(StateMachineOwner owner)
{
//初始化先保存自己
this.owner = owner;
}
//记录当前状态

public void ChangeState<T>() where T : StateBase, new()
{
if (currentState!= null && typeof(T) == currentState.GetType())
{
Debug.Log("你切换的状态与当前状态相同");
return;
}
if(currentState != null)
{
currentState.OnExit();
//移除方法
MonoManager.MainInstance.RemoveUpdate(currentState.OnUpdate);
MonoManager.MainInstance.RemoveFixedUpdate(currentState.OnFixedUpdate);
MonoManager.MainInstance.RemoveLateUpdate(currentState.OnLateUpdate);
}
//切换了状态,并执行一系列函数
currentState = GetState<T>();
if (currentState == null) Debug.Log("currentState为空,没有找到该状态");
currentState.OnEnter();
MonoManager.MainInstance.AddUpdate(currentState.OnUpdate);
MonoManager.MainInstance.AddFixedUpdate(currentState.OnFixedUpdate);
MonoManager.MainInstance.AddLateUpdate(currentState.OnLateUpdate);
}

//获得状态
private StateBase GetState<T>() where T : StateBase, new()
{
// 如果不包含,就添加进去
if (!stateDic.ContainsKey(typeof(T)))
{
// 添加状态
stateDic.Add(typeof(T), new T());
// 初始化状态
stateDic[typeof(T)].Init(owner);
}
return stateDic[typeof(T)];
}
//状态机清除
public void ClearFSM()
{
//执行反初始化卸载函数
currentState.UnInit();
//移出方法
MonoManager.MainInstance.RemoveUpdate(currentState.OnUpdate);
MonoManager.MainInstance.RemoveFixedUpdate(currentState.OnFixedUpdate);
MonoManager.MainInstance.RemoveLateUpdate(currentState.OnLateUpdate);
//清除状态机
stateDic.Clear();
}

}


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

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

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

private void Awake()
{
if (_animator == null) Debug.Log("你忘记拖入Aniamto组件啦");
}

//动画事件注册
}

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

plaintext
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player_Controller : MonoBehaviour,StateMachineOwner
{
//拖入角色模型
[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

plaintext
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
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();
//检测攻击

//检测跳跃

//检测玩家移动
}
}

PlayerState脚本部分:

plaintext
1
2
3
4
5
6
7
8
9
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum PlayerState
{
Idle,
Move,
}

解释部分?

这个状态机的组成部分,最少共调用了六个脚本,StateBase、Player_Controller、PlayerState、PlayerStateBase、Player_Model、StateMachine,最初我是看得一愣一愣。这里稍微解释一下,不想看解释、想要直接上手用可以往下看、跳过。

首先StateBase是所有状态的基类,是我们每个状态都会用的方法。Init是初始化方法,传入我们的状态机、进行初始化,UnInit是卸载资源用的方法。

StateMachine是我们的状态机主逻辑脚本,因为分有敌人状态机、玩家状态机,所以再写一个接口,两种状态机继承该接口。

PlayerState是把状态封装成枚举类型。

PlayerStateBase是我们玩家的状态基类,我们所有玩家的状态都要继承它。由于我们的每个状态都要用到角色控制器,角色控制器又是状态机的子类,所以要引入控制器。Init方法是在StateMachine的GetState里调用的,每注册一个状态都会初始化一次。

Player_Controller是我们的玩家状态机,主要通过模型层和状态机处理逻辑。

Player_IdleState是写的一个示例状态脚本,Player_Model是我们的模型层上文件,拖给角色模型。

如何使用?

首先我们打开PlayerState,在枚举中添加状态。

接着我们创建新的状态脚本,继承PlayerStateBase,书写对应方法。通过player_Controller.ChangeState(PlayerState.枚举状态)来转换状态。

最后打开Player_Controller,在ChangeState方法里写新的case,stateMachine.ChangeState<状态脚本文件>();

plaintext
1
2
3
case(PlayerState.枚举状态):
stateMachine.ChangeState<状态脚本>();
break;