Enemy状态机设计思路
前言:
为了更清晰的认识状态机并且理清 Enemy 设计思路,所以整理了一下 Enemy 的代码设计逻辑
做了一张简单的思维图先进行一个简单的认识
干货:FMS有限状态机
状态机类似于动画器 (animator) ,动画器可以简单清晰地管理游戏角色的动画:待机、跳跃、下落、跑步……,状态机的目的也是如此,每一个角色都有不同的行为方式,当这个角色的行为方式数量极大时,就有可能出现代码处理漏掉等各种问题,而为了更方便清晰地管理每一个状态,减少维护成本,引入了状态机这个概念
如何具体设计?
设计状态机的目的就是为了优化代码,使逻辑清晰
根据我们的需求来设计
首先创建一个简单的敌人(本体)
敌人包含了他的属性,以及一些必要的行为(函数),函数的具体内容就不写出来了,可以根据需求填入
为了简化代码,这个 Enemy类也可以作为一个父类,不同的敌人都继承这个父类获取必要的属性等,如果有部分不同可以直接进行函数的重写
public class Enemy: MonoBehaviour { public float speed; //速度 public float maxhealth; //最大生命值 public float currenthealth; //当前生命值 public Transform pointA, pointB; //来回巡逻的两个端点 public Vector2 targetpoint; //移动目标点 public Animator anim; //获取动画器播放动画 private void Start() { } private void Update() { } public void MoveAction() //移动函数 { } public void IdleAction() //待机函数 { anim.play("idle");//播放待机动画 } //不同的敌人攻击方式可能不同,有的是远程攻击,有的是近战攻击,所以用关键字 virtual 写成虚方法,然后在子类直接重写 public virtual void AttackAction() //攻击函数 { } }
其次设计状态机
当我们从一个状态进入到另一个状态,那么就会涉及到 第一个状态的结束(这一步根据需求可有可无) , 第二个状态的开始 , 第二个状态的持续 这三个过程(直到收到切换为下一个状态的信号才跳出)
所以我们会有 这三个必要过程 来组成每个状态,但是因为每个不同的状态都会有这三个过程,这里不妨优化一下代码,让所有的状态都继承一个父类,减少冗杂繁琐的相同代码,所以创建一个抽象的基类,命名为 EnmeyBaseState
,这个类声明三个抽象函数,函数的实现由子类确定:
(利用面向对象的 抽象类 实现 多态 )
public abstract class EnemyBaseState { protected Enemy enemy; //创建 Enemy 用于在子类中获取项目本体方便调用本体中的函数 public abstract void EnterState(); //状态的开始 public abstract void OnUpdate(); //状态的持续 public abstract void EndState(); //状态的结束 }
抽象类以及抽象函数创建好了,函数的实现我们就只需要在子类每个状态中实现
以攻击状态为例:
攻击状态命名为 AttackState
, 并且需要继承我们的基类 EnemyBaseState
同时 必须实现 EnemyBaseState
中声明的抽象函数(如果不需要用到对应的过程我们可以不填写实现内容,但是函数必须实现)
为了方便获取项目本体并且 减少一点代码量 ,我用了构造函数获取本体
public class AttackState : EnemyBaseState { //用构造函数直接获取项目本体 public AttackState(Enemy object) { enemy = object; } //不需要用到的过程我实现了函数后里面没有填写任何东西 public override void EnterState() { } //持续执行攻击函数 public override void OnUpdate() { //调用本体的攻击函数实现攻击 enmey.AttackAction(); } //不需要用到的过程我实现了函数后里面没有填写任何东西 public override void EndState() { } }
按着这个样子同样写好待机状态和巡逻状态,这里就不重复了
最后合并状态机和本体项目
本体要获取到状态机的所有状态,当状态数量太大时可能会影响我们编写代码的效率以及速度,所以我们可以使用枚举代表所有的状态,并且用字典来将枚举中的状态和状态机的状态一一对应起来
在 Enemy 类中继续添加,并且最开始执行代码时添加到字典中
public enum State //创建枚举 { idle, patrol, attack } private Dictionary<State_Enum, EnemyBaseState> states = new Dictionary<State_Enum, EnemyBaseState>(); //创建字典 private void Awake() { states.Add(State.idle, new IdleState(this)); //添加待机状态到字典 states.Add(State.patrol, new PatrolState(this)); //添加巡逻状态到字典 states.Add(State.attack, new AttackState(this)); //添加攻击状态到字典 }
获取了状态之后我们要写一个函数用于状态的转换
之后我们就可以直接通过这个函数 传入状态参数来切换目标状态
//创建一个 EnemyBaseState 类的参数,用于控制 Enemy 当前的状态 public EnemyBaseState currentState; public void TransitionState(State type) //切换状态函数 { currentState.EndState(); //调用上一个状态的结束 currentState = states[type]; //切换下一个状态 currentState.EnterState(); //调用下一个状态的开始 } public void Update() { currentState.OnUpdate(); //持续调用这一个状态的持续 }
最后本体的整体代码如下:
具体什么时候需要切换状态,根据自己需求设置
public class Enemy: MonoBehaviour { public float speed; //速度 public float maxhealth; //最大生命值 public float currenthealth; //当前生命值 public Transform pointA, pointB; //来回巡逻的两个端点 public Vector2 targetpoint; //移动目标点 public Animator anim; //获取动画器播放动画 public EnemyBaseState currentState; //当前状态 private float idletime; public enum State //创建枚举 { idle, patrol, attack } private Dictionary<State_Enum, EnemyBaseState> states = new Dictionary<State_Enum, EnemyBaseState>(); //创建字典 private void Awake() { states.Add(State.idle, new IdleState(this)); //添加待机状态到字典 states.Add(State.patrol, new PatrolState(this)); //添加巡逻状态到字典 states.Add(State.attack, new AttackState(this)); //添加攻击状态到字典 TransitionState(idle); //一开始直接调用idle状态 } public void Update() { currentState.OnUpdate(); //持续调用这一个状态的持续 } public void TransitionState(State type) //切换状态函数 { currentState.EndState(); //调用上一个状态的结束 currentState = states[type]; //切换下一个状态 currentState.EnterState(); //调用下一个状态的开始 } public void MoveAction() //移动函数 { } public void IdleAction() //待机函数 { idleTime -= Time.deltaTime; anim.play("idle");//播放待机动画 if (idleTime <= 0) { TransitionState(State.patrol); //待机时间结束切换巡逻状态 } } //不同的敌人攻击方式可能不同,有的是远程攻击,有的是近战攻击,所以用关键字 virtual 写成虚方法,然后在子类直接重写 public virtual void AttackAction() //攻击函数 { anim.play("attack"); } }
总结:
敌人作为一个父类 统一管理所有子类,通过状态机来进行状态的切换,状态机有一个 基类作为父类 ,管理所有的状态
敌人通过 枚举 和 字典 获取所有的状态,并且在需要的时候切换状态
每个状态都用 构造函数 来获取敌人本体,用于在切换到当前状态时获取本体的一些数据或者使用本体的函数
本文作者:shadow-fy
本文链接:https://www.cnblogs.com/shadow-fy/p/17316527.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步