学习有限状态机的写法,我们会用一个抽象类继承的方法来写
首先,现在我们已经用过类的继承了,就是在我们敌人和野猪的这个代码当中,
我们打开野猪的代码,它继承了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