Program enemy AI with state machine in unity

final effect:

content

Writing ideas

basic code framework

patrol status

pursuit state

Attack status

hit state


Writing ideas

The state machine is as follows:

Code ideas, write a general state interface, including onEnter, onExit, and onUpdate three functions. Then other states only need to implement the interface according to different states.

Write a script for a finite state machine, which contains various states, which are stored in a dictionary. The state machine can only be in one state at any time, and the functions common to each state, such as switching and turning of each state.

There are onEnter and onExit functions in each state script. In the state machine script, when executing state switching, we call the onExit function of the current state and the onEnter function of the new state. In the update of the state machine, we continue to execute the onUpdate function of the corresponding state.

Taking the Idle state as an example, in the update function of the Idle state, the player will stay for the time of IdleTime, and will switch to the Patrol state after the time elapses. At this time, the switch state function in the finite state machine FSM is called.

 basic code framework

First write an interface of a state, and the following states must implement the interface.


public interface IState_ 
{
    void OnEnter();
    void OnUpdate();
    void OnExit();


}

Create animation controllers for enemies in animation controllers:

Since the animation controller itself is a state machine, there is no need to set the connection, just use the script to control the switching.

Set patrol and pursuit points:

Next, write the framework of a simple state control machine:

Contains the enumeration of various states, TransititionState, the operation that needs to be performed when the state is switched, and the flipto steering function, as well as the parameters included in all functions, such as patrol time and tracking time.

In addition, the state machine acts as a general state machine, which contains all the states, which are stored here using a dictionary.

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

public enum StateType_
{
    Idle,Patrol,Chase,React,Attack
}

[Serializable]
public class Parameter_
{
    public int health;
    public float moveSpeed;
    public float chaseSpeed;
    public float idleTime;
    public Transform[] patrolPoints;
    public Transform[] chasePoints;
}
public class FSM_ : MonoBehaviour
{
    public Animator animator;

    private IState_ currentState;
    private Dictionary<StateType_, IState_> states = new Dictionary<StateType_, IState_>();

    public Parameter_ parameter;
    void Start()
    {
        states.Add(StateType_.Idle, new IdleState_(this));
        states.Add(StateType_.Patrol, new PatrolState_(this));
        TransititionState(StateType_.Idle);

        animator = GetComponent<Animator>();
    }
    private void Update()
    {
        currentState.OnUpdate();
    }

    public void TransititionState(StateType_ type)
    {
        if (currentState != null)
            currentState.OnExit();
        currentState = states[type];
        currentState.OnEnter();
    }
    public void FlipTo(Transform target)
    {
        if (target != null)
        {
            if (transform.position.x > target.position.x)
            {
                transform.localScale = new Vector3(-1, 1, 1);
            }
            else if (transform.position.x < target.position.x)
            {
                transform.localScale = new Vector3(1, 1, 1);
            }
        }
    }
}

Next, take IdleState as an example to write the waiting state function.

IdleState needs some information, such as waiting time, target point, animator, these parameters should be placed in another script, and then hooked to enemy. For convenience, these parameters are placed in the FSM script. Therefore, these parameters need to be used when using the IdleState states. Here, they are instantiated by passing the FSM to these states. (And the state switching function here is also implemented in the FSM script. In order to use this function conveniently,

code show as below:

IdleState realizes that when the waiting time is up, it will automatically switch to another state.

public class IdleState_ : IState_
{
    private FSM_ manager;
    private Parameter_ parameter;

    private float timer;
    public IdleState_(FSM_ manager)
    {
        this.manager = manager;
        this.parameter = manager.parameter;
    }
    public void OnEnter()
    {
        parameter.animator.Play("Idle");
    }
    public void OnUpdate()
    {
        timer += Time.deltaTime;
        if (timer >= parameter.idleTime)
        {
            manager.TransititionState(StateType_.Patrol);
        }
    }
    public void OnExit()
    {
        timer = 0;
    }
}

An enemy's state machine is implemented here. If you want to use the code of the state machine for other enemies, you can keep the general part, and then use the state machine of other characters to inherit this state machine.

patrol status

Next, write the patrol status:

The whole is similar to the above,

public class PatrolState_ : IState_
{
    private FSM_ manager;
    private Parameter_ parameter;

    private int patrolPosition;//巡逻到第几个点了
    public PatrolState_(FSM_ manager)
    {
        this.manager = manager;
        this.parameter = manager.parameter;
    }
    public void OnEnter()
    {
        parameter.animator.Play("Walk");
    }
    public void OnUpdate()
    {
        manager.FlipTo(parameter.patrolPoints[patrolPosition]);//让敌人始终朝向巡逻点的方向
        manager.transform.position = Vector2.MoveTowards(manager.transform.position, parameter.patrolPoints[patrolPosition].position, parameter.moveSpeed * Time.deltaTime);

        if (Vector2.Distance(manager.transform.position, parameter.patrolPoints[patrolPosition].position)<0.1f) {
            manager.TransititionState(StateType_.Idle);
        }
    }
    public void OnExit()
    {
        patrolPosition++;
        if (patrolPosition >= parameter.patrolPoints.Length)
        {
            patrolPosition = 0;
        }
    }
}

There is a bug here. I don't know why the animator did not successfully use getcomponent to obtain it automatically, but it can only be obtained manually. I don't know why for the time being.

The effect after implementation is as follows:

pursuit state

Add a child object to the enemy to act as the enemy's eyes. When it enters the enemy's field of vision, it will switch to the pursuit state:

    private void OnTriggerEnter2D(Collider2D collision)
    {
        parameter.target = collision.transform;
    }

Next, write the function of the pursuit state:

public class ChaseState_ : IState_
{
    private FSM_ manager;
    private Parameter_ parameter;

    private float timer;
    public ChaseState_(FSM_ manager)
    {
        this.manager = manager;
        this.parameter = manager.parameter;
    }
    public void OnEnter()
    {
        parameter.animator.Play("Walk");
    }
    public void OnUpdate()
    {
        manager.FlipTo(parameter.target);
        if (parameter.target != null)
        {
            manager.transform.position = Vector2.MoveTowards(manager.transform.position, parameter.target.position, parameter.chaseSpeed * Time.deltaTime);
        }
        if(parameter.target==null ||
            manager.transform.position.x < parameter.chasePoints[0].position.x ||
            manager.transform.position.x > parameter.chasePoints[1].position.x)
        {
            manager.TransititionState(StateType_.Patrol);
        }
        if (true)//进入攻击范围,执行攻击)
            
        {
        }


    }
    public void OnExit()
    {
        timer = 0;
    }
}

When the player enters the enemy's attack range, the enemy will switch to the attacking state.

Attack status

Although the state of entering the attack range can be achieved simply by the distance from the player, it is more troublesome to manually measure it. Here, the function of range detection is used to set the enemy's attack point and attack range radius. In the parameter Add these two parameters.

    private void OnTriggerEnter2D(Collider2D collision)
    {
        parameter.target = collision.transform;
    }
    
    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            parameter.target = null;
        }
    }

In order to visually see the range of the circle, you can use paint to draw it:

    private void OnDrawGizmos()
    {
        Gizmos.DrawWireSphere(parameter.attackPoint.position, parameter.attackAreaRadius);
    }

Then add the conditions for switching to attack and switch the attack state in the update of the pursuit:

        if (Physics2D.OverlapCircle(parameter.attackPoint.position,parameter.attackAreaRadius,parameter.targetLayer))//进入攻击范围,执行攻击)
         //注意,此处需要添加层级,以防止敌人检测到其他的物体则也进入攻击   
        {
            manager.TransititionState(StateType_.Attack);
        }

Add reaction status

In order to enable the enemy to enter the state of attacking the player when patrolling, add code to idleState and patrolState:

If there is a target, and the target is not out of the pursuit range, it will pursue it. If it is out of the pursuit range even if it sees the target, it will not pursue.

The reaction state is actually relatively simple. You only need to play the animation, and switch back to the pursuit state when the animation is completed:

Attack status is similar to this

public class AttackState_ : IState_
{
    private FSM_ manager;
    private Parameter_ parameter;

    private AnimatorStateInfo animStateInfo;//动画状态信息
    public AttackState_(FSM_ manager)
    {
        this.manager = manager;
        this.parameter = manager.parameter;
    }
    public void OnEnter()
    {
        parameter.animator.Play("Attack");
    }
    public void OnUpdate()
    {
        animStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);
        if (animStateInfo.normalizedTime >= .95f)//当动画进度接近1的时候,认为动画接近完成了
        {
            manager.TransititionState(StateType_.Chase);
        }
    }
    public void OnExit()
    {
    }
}

In this way, the state switching of standing guard, patrol, reaction, pursuit and attack is realized.

To sum up, the logic is actually very simple. First implement the simple guard patrol logic, add a more priority judgment condition before the logic of the guard patrol mutual switching, and switch to the reaction state if a target is found during the guard or patrol (implemented by triggers). , the reaction state is simply to play the reaction animation. After playing, the player immediately enters the pursuit animation, and when the pursuit animation is close enough (judged by the ball), it can enter the attack state. After each attack, it will automatically enter the pursuit state. If it is still in the attack range, it will immediately enter the attack state again. If it exceeds the attack range, it will pursue the attack, and if it exceeds the pursuit range, it will go back to patrol.

In such a state machine, if a new state appears, you only need to register the new state and write the switching conditions.

hit state

 The attacked state is special. The determination of the attacked state has a higher priority in the above-mentioned various states. Therefore, in the onupdate function of all the above states, it should be embedded to switch to the attacked state if it is attacked.

Add a damage check:

In all states add:

        if (parameter.getHit)
        {
            manager.TransititionState(StateType_.Hit);
        }

Attacked state and death state:

public class HitState_ : IState_
{
    private FSM_ manager;
    private Parameter_ parameter;

    private AnimatorStateInfo animStateInfo;//动画状态信息
    public HitState_(FSM_ manager)
    {
        this.manager = manager;
        this.parameter = manager.parameter;
    }
    public void OnEnter()
    {
        parameter.animator.Play("Hit");
        parameter.health--;
    }
    public void OnUpdate()
    {
        animStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);

        if (parameter.health <= 0)
        {
            manager.TransititionState(StateType_.Death);
        }
        if (animStateInfo.normalizedTime >= .95f)
        {
            parameter.target = GameObject.FindWithTag("Player").transform;

            manager.TransititionState(StateType_.Chase);
        }
    }
    public void OnExit()
    {
        parameter.getHit = false;
    }
}


public class DeathState_ : IState
{
    private FSM manager;
    private Parameter parameter;

    public DeathState_(FSM manager)
    {
        this.manager = manager;
        this.parameter = manager.parameter;
    }
    public void OnEnter()
    {
        parameter.animator.Play("Dead");
    }

    public void OnUpdate()
    {

    }

    public void OnExit()
    {

    }
}

Guess you like

Origin blog.csdn.net/weixin_43757333/article/details/122858387