Enemy状态机设计思路

前言:

为了更清晰的认识状态机并且理清 Enemy 设计思路,所以整理了一下 Enemy 的代码设计逻辑

做了一张简单的思维图先进行一个简单的认识

image-20230413123527705

干货: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");
    }
}

总结:

image-20230413213033870

敌人作为一个父类 统一管理所有子类,通过状态机来进行状态的切换,状态机有一个 基类作为父类 ,管理所有的状态

敌人通过 枚举字典 获取所有的状态,并且在需要的时候切换状态

每个状态都用 构造函数 来获取敌人本体,用于在切换到当前状态时获取本体的一些数据或者使用本体的函数

posted @ 2023-04-13 21:34  Shadow-Fy  阅读(58)  评论(0编辑  收藏  举报