Unity's simple NPC state machine

Today I roughly learned how to write a state machine, and after summarizing it, I think the state machine is to unify the scattered methods originally written in the NPC script Update function into each state machine.

To use a state machine, first we need to know what are the benefits of using a state machine

For example, in the example I did today, the NPC wild boar will keep moving forward, and now I want to add a function for him. The wild boar will turn around after hitting a wall or encountering a cliff.

If I don't use a state machine, then my code should be written in Update

The approximate implementation method is to have a judgment point on the left, right, and sole of the foot. If the judgment is reached, turn around.

An additional feature added here is that there will be a timer when hitting a wall, and it will turn around again after two seconds to prevent continuous turning in a small place.

    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();
    }

Later, we needed to add a crazy state to the wild boar, which roughly means that after discovering the player, it will move faster, no longer wait 2 seconds to turn around, and will not stop when encountering a cliff.

If I don't use a state machine, then my code may be written like this

​
    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();
    }

​

If I need more states (weakness, dizziness), then my update function will be very complicated and inconvenient for subsequent modifications. We know that it is generally best to add code rather than modify it.

Use state machine

A state machine represents a state, what to do to enter this state, what to do in this state, and what to do to exit this state. This is the code to be written in the state machine. To use a state machine, we can first define a state machine base class.

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();
}

For example, in the normal state above, we define a state machine BoarPatrolState for the boar's normal state and inherit the base class.

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);
    }

}

We need to define state machine variables in the NPC script. At this time, we only need this line of code in our update function.

    //巡逻
    protected BaseState patrolState;
    //追击
    protected BaseState chaseState;
    //当前状态
    protected BaseState currentState;
    private void Update()
    {
        faceDir = new Vector3(-transform.localScale.x, 0, 0);

        //状态机逻辑更新
        currentState.LogicUpdate();

        TimeCounter();
    }

We also define the state machine BoarChaseState for the wild boar's mad (chasing) state.

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);
    }

}

You can see that there is logic for switching states in both state machines. To switch states is to call this function.

    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
}

The patrolState and chaseState here are injected in the specific NPC script. For example, these two states of the wild boar are injected in the wild boar's Awake. If it is other new enemies (bees, snails), instantiate them in their respective Awakes.

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();
    }
}

Summarize

If we want to add a new state to the wild boar, we only need to write a new state machine script to inherit the state machine base class and write the corresponding logic in it, then go to the monster script (Boar) to define the variables of that state, and then go to the SwtichState function Just write one more case.

Guess you like

Origin blog.csdn.net/holens01/article/details/131832388