简单的前言
在平时我们会玩到一些RPG游戏,其中的角色能走能跑能飞能打架,打架的时候还能释放各种眼花缭乱的技能,我们为这个角色赋予了很多特性,这个时候就需要编写一套状态管理系统来对不同状态之间的切换进行统一的管理。
由于角色在某一时间只会处于一种状态,不可能边飞边站立嘛!,因此适合用有限状态机(FSM)进行管理 ,这篇文章将会以小怪状态管理为例,简单介绍FSM在Unity中的实现。寒假写的代码可以拿来摸鱼啦。
写代码前的构思
首先先,我们需要理清楚不同状态之间的逻辑联系,画好图,才能条理清晰地打出好康的代码。
我们的小怪,设定是在没探测到角色时巡逻,探测到角色时追击,接近敌人时攻击,HP为0时死亡。巡逻的设定是到达巡逻点时站着不动一段时间,然后前往下个巡逻点。
能想到的状态有:
- 站着不动 Idle
- 走向下个巡逻点 Patrol
- 追击 Chase
- 攻击 Attack
- 死亡 Die
还不算很复杂,下面画出好丑好丑好丑的关系图。
下面来写代码啦。
代码展示
接口定义
每种状态独立创建为一个类,都有状态进入,状态进行,状态退出三个的方法(也就是函数,C#中称之为方法),这样能使得写出来的代码比较具有扩展性。(比如扩展成多层状态机?)
既然每种状态写成类,而且每种状态的结构大致相同,那么就需要定义一个接口,来实现对不同状态的调用。
public interface EState
{
void StateIn();
void StateStay();
void StateExit();
}
有了接口,我们就可以写出各个状态的框架啦!
不过先不用着急写各个状态的内容,框架搭好后,就来写SFM了。
搭建FSM
封装小怪的各种参数
public class EParam
{
public int HP;
public float moveSpeed; //巡逻速度
public float chaseSpeed; //追击速度
public float idleTime; //站立时间
public Transform[] partolPoint; //巡逻点
public float chaseRange; //追击范围
public Animator animator; //小怪的动画,如果有的话
public Transform player; //如果检测到玩家,则不为null
public EState current; //当前状态
}
检索状态
由于我们将状态写成类,直接调用费时费力,因此利用到Dictionary来实现不同状态的调用。
//先定义枚举类型StateType作为Dictionary的Key
public enum StateType
{
Idle,Patrol,Chase,Attack,Die
}
public class FSM : MonoBehaviour
{
//以StateType为Key,Estate为value,创建Dictionary对象states
private Dictionary<StateType, EState> states = new Dictionary<StateType, EState>();
public EParam param;
private EState currentState;
//在Start函数中注册状态,并将初始状态设置为Idle
private void Start()
{
states.Add(StateType.Idle, new IdleState(this));
states.Add(StateType.Patrol, new PatrolState(this));
states.Add(StateType.Attack, new AttackState(this));
states.Add(StateType.Die, new DieState(this));
states.Add(StateType.Chase, new ChaseState(this));
TransformState(StateType.Idle);
}
}
切换状态
//切换状态要先退出再进入
public void TransformState(StateType type)
{
if (currentState != null)
currentState.StateExit();
currentState = states[type];
currentState.StateIn();
}
保持状态
private void Update()
{
currentState.StateStay();
param.current = currentState;
//无论哪种状态都要判断是否死亡,所幸直接Update里进行判断
if (param.HP <= 0)
TransformState(StateType.Die);
}
角色探测
这里调用了Unity的触发检测的接口
private void OnTriggerEnter(Collider other)
{
if (other.tag == "Character")
{
param.player = other.transform;
other.GetComponent<DectiveEnermy>().Enermys.Add(this.transform);
}
}
private void OnTriggerExit(Collider other)
{
if (other.tag == "Character")
{
transform.GetComponent<MeshRenderer>().enabled = false;
other.GetComponent<DectiveEnermy>().Enermys.Remove(this.transform);
param.player = null;
}
}
左右转向
public void Filpto(Transform target)
{
if (transform.position.x > target.position.x)
transform.localScale = new Vector3(-1, 1, 1);
else
transform.localScale = new Vector3(1, 1, 1);
}
OK,这就是挂载在小怪身上的全部代码啦,接下来我们来分别完善各个不同的状态。并实现不同状态之间的切换。
编写各状态
IdleState
由图可知,IdleState在超过指定时间后会切换为PatrolState,探测到角色时切换为ChaseState。
public class IdleState : EState
{
private FSM manager;
private EParam param;
private float timer;//计时器
//初始化
public IdleState(FSM manager)
{
this.manager = manager;
param = manager.param;
}
public void StateIn()
{
}
public void StateStay()
{
timer += Time.deltaTime;
if(timer>param.idleTime)
{
manager.TransformState(StateType.Patrol);
}
if (param.player!=null)
manager.TransformState(StateType.Chase);
}
public void StateExit()
{
timer = 0;
}
}
PatrolState
PatrolState在到达巡逻点时切换为IdleState,检测到角色时切换为ChaseState。
public class PatrolState : EState
{
private FSM manager;
private EParam param;
private int num;//记录巡逻点
public PatrolState(FSM manager)
{
this.manager = manager;
param = manager.param;
}
public void StateIn()
{
}
public void StateStay()
{
//实现转向。
manager.Filpto(param.partolPoint[num]);
manager.transform.position = Vector3.MoveTowards(manager.transform.position,param.partolPoint[num].position, param.moveSpeed * Time.deltaTime);
//如果角色与巡逻点之间的距离<0.01,可认为角色到达巡逻点
if (Vector3.Distance(manager.transform.position, param.partolPoint[num].position) < 0.1)
manager.TransformState(StateType.Idle);
if (param.player)
manager.TransformState(StateType.Chase);
}
public void StateExit()
{
num++;
//如果到达最后一个巡逻点,则回归第一个巡逻点
if (num >= param.partolPoint.Length)
num = 0;
}
}
ChaseState
ChaseState在角色出探测范围时转换为PatrolState,在接近角色时转换为AttackState。
public class ChaseState : EState
{
private FSM manager;
private EParam param;
public ChaseState(FSM manager)
{
this.manager = manager;
param = manager.param;
}
public void StateIn()
{
}
public void StateStay()
{
if (param.player)
{
manager.Filpto(param.player);
manager.transform.position = Vector3.MoveTowards(manager.transform.position,param.player.position, param.chaseSpeed * Time.deltaTime);
if (Vector3.Distance(manager.transform.position, param.player.position) < 0.5)
{
manager.TransformState(StateType.Attack);
}
}
if (param.player == null || Vector3.Distance(manager.transform.position, param.player.position) > param.chaseRange)
manager.TransformState(StateType.Patrol);
}
public void StateExit()
{
}
}
AttackState
AttackState在角色远离时转换为ChaseState
public class AttackState : EState
{
private FSM manager;
private EParam param;
private float timer;
public AttackState(FSM manager)
{
this.manager = manager;
param = manager.param;
}
public void StateIn()
{
}
public void StateStay()
{
timer += Time.deltaTime;
//攻击效果有些复杂,于是用靠近时主角受击来代替。
if (timer < 0.25)
param.player.GetChild(0).transform.GetComponent<SpriteRenderer>().material.color = Color.red;
else
{
if (timer < 1.5)
param.player.GetChild(0).transform.GetComponent<SpriteRenderer>().material.color = Color.white;
else
timer = 0;
}
if(Vector3.Distance(manager.transform.position,param.player.transform.position)>0.5)
manager.TransformState(StateType.Chase);
}
public void StateExit()
{
}
}
DieStates
public class DieState : EState
{
private FSM manager;
private EParam param;
public DieState(FSM manager)
{
this.manager = manager;
param = manager.param;
}
public void StateIn()
{
GameObject.Destroy(manager.transform.gameObject);
}
public void StateStay()
{
}
public void StateExit()
{
}
}
OK,这样所有的状态都写好了。
接下来看看效果吧
巡逻状态转追击状态
脱离追击
追击状态转攻击状态