当前位置: 首页 > article >正文

有限状态机和抽象类多态

学习有限状态机的写法,我们会用一个抽象类继承的方法来写

首先,现在我们已经用过类的继承了,就是在我们敌人和野猪的这个代码当中,

我们打开野猪的代码,它继承了Enemy这个父类,所以可以遗传它父类当中所有的变量和函数方法,我们还可以通过Override进行复写,重载,重新写里面的函数方法。这些都是类继承的好处。

首先考虑抽象类之前,我们先来看看状态机是什么(之前我们有写过一些代码,这个代码挂载在我们的Animator当中,是一个Animator State动画状态机的函数,)我们双击打开其中一个

其实这个就是一个状态机的函数的写法了

首先由这个状态进入的时候,要执行什么,在状态持续不断更新的时候Update要执行什么,以及在这个状态要退出的时候执行什么,这就是一个基本的状态机的写法

我们创建的代码名字,默认继承了StateMachineBehaviour,我们可以点击打开一下,打开一会就会发现,这就是一个抽象类,我们要写的就是这样的方法

class类型前有一个abstract(抽象的意思)

抽象类下面所有的函数方法,都使用的是一个虚方法virtual方法,它可以被重写

抽象类当中的方法执行方法的声明,不写这个函数的实现(抽象类继承)(类比,比萨一定有饼皮,但是面粉可以不同,饼皮的样子可以不同)

接下来,我们进行编写

首先我们要有一个基本的抽象的方法,在Enemy文件夹中创建一个c#文件,命名为BaseState,双击打开

我们把这个代码作为我们的抽象基类,所以它不会继承MonoBehaviour(如果一个代码没有继承MonoBehaviour,我们就没有办法把它挂载在我们的gameObject上。我们要先写上关键词,在class前写上abstract抽象类

public abstract class BaseState 
{
    
}

抽象类里面,我们要写一些基本的函数声明,和动画状态机一样,也要有Update和退出。我们先来写一下,这些函数方法,我们也用abstract来定义(之前用的是virtual,因为它继承了ScriptObject,也是unity当中的一种类的类型,里面也包含了一些,帮我们写好的预制函数的方法等等的一些逻辑),在这里,我们什么都没有继承,我们直接用abstract来修饰这个函数方法

我们要写它最基本的逻辑更新logicUpdate,这个逻辑更新,我们要放在最基类的Enemy当中的Update当中来进行执行,所以所有的布尔值的判断,我们都会放在这个逻辑判断当中

然后我们还有(FixedUpdate当中都执行的是物理判断),所以我们要添加一个PhysicsUpdate物理逻辑判断,要当到FixedUpdate当中去执行

最后写一个退出的方法

这样我们就有了一个最基本的抽象的类,作为它基本的状态,我们要根据这个状态去创建它各种各样的状态

public abstract class BaseState 
{
    public abstract void OnEnter();
    public abstract void LogicUpdate();
    public abstract void PhysicsUpdate();
    public abstract void OnExit();
}

保存代码,返回unity

以野猪为例,首先我们创建一个野猪的巡逻逻辑c#代码,在Enemy文件夹下,创建一个巡逻状态BoarPatrolState

打开代码,我们要用继承的方式来写,这个状态是通过我们抽象类继承而来的,我们先删掉里面基本的方法,让这个代码继承我们的BaseState。

出现红色波浪线,提示我们并没有实现抽象的方法

对于抽象类,我们一定要有所谓的饼皮,目前我们还没有这些东西,要把它添加进来。选中它之后,我们可以通过提示,帮助我们快速的创建所有的抽象的方法

所有在这些函数执行过程当中,要实现的方法,我们就可以写在大括号当中了

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoarPatrolState : BaseState
{
    public override void LogicUpdate()
    {
        throw new System.NotImplementedException();
    }

    public override void OnEnter()
    {
        throw new System.NotImplementedException();
    }

    public override void OnExit()
    {
        throw new System.NotImplementedException();
    }

    public override void PhysicsUpdate()
    {
        throw new System.NotImplementedException();
    }
}

我们回到Enemy代码当中

方便观看,分屏处理

接下来我们要来做这个抽象类的状态,来执行抽象类的状态

我们在最下面来创建这些状态,创建BaseStste类型的状态,无论是什么样的一个状态名字,都可以通过BaseState来调用,起名为patrolState

无论是野猪,蜜蜂,蜗牛的巡逻,我们都把它叫成patrolState

  [Header("状态")]
  public bool isHurt;
  public bool isDead;

  protected BaseState patrolState;

然后我们去重新创建,去new一个新建对象,就可以来使用它了

有了这个类型之后,我们还要新建一个currentState,我们可以通过这样的方式去切换各种各样的状态。

我们再创建一个chaseState追击状态

[Header("状态")]
public bool isHurt;
public bool isDead;

protected BaseState currentState;
protected BaseState patrolState;//巡逻状态
protected BaseState chaseState;//追击状态

我们来快速写一下,我们希望在这个当前的敌人被激活的时候,我们就进入一个最新的基本状态。

我们添加一个周期函数叫做OnEnable,这个物体被激活的时候,我们让当前状态等于我们的巡逻状态,然后我们要在这里执行currentState.OnEbter,所有在这个状态一进入,一开始的代码,我们就在这个位置都给他执行了,无论切换到任何的状态,enter的函数都会在这个位置调用

 private void OnEnable()
 {
     currentState = patrolState;
     currentState.OnEnter();
    
 }

然后在Update这个过程当中,我们就可以在这里面持续不断地执行在当前的状态里面的逻辑LogicUpdate

 private void Update()
 {
     faceDir = new Vector3(-transform.localScale.x, 0, 0);

     if (physicsCheck.touchLeftWall && faceDir.x<0 || physicsCheck.touchRightWall && faceDir.x>0)
     {
         wait = true;
         anim.SetBool("walk",false);
     }

     TimeCounter();
     currentState.LogicUpdate();
 }

在FixedUpdate当中要执行我们的PhysicsUpdate

 private void FixedUpdate()
 {
     if(!isHurt && !isDead)
         Move();

     currentState.PhysicsUpdate();
 }

我们再创建一个OnDisable函数(会在我们人物被关闭,从场景中消失的时候执行一次),把退出写入这个函数当中

 private void OnDisable()
 {
     currentState.OnExit();
 }

这样我们就成功进入了一个状态,游戏一开始就会进入巡逻状态,执行巡逻一开始的函数方法,在Update当中也会持续调用我的逻辑的循环判断,在物理循环也调用,在退出的时候也会被调用一次。

所以我们要做的就是,比如在patrolState巡逻过程当中,如果一旦发现敌人,我们就切换到追击的状态,对应的currentState=chaseState,然后接下来就会调用chaseState当中里面的Enter,PhysicsUpdate,Exit,这就是抽象类的状态机的写法

有限状态机的意思就是一个物体,他在一段时间一定的条件下只执行一个状态,他不会有其他的额外的状态判断。巡逻的话,只有巡逻的状态在执行,不会受到其他的影响;追击的时候就只执行追击状态里面所有的逻辑

这样的话就可以非常好的帮助我们去继续不断的扩展有限状态机,有限状态机无限扩展下去(当前的敌人又很少的状态,后续可能会有很多状态,这些都可以通过状态机来无限扩展下去)我们要做的就是在每个状态中,写好逻辑的判断,符合逻辑就切换到下一个状态

我们先来把巡逻状态用有限状态机的方式来写一下

首先先返回到野猪的Boar代码,把以前写的Move函数删掉,我们不用重写我们的移动,就放在FixedUpdate当中去执行就好了,至于动画,我们会放在我们的状态机里面去执行,

打开状态机的代码,稍微改变一下函数的顺序

如果我们在当前的状态当中,想要调用对应Enemy当中的一些函数方法,目前我们没办法直接调用。

所以在一开始的时候,我们要知道当前的npc是谁,他身上的Enemy的代码是什么,我们要找到当前的Enemy代码,然后我们就可以调用他的这些公开变量和函数

我们再次来修改一下我们的BaseState代码,打开。在一开始我们要创建一下,用protected来修饰一下,可以帮助我们所有继承的子类和访问。创建一个Enemy类型的变量currentEnemy

public abstract class BaseState 
{
    protected Enemy currentEnemy;
    public abstract void OnEnter();
    public abstract void LogicUpdate();
    public abstract void PhysicsUpdate();
    public abstract void OnExit();
}

在OnEnter,状态一进入的时候,我们先获得一下我们当前的Enemy是谁

public abstract class BaseState 
{
    protected Enemy currentEnemy;
    public abstract void OnEnter(Enemy enemy);
    public abstract void LogicUpdate();
    public abstract void PhysicsUpdate();
    public abstract void OnExit();
}

保存一下

点开BoarPatrolState,在这里面抽象就会显示当前出错,我们把它改成对应相同的就可以了

在这里面我们就可以访问currentEnemy,因为在我们的父类当中,我们写了protectedEnemy;所以我们当前的Enemy就等于我们传进来的enemy,

那接下来通过currentEnemy访问Enemy中的内容,(访问hurtForce把它改成其他的数值,等等)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoarPatrolState : BaseState
{   
    public override void OnEnter(Enemy enemy)
    {
        currentEnemy = enemy;
    }
    public override void LogicUpdate()
    {
        throw new System.NotImplementedException();
    }

    public override void PhysicsUpdate()
    {
        throw new System.NotImplementedException();
    }
    public override void OnExit()
    {
        throw new System.NotImplementedException();
    }
}

通过这样的方法,我们类型一旦进入进来的时候,我们就把当前的Enemy传递进去,所以野猪一,野猪二等等都会获得当前的这个物体对应的这个身上挂的代码了,在Enemy的代码的OnEable中,,我们添加一个this传递进去就可以了

private void OnEnable()
{
    currentState = patrolState;
    currentState.OnEnter(this);
   
}

接下来,我们把巡逻的方法在代码中完成

发现敌人就切换到追击状态,(目前我们还没有设计追击和发现敌人的功能,我们后面再来做),我们先把这个正常的巡逻,然后撞墙返回的部分在代码中写好。我们找一下Enemy代码Update当中的这个代码,整个if判断撞墙的代码我们就不要了;因为这部分的内容,我们就可以放到我们当前的逻辑里去判断(因为普通的巡逻是这样的代码,追击的时候就不这样了)

我们将我们的if这段代码放到BasePatrolState代码中,出现了波浪线

当前我们无法访问CurrentEnemy;我们调用currentEnemy.physicsCheck,,注意Enemy代码当中的physicsCheck为私有private,我们把它改为public,这样就可以访问它了

接下来设置一下faceDir使它可以被访问。前面也应该加上currentEnemy

anim也是同样的方法

这样我们就把整个逻辑,成功移植到我们的当前的这个状态当中了

//Enemy

  Rigidbody2D rb;
  public Animator anim;
  public  PhysicsCheck physicsCheck;

//BoarPartolState

public override void LogicUpdate()
 {
     //发动layer切换到chase
     if (currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)
     {
         currentEnemy.wait = true;
         currentEnemy.anim.SetBool("walk", false);
     }
 }

保存代码,返回unity

选中野猪,查看野猪身上的代码

Boar中有非常多的变量,有些变量其实我们并不需要,例如Physics Check这是我们直接在本人身上去进行获得的,Anim也是一样的;我们不希望在这个组件当中看到他,

我们可以到Enemy代码当中,在前面添加一个[HideInInspector],在我们的inspector当中把它隐藏起来,不需要显示出来的都可以隐藏掉

public class Enemy : MonoBehaviour
{
    Rigidbody2D rb;
    [HideInInspector]public Animator anim;
    [HideInInspector] public  PhysicsCheck physicsCheck;

    [Header("基本参数")]
    public float normalSpeed;//普通速度
    public float chaseSpeed;//加速冲
    [HideInInspector] public float currentSpeed;//当前速度
    public Vector3 faceDir;//面朝方向
    public float hurtForce;//受伤带来的冲击力

    public Transform attacker;

    [Header("计时器")]
    public float waitTime;
    public float waitTimeCounter;
    public bool wait;

    [Header("状态")]
    public bool isHurt;
    public bool isDead;

    protected BaseState currentState;
    protected BaseState patrolState;//巡逻状态
    protected BaseState chaseState;//追击状态

保存代码,返回unity

现在那些变量就已经被隐藏起来了

可以用这样的方法清洁一下代码和显示

我们来看一下BoarPatrolState当中的逻辑,在我们的普通的巡逻的模式当中,持续不断的执行判断,撞墙了之后转身;不过在这里面,之前有一个我们忽略的状态,现在我们的野猪虽然可以左右移动,不过如果他一旦一旦到了悬崖的这个位置,他就掉下去了,因为悬崖不会撞墙。

我们不希望他在悬崖掉下去,增加判断野猪是否在地面上,不是在地面或装左墙撞右墙都应该停下来,然后进入计时,同时要停止播放我们移动的这个动画

 public override void LogicUpdate()
 {
     //发动layer切换到chase
     if (!currentEnemy.physicsCheck.isGround|| currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)
     {
         currentEnemy.wait = true;
         currentEnemy.anim.SetBool("walk", false);
     }
 }

下面其他的逻辑我们暂时先不写

保存代码,那么我们这个状态怎么样才能进入进来,一开始这个变量patrolState这个变量我们创建了,但是是空的;我们的patrolState到底执行的是野猪,蜜蜂还是蜗牛

我们在Boar代码当中写一个awake来写(但是awake本身会执行获得我们基本的组件);我们先把Enemy代码当中的Awake的修饰词改一下,改为protected virtual

protected virtual void Awake()
{
    rb = GetComponent<Rigidbody2D>();
    anim = GetComponent<Animator>();
    physicsCheck = GetComponent<PhysicsCheck>();
    currentSpeed = normalSpeed;//初始速度=普通速度
    waitTimeCounter = waitTime;//不需要在Awake中初始化,后续会修改掉
}

然后再Boar当中,我们就可以Override awake,在基本的awake都执行的前提之下,我们要给这些变量进行赋值;当前我们是野猪,我们要new一个野猪的巡逻模式出来,这样我们就成功创建了一个野猪的巡逻逻辑,给到我们的Enemy的基类当中的patrolState;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Boar : Enemy
{
    protected override void Awake()
    {
        base.Awake();
        patrolState = new BoarPatrolState();
    }

这个patrolState现在有了他的赋值之后,就可以执行OnEnable这里面的逻辑了,还有一点要留意的是我们代码是有执行的顺序的,从上往下一次去执行,所以我们的TimeCounter应该放在我们的逻辑判断的下边

 private void Update()
 {
     faceDir = new Vector3(-transform.localScale.x, 0, 0);

     currentState.LogicUpdate(); 
     TimeCounter();
 }

所以我们要持续判断逻辑,一旦进入wait了之后,他的OnEnable--TimeCounter这个部分要进入时间的计时保存

保存代码,返回unity,测试一下

目前我们还原了野猪的巡逻逻辑,我们重画地图,看野猪遇到悬崖的效果

野猪走到坑的位置,动画切换回去了,他本来应该停下来,可是他继续滑动的往前走了

回到代码Enemy当中,在FixedUpdate函数中,有我们的Move函数,Move在没有受伤,没有死亡的时候就会执行,所以wait的时候,他仍然会执行Move,还是朝当前方向继续移动,所以我们在上面要再加上另外一个约束

private void FixedUpdate()
{
    if(!isHurt && !isDead && !wait)
        Move();

    currentState.PhysicsUpdate();
}

保存代码,返回unity,再次测试

这就是整个的巡逻状态

我们在巡逻结束的时候,退出状态,walk=false;这样可以有效配合我们Animator状态机的动画的切换了

在BoarPatrolState

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoarPatrolState : BaseState
{   
    public override void OnEnter(Enemy enemy)
    {
        currentEnemy = enemy;
    }
    public override void LogicUpdate()
    {
        //发动layer切换到chase
        if (!currentEnemy.physicsCheck.isGround ||( currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0))
        {
            currentEnemy.wait = true;
            currentEnemy.anim.SetBool("walk", false);
        }
        else
        {
            currentEnemy.anim.SetBool("walk", true);
        }
    }

    public override void PhysicsUpdate()
    {
        
    }
    public override void OnExit()
    {
        currentEnemy.anim.SetBool("walk", false);
    }
}

目前我们做好了一个状态,还需要另外一个状态追击状态,我们还没有完成发现Player的函数方法

抽象类只做函数声明,不写函数的实现,Enemy基本的父类也改成使用我们的状态机的方式,方便进行扩展状态,只需要添加对应的状态,每一次切换赋值给currentState


http://www.kler.cn/news/358458.html

相关文章:

  • 再论保距变换概念让5000年都无人能识的N外标准自然数一下子浮出水面推翻百年集论
  • WebGL编程指南 - 颜色与纹理
  • 【AWS AMI跨境备份】跨境使用 S3 备份和还原 AMI 镜像
  • 最新版!《末日地带2》十四项修改器 增加健康/增加信心/设置游戏速度
  • Scala中的reduce
  • PROFINET开发EtherNet/IP开发Vline板卡在称重设备行业的应用
  • Python SQL 注入攻击及其防护措施:编写安全的数据库查询
  • 数据结构之链表——单向链表
  • Centos7系统Python3.11.2版本安装
  • 理解ES6中的模块
  • Leetcode刷题. 贪心算法
  • MySQL【知识改变命运】10
  • 408数据结构-查找的基本概念,顺序查找 自学知识点整理
  • 【React】useLayoutEffect、useInsertionEffect
  • 如何将一个前端项目装进 docker image 里
  • 科研绘图系列:R语言散点相关系数图(scatter plot)
  • linux系统中chmod用法详解
  • 贪心算法简记
  • 数据分析和可视化python库orange简单使用方法
  • python 基础笔记 2(函数, 类)