NPC
是游戏中很重要的一部分,功能复杂,行为智能的NPC
可以给玩家带来更加优秀的游戏体验,我们可以通过对NPC
的行为模式的设计来理解有限状态机
而对于游戏开发者而言,要实现一个功能完备,逻辑清晰的AI状态图可不是一件简单的事情,如果没有前期的完备的设计,以及正确的编程方法,会在开发过程中产生很多难以解决的问题
在游戏开发过程中,你想要实现一个NPC
的一些功能,比如静止与攻击,当玩家靠近NPC
,NPC
就会攻击玩家,编写一段逻辑代码来实现这样的一个效果:
if(玩家靠近)
NPC攻击玩家代码
if(玩家远离)
NPC静止代码
在NPC
行为状态比较简单时,我们可以通过if判断来实现这样的效果
但是当NPC
的状态增加时呢,比如说有静止、攻击、追击、巡逻这些状态时呢,我们所有逻辑在使用if else
实现这样的转换就会使得逻辑变得很难理解,这个时候就可以使用Switch case
配合枚举 来进行状态的转化:
public enum State
{
idle,
patrol,
attact,
pursuit,
}
public class FishRod : MonoBehaviour
{
State state=new State();
public void Test()
{
switch (state)
{
case State.idle:
//idle状态下的NPC逻辑代码
break;
case State.patrol:
//巡逻状态下NPC逻辑代码
break;
case State.attact:
//攻击状态下的NPC逻辑代码
break;
case State.pursuit:
//追击状态下的NPC逻辑代码
break;
}
}
}
很明显,这样逻辑就会清晰很多,只需要在不同状态下修改state
对应的枚举类型即可,这样可以将判断代码与执行代码进行分离
通过Switch case
结合枚举可以很好的处理NPC
行为状态的逻辑,但是在实际生产中,对于每一种状态的处理也是很复杂,有事使用Switch case
会有一定的局限性,使用起来依旧很复杂,这就需要专门的状态管理的设计模式有限状态机来进行逻辑的处理
为了更好的实现对NPC
的行为状态的设计,逐渐产生了有限状态机这样的设计模式,来帮助游戏开发者通过框架层的开发特点进行更为简单的游戏开发
1、什么是有限状态机
有限状态机是基于状态模式的一种,首先第一点就是在有限的状态内进行相应状态的切换,而一种状态对应的是一个对象,即通过一个类来实现一个状态,这是有限状态机与Switch Case
的主要区别之处
什么是状态模式:
- 状态模式是一种设计模式
- 在状态模式(
State Pattern
)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式- 在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的
context
对象
有限状态机的实现的目的与特点:
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类
主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用:代码中包含大量与对象状态有关的条件语句。
如何解决:将各种具体的状态类抽象出来。
使用场景: 1、行为随状态改变而改变的场景 2、条件、分支语句的代替者
注意事项:在行为受状态约束的时候使用状态模式,而且状态不超过 5 个
为什么状态不超过五个呢
- 首先状态太多不好理解,难以记忆,其次超过五个状态一般来说很多状态没有联系
- 如果超过五个可以使用层次模式,大的状态嵌套小的没有联系状态
使用有限状态机的优点:
缺点:
2,代码实现有限状态机
第一步,创建一个脚本,定义一个接口IState
,用来声明一种状态的运行周期,如果你了解Unity脚本生命周期,很清楚代码三个方法代表的意义,如果你不了解也没有关系,我在下面列出了三个方法的关键用法:
OnEnter
:可以用来处理变量的初始化、只在进入状态时调用一次的代码模块OnUpdate
:用来处理重复调用的代码模块OnExit
:在状态退出时执行的代码,同样只执行一次接口IState的代码为:
public interface IState
{
//进入状态时调用一次
void OnEnter();
//处于状态时,连续调用
void OnUpdate();
//退出状态时调用一次
void OnExit();
}
接下来写一个基础的状态类BaseFSM
,用来实现状态的定义与切换:
首先定义一个枚举类型State_Enum
作为对象的所有状态,为了代码简洁,本次案例使用Idle
与Run
两种状态来进行有限状态机的实现:
// BaseFSM脚本
/// <summary>
/// 状态枚举类型
/// </summary>
public enum State_Enum
{
idle,
run
}
接下来创建一个新的脚本AllStates
两个新的类Idle
与Run
并继承于IState
,即创建一个状态的对象,用来实现状态的具体代码:
//AllStates脚本
/// <summary>
/// Idle状态
/// </summary>
public class Idle: IState
{
private BaseFSM manager;
//构造函数获取刚刚创建脚本的属性方法
public Idle(BaseFSM manager)
{
this.manager=manager;
}
public void OnEnter()
{
//进入Idle状态脚本写在这里
}
public void OnExit()
{
//退出Idle状态脚本写在这里
}
public void OnUpdate()
{
//Idle动态的脚本写在这里
}
}
/// <summary>
/// Run状态
/// </summary>
public class Run: IState
{
private BaseFSM manager;
//构造函数获取刚刚创建脚本的属性方法
public Idle(BaseFSM manager)
{
this.manager=manager;
}
public void OnEnter()
{
//进入Run状态脚本写在这里
}
public void OnExit()
{
//退出Run状态脚本写在这里
}
public void OnUpdate()
{
//Run状态动态的脚本写在这里
}
}
接下来我们要返回BaseFSM
脚本,去实现不同状态与其对象的管理与切换功能,基本流程如下:
BaseFSM
脚本中调用IState
中对应三种运行状态具体的代码如下:
//在BaseFsm脚本中
public class BaseFSM : MonoBehaviour
{
//一个状态生命周期的三个关键函数
public IState currenState;
//定义字典通过键值对建立状态与其对象的联系
private Dictionary<State_Enum, IState> states = new Dictionary<State_Enum, IState>();
private void Awake()
{
//向字典中添加对应的状态与其对象
states.Add(State_Enum.idle,new Idle(this);
states.Add(State_Enum.run,new Run(this);
//默认状态为idle
TransitionState(State_Enum.idle);
}
private void Update()
{
//同样在Update中调用每一种状态对应的OnUpdate
currenState.OnUpdate();
}
/// <summary>
/// 有限状态机状态切换函数
/// </summary>
/// <param name="type"></param>
public void TransitionState(State_Enum type)
{
if(currenState!=null)
{
currenState.OnExit();
}
currenState = states[type];
currenState.OnEnter();
}
}
这样一个基本的有限状态机的框架就搭建完毕了,需要通过状态切换的触发方法来进行状态的切换,比如说,当玩家按下W
键时,我们可以在BaseFSM
来监控这样的一个时间,从而实现状态的切换:
//BaseFSM脚本
private void Update()
{
//同样在Update中调用每一种状态对应的OnUpdate
currenState.OnUpdate();
if(Input.GetKeyDown(KeyCode.W))
{
TransitionState(State_Enum.run);
}
}
当然,目前来说这只是一个简单的有限状态机框架,如果要实现完整的功能的话,还需要一些细节去完善这个代码框架,而这样的工作就需要游戏开发者自己去完成,所以快快动手,实现自己需要的功能吧