首页 > 其他分享 >Enemy状态机设计思路

Enemy状态机设计思路

时间:2023-04-13 21:44:50浏览次数:34  
标签:Enemy 函数 状态 void 状态机 思路 public

前言:

为了更清晰的认识状态机并且理清 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

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

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

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

标签:Enemy,函数,状态,void,状态机,思路,public
From: https://www.cnblogs.com/shadow-fy/p/17316527.html

相关文章

  • 反欺诈(羊毛盾)API 实现用户行为分析的思路分析
    简介反欺诈(羊毛盾)API是一种用于识别和防范各种欺诈行为的技术解决方案。它可集成到各种应用程序和平台中,通过手机号码、手机IP进行异常检测,达到防范恶意注册、虚假评论、虚假交易等欺诈行为的目的。本文主要介绍反欺诈(羊毛盾)API的工作原理、以及在用户行为分析技术、地理位置......
  • 不要再说你不会了——网络性能问题排查思路
    网络性能问题排查思路服务监控系列文章服务监控系列视频网络问题往往是性能排查中最复杂的一个问题,因为网络问题往往涉及的链路比较长,排查起来不仅仅是看本地机器的指标就可以了。本文将展示一个比较系统的排查网络问题的思路。我们往往都是通过类似prometheus,grafana搭建的......
  • 手机号码归属地 API 实现个性化推荐的思路分析
    前言随着移动互联网和智能手机的普及,越来越多的人使用手机上网和购物,移动营销已成为企业获取用户和提升品牌知名度的重要手段。手机号码归属地API作为移动营销的关键工具,具有广阔的应用前景。本文将探讨如何利用手机号码归属地API进行个性化推荐和精准广告投放,希望对大家有......
  • 运维故障排查思路::::::
    一文带你搞懂Linux运维故障排查思路入门小站 入门小站 2023-04-0721:30 发表于湖北收录于合集#Linux755个入门小站分享运维技巧及10k+Stars的开源项目234篇原创内容公众号【Linux250个常用命令速查手册】关注【入门小站】,后台回复「1001」自取......
  • java大数加法的一种思路
    packageorg.example;importjava.util.ArrayList;importjava.util.List;importjava.util.Scanner;classSuperNum{publicList<Integer>numList;/***成员变量的set方法*@paramnumList*/publicvoidsetNumList(List<Inte......
  • Java虚拟机整体思路
    我们日常编程的Java编程是在Java语言规范代码,通过javac前端编译器编译器,产生字节码规范,此时我们应该对字节码文件结构有一个大致的认识,此时我们了解了Java虚拟机内存面局(专业术语叫运行时数据区),类加载器通过加载器将字节码文件加载到内存中(此时应该对类加载的过程有一个大致的了解......
  • 马尔科夫链文本生成(散列表,状态机,马尔科夫链)
    Codingame散列表为主题的练习题中,马尔科夫链文本生成吸引到了我的注意力。它集合了马尔科夫链,状态机和散列表三个方面的学习内容。其中,n-gram马尔科夫链运用到了文本聊天机器人的设计中,还是蛮有启发性的,应该是chatgpt之前的一项经典技术。下面简单讲讲这个编程练习题。目标制作......
  • controller随时取出登录用户信息的思路
    1.新建一个BaseController,里面写好公共方法,这些方法可以从springContextHoder取出当前线程绑定的请求信息,例如request和response,在这些方法里对request进行取出或者分析操作,例如header里的token。2.其他controller则继承BaseController,调用父类的方法,则可以随时取出当前登录用户......
  • 带头节点的单链表的思路及代码实现
    带头节点的单链表的思路及代码实现(JAVA)一、什么是的单链表①标准定义单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象)+指针(指示后继元素存储位置,元素就是存储数据的存储单......
  • 数组模拟环形队列的思路及代码
    JAVA实现数组模拟环形队列的思路及代码前言在对Java实现数组模拟队列零了解的情况下,建议先去阅读《JAVA实现数组模拟单向队列的思路及代码》一文,可以辅助理解本文核心思想。一、环形数组队列实现:让数组达到复用的效果,即:当我们从数组队列中取出了数据,那取出数据后后这个空间可......