Unity之简单的NPC状态机

今天粗略的学习了一下状态机的写法,归纳了一下,我觉得状态机就是把原本写在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&&currentEnemy.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即可。

猜你喜欢

转载自blog.csdn.net/holens01/article/details/131832388