今天粗略的学习了一下状态机的写法,归纳了一下,我觉得状态机就是把原本写在NPC脚本Update函数中一个个零散的方法统一归类到各个状态机中。
要使用状态机,首先我们要知道使用状态机有什么好处
例如我今天做的这个事例,这个NPC野猪会一直向前移动,而我现在要为他添加一个功能,野猪撞到墙和遇到悬崖之后就会转身。
如果我没有使用状态机,那么我的代码要写在Update里面
大概的实现方法是左、右、脚底各有一个判定点,如果判定到了就转身。
这里增加了一个额外的功能就是撞到墙会有一个计时器,两秒之后才会再次转身,防止在狭小的地方不断转身
private void Update()
{
//判定方向
faceDir = new Vector3(-transform.localScale.x, 0, 0);
if ((!physicsCheck.isGround||physicsCheck.touchLeftWall|| physicsCheck.touchRightWall)&&coundTurn)
{
transform.localScale = new Vector3(currentEnemy.faceDir.x, 1, 1);
coundTurn = false;
animator.SetBool("isWalk", true);
}
//计时器方法
TimeCounter();
}
后来又需要为野猪添加一个发狂的状态,大概意思就是发现玩家之后,移动速度加快,而且不再等待2秒转身,遇到悬崖也不会停下来
如果不使用状态机,那么我的代码可能就要这样写
private void Update()
{
//判定方向
faceDir = new Vector3(-transform.localScale.x, 0, 0);
if(普通状态)
{
if ((!physicsCheck.isGround||physicsCheck.touchLeftWall|| physicsCheck.touchRightWall)&&coundTurn)
{
transform.localScale = new Vector3(currentEnemy.faceDir.x, 1, 1);
coundTurn = false;
animator.SetBool("isWalk", true);
}
}
if(发狂状态)
{
//发狂状态逻辑
}
//计时器方法
TimeCounter();
}
假如我还需要更多的状态(虚弱、眩晕),那我这个update函数就会非常的复杂,也不便于后续的修改,我们知道一般最好是添加代码而避免修改代码的。
使用状态机
状态机表示一个状态,进入这个状态要做什么,在这个状态里要做什么,退出这个状态要做什么。这就状态机里面要编写的代码。要使用状态机,我们可以先定义一个状态机基类。
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并继承基类
public class BoarPatrolState : BaseState
{
//进入状态时把当前的怪物传给状态机
public override void OnEnter(Enemy enemy)
{
currentEnemy = enemy;
currentEnemy.currentSpeed = currentEnemy.normalSpeed;
}
public override void LogicUpdate()
{
//发现玩家切换到chase追击
if (currentEnemy.FoundPlayer())
{
currentEnemy.SwitchState(NPCState.Chase);
}
//碰到墙壁回头
if ((!currentEnemy.physicsCheck.isGround||currentEnemy.physicsCheck.touchLeftWall || currentEnemy.physicsCheck.touchRightWall)&& currentEnemy.coundTurn)
{
currentEnemy.transform.localScale = new Vector3(currentEnemy.faceDir.x, 1, 1);
currentEnemy.coundTurn = false;
currentEnemy.animator.SetBool("isWalk", true);
}
if (currentEnemy.rb.velocity==new Vector2(0,0))
{
currentEnemy.animator.SetBool("isWalk", false);
}
}
public override void PhysicsUpdate()
{
}
public override void OnExit()
{
currentEnemy.animator.SetBool("isWalk", false);
}
}
我们要在NPC脚本定义状态机变量,这时候我们的update函数里面就只需要这一行代码即可
//巡逻
protected BaseState patrolState;
//追击
protected BaseState chaseState;
//当前状态
protected BaseState currentState;
private void Update()
{
faceDir = new Vector3(-transform.localScale.x, 0, 0);
//状态机逻辑更新
currentState.LogicUpdate();
TimeCounter();
}
我们也定义了野猪发狂(追击)状态的状态机BoarChaseState
public class BoarChaseState : BaseState
{
public override void OnEnter(Enemy enemy)
{
currentEnemy = enemy;
Debug.Log("切换到Chase");
currentEnemy.currentSpeed = currentEnemy.chaseSpeed;
currentEnemy.animator.SetBool("isRun", true);
}
public override void LogicUpdate()
{
if (currentEnemy.lostTimeCounter<=0)
{
currentEnemy.SwitchState(NPCState.Patrol);
}
if (currentEnemy.physicsCheck.touchLeftWall&¤tEnemy.faceDir.x<0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x>0)
{
currentEnemy.transform.localScale = new Vector3(currentEnemy.faceDir.x, 1, 1);
}
}
public override void PhysicsUpdate()
{
//throw new System.NotImplementedException();
}
public override void OnExit()
{
currentEnemy.animator.SetBool("isRun", false);
}
}
可以看到在两个状态机中都有切换状态的逻辑,切换状态就是调用这个函数
public void SwitchState(NPCState state)
{
//退出当前状态
currentState.OnExit();
//根据状态切换
switch (state)
{
case NPCState.Patrol:
currentState = patrolState;
break;
case NPCState.Chase:
currentState = chaseState;
break;
case NPCState.Skill:
break;
}
//进入新状态
currentState.OnEnter(this);
}
public enum NPCState
{
Patrol,
Chase,
Skill
}
这里的patrolState和chaseState是在NPC具体的脚本里注入的,例如野猪的这两个状态就在野猪的Awake里面注入。如果是其他的新敌人(蜜蜂、蜗牛),就在它们各自的Awake里面实例化
public class Boar : Enemy
{
public override void Move()
{
base.Move();
animator.SetBool("isWalk", true);
}
protected override void Awake()
{
base.Awake();
patrolState = new BoarPatrolState();
chaseState = new BoarChaseState();
}
}
总结
如果我们要给野猪添加新的状态,只需要写一个新的状态机脚本继承状态机基类,并在里面写好相应逻辑,然后到怪物脚本(Boar)定义那个状态的变量,再到SwtichState函数多写一个case即可。