在unity中使用状态机编写敌人的AI(纯代码方式)

最终效果:

在unity中使用状态机编写敌人的AI(纯代码方式)_第1张图片

目录

编写思路

基本代码框架

巡逻状态

追击状态

攻击状态

受击状态


编写思路

状态机如下:

在unity中使用状态机编写敌人的AI(纯代码方式)_第2张图片(前排提示:本文是使用纯代码方式来实现状态机,包括各种状态的设定,以及状态间的切换,其他很多教程都不是这样的形式,而是直接使用unity内置的animator controller,然后手动设定不同状态间的衔接条件,然后在代码中控制条件,通过这种方式实现状态机,例如:

在unity中使用状态机编写敌人的AI(纯代码方式)_第3张图片 

而本文的实现方式是使用纯代码层面实现状态机。

代码思路,书写一个通用的状态接口,包括onEnter、onExit、和onUpdate三个函数。然后接下来其他状态只需要根据不同的状态去实现该接口即可。

 

书写一个有限状态机的脚本,其包含了各个状态,用字典来存储,状态机在任何一个时刻只会处于一个状态。在状态机脚本里书写各个状态通用的功能,例如各个状态的切换函数,人物的转向函数。

在每个状态脚本都有onEnter、onExit函数,我们在状态机的脚本中,执行状态切换的时候去调用当前状态的onExit函数,以及新状态的onEnter函数。

在该状态的的update中,我们持续执行对应状态的onUpdate函数。然后当某个状态达到切换条件时,就需要切换状态,切换状态的方式就是让该状态的脚本去调用有限状态机的切换函数。

以Idle状态举例,在Idle状态的update函数中,玩家会停留IdleTime的时间,时间过后将会切换到Patrol状态,此时就调用有限状态机FSM中的切换状态函数。

 基本代码框架

先书写一个状态的接口,接下来的各种状态都要实现该接口。


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


}

在动画控制器中为敌人创建动画控制器:

在unity中使用状态机编写敌人的AI(纯代码方式)_第4张图片

由于动画控制器本身就是状态机,因此不需要给这些动画间设置衔接,用脚本控制切换即可。

(当然你可以在很多地方的教程发现他们手动设置了很多条件,然后在不同动画的切换间设定了条件,这样也是一种状态机的实现方式,就是使用unity内置的animator的那种状态切换来实现的。那样的有限状态机的实现会更为简单易上手,并且各种状态的切换也更为可视化。玩家只需要设定好不同条件的衔接切换条件即可)

而本文是使用代码层面去完整的实现一个有限状态机的方式)

设定巡逻点和追击点:

在unity中使用状态机编写敌人的AI(纯代码方式)_第5张图片

在unity中使用状态机编写敌人的AI(纯代码方式)_第6张图片

接下来书写简易的状态控制机的框架:

包含了各种状态的枚举,TransititionState即状态切换时需要执行的操作以及flipto转向功能,以及所有功能中包含的参数,如巡逻时间,追踪时间。

除此之外,该状态机作为一个总的状态机,其包含所有的状态,此处使用字典来存储。

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 states = new Dictionary();

    public Parameter_ parameter;
    void Start()
    {
        //初始往状态机里新建两个字典
        states.Add(StateType_.Idle, new IdleState_(this));
        states.Add(StateType_.Patrol, new PatrolState_(this));
        TransititionState(StateType_.Idle);

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

接下来以IdleState为例,书写等候的状态函数。

IdleState需要一些信息,比如等待时长,目标点,动画器,这些参数应该放在另外一个脚本中,然后挂接到enemy身上。为了方便,这里把那些参数放在了FSM这个脚本中,于是,使用IdleState这些状态的时候需要用到这些参数,此处就通过将FSM传给这些状态用来实例化。(而且此处的状态切换的函数也是在FSM脚本中实现的,为了方便的使用该函数,

代码如下:

IdleState实现了当等顿时间到了的时候就会自动切换到另外一个状态。

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

此处实现了一个敌人的状态机,如果想要将该状态机的代码给其他敌人使用,可以将其通用的部分保留下来,然后用其他角色的状态机去继承这个状态机即可。

巡逻状态

接下来书写巡逻状态:

整体与上面类似,

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

实现后效果如下:

追击状态

为敌人添加一个子物体,用于充当敌人的眼睛,当进入敌人的视野范围内,将会切换到追击状态:

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

接下来书写追击状态的函数:

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

当玩家进入敌人的攻击范围时,敌人将切换到攻击状态。

攻击状态

进入到攻击范围的状态虽然可以简单的通过与玩家的距离来实现,但是那样需要手动测量较为麻烦,此处使用范围检测的函数实现,需要设定敌人的攻击点,攻击范围半径,在parameter里添加这两个参数。

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

为了直观的看清楚圆的范围,可以使用画图将其画出来:

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

在unity中使用状态机编写敌人的AI(纯代码方式)_第7张图片

然后在追击的update中添加转换到攻击的条件及切换攻击状态:

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

添加反应状态

为了使敌人在巡逻时也能进入攻击玩家的状态,在idleState和patrolState中添加代码:

如果存在目标,并且目标没有超出追击范围以外的话则追击,如果即使看到了目标,超出了追击范围,则也将不再追击。

在unity中使用状态机编写敌人的AI(纯代码方式)_第8张图片

在unity中使用状态机编写敌人的AI(纯代码方式)_第9张图片

对于反应状态其实比较简单,只需要播放动画,当动画播放完成时切换回追击状态即可:

在unity中使用状态机编写敌人的AI(纯代码方式)_第10张图片

攻击状态与此类似

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

这样就实现了站岗、巡逻、反应、追击、攻击的状态切换。

总结一下就是其实逻辑很简单,先实现简单的站岗巡逻逻辑,在站岗巡逻相互切换的逻辑之前添加一个更优先的判定条件,如果站岗或巡逻中发现目标(通过触发器实现)则切换到反应状态,反应状态就只是简单的播放反应动画,播放完后玩家立马进入追击动画,追击动画足够近时(通过球形判定)则进入攻击状态即可。每攻击完一次,自动进入追击状态,如果还处于攻击范围则会立马再次进入攻击状态,如果超出攻击范围则进行追击,超出追击范围则回去巡逻。

在unity中使用状态机编写敌人的AI(纯代码方式)_第11张图片

这样的状态机,如果出现了新的状态只需要注册新的状态,并书写切换条件即可

受击状态

 受击状态比较特别,受击状态的判定在上述的各种状态中优先级更高,因此在上述所有状态的onupdate函数中都应该嵌入若受击则切换到受击状态。

添加一个伤害判定:

在unity中使用状态机编写敌人的AI(纯代码方式)_第12张图片

在所有状态那里添加:

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

在unity中使用状态机编写敌人的AI(纯代码方式)_第13张图片

受击状态和死亡状态:

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()
    {

    }
}

你可能感兴趣的:(unity游戏开发,unity,人工智能,游戏引擎)