Unity有限状态机实现怪物AI(代码框架思路)

目录

状态的枚举

状态基类

接口(规范不同对象的同一行为)

 状态机类(作为媒介用于管理各个状态之间的转换)

附带一个攻击状态的子类脚本作为示例:


状态的枚举

首先最容易想到的是状态的枚举,比如说攻击状态、巡逻状态、追击状态等等,用枚举进行表示

public enum E_AI_State 
{
    /// <summary>
    /// 睡眠状态
    /// </summary>
    Sleep,
    /// <summary>
    /// 巡逻状态
    /// </summary>
    Patrol,
    /// <summary>
    /// 聊天状态
    /// </summary>
    Chat,
    /// <summary>
    /// 逃跑状态
    /// </summary>
    Run,
    /// <summary>
    /// 追逐玩家状态
    /// </summary>
    Chase,
    /// <summary>
    /// 攻击玩家的状态
    /// </summary>
    Atk,
    /// <summary>
    /// 警觉状态
    /// </summary>
    Alertness,
}

状态基类

再就是所有怪物 对应的状态都会有一个 脚本去控制该状态下该执行什么,而这些状态肯定会有一部分的逻辑是相同的,所以可以提取出一个抽象类,即状态基类

public abstract class BaseState
{
    //有限状态机实现的AI中的 这些 状态类 它的本质 对于我们来说 是在做什么?
    //逻辑处理(不仅仅是做AI,不管你用代码做什么样的事情 都是在进行逻辑处理)

    //AI状态的切换 
    //切换这个词 就意味着
    // 状态1 ——> 状态2

    //在这个基类中 可以去实现所有状态共有的 进入、离开、处于状态的行为(函数、方法)
    //但是这些方法中 由于是基类,没有明确是哪种状态,也就意味着方法中不会写内容
    //那么 不能写内容的函数 你能联想到什么?
    //1.如果是接口 ,那么直接声明
    //2.如果是类,那么可以考虑抽象方法——一定是抽象类

    //管理自己的有限状态机对象
    protected StateMachine stateMachine;


    /// <summary>
    /// 初始化状态类时  将管理者传入 进行记录
    /// </summary>
    /// <param name="machine"></param>
    public BaseState(StateMachine machine)
    {
        stateMachine = machine;
    }

    //当前状态的类型
    public virtual E_AI_State AIState
    {
        get;
    }


    // 1.离开状态时 做什么
    public abstract void QuitState();

    // 2.进入状态时 做什么
    public abstract void EnterState();

    // 3.处于状态时 做什么(核心逻辑处理)
    public abstract void UpdateState();
}

接口(规范不同对象的同一行为)

public interface IAIObj 
{
    //所有AI对象都应该可以获取到它的Transform信息
    public Transform objTransform
    {
        get;
    }

    //所有AI对象都应该有一个当前的位置
    public Vector3 nowPos
    {
        get;
    }

    //AI对象的目标对象所在的位置
    public Vector3 targetObjPos
    {
        get;
    }

    //所有AI对象都应该有一个攻击范围的概念
    //好用于判断 什么时候开始攻击玩家
    public float atkRange
    {
        get;
    }

    //出生位置 需要继承它的AI对象提供
    public Vector3 bornPos {
        get;
        set;
    }

    //AI对象中 应该有 移动相关的方法
    public void Move(Vector3 dirOrPos);
    //AI对象中 应该有 停止移动相关的方法
    public void StopMove();
    //AI对象中 应该有 攻击相关的方法
    public void Atk();
    //AI对象中 可能想要单独 切换指定动作
    //切换动作 应该传递一些相关参数 才能够指定切换哪个动作吧
    public void ChangeAction(E_Action action);


    //我们应该根据AI不同的状态 去提取出他们的行为合集
}

 状态机类(作为媒介用于管理各个状态之间的转换)

public class StateMachine
{
    //他要管理AI的所有状态
    //所以我们通过一个容器去存储这些状态
    //这些状态会随时的取出来进行切换 因此我们要选用一个方便查找获取的容器存储
    //key —— 状态类型(是有限的状态类型,那么就可以是一开始定死的,
    //                 即使以后策划天马行空 有了新状态需求 ,我们改代码即可,因为我们有了热更新技术 所以也没有太大的影响)
    //value —— 代表的是处理状态的逻辑对象
    private Dictionary<E_AI_State, BaseState> stateDic = new Dictionary<E_AI_State, BaseState>();

    //表示当前有限状态 处于的状态(也就是对应的怪物或玩家当前处于的AI状态)
    private BaseState nowState;

    //这个就是ai有限状态机 管理的 ai对象 会去通过ai状态命令该对象 执行对应的行为
    public IAIObj aiObj;

    //回归的判断临界距离 现在我们写死 以后可能是从AI表中读取
    public float backDis = 15;

    //我们的有限状态机制作的AI 里面有很多的AI状态
    //那么这些AI状态逻辑当中,最终要去针对什么处理对应的状态逻辑
    //处理的其实是 游戏当中需要AI的对象 比如 怪物、玩家、宠物、NPC等等

    //虽然这些对象都是不一样的对象 但是 他们理论上来说需要具备共同的行为
    //这样在处理AI逻辑时 才更方便进行一些行为的调用

    //我们其实可以尝试 在AI模块把这些内容提取出来 作为接口 让这些需要AI的对象 必须要实现这个接口 才行

    /// <summary>
    /// 初始化有限状态机类 
    /// </summary>
    /// <param name="aiObj">传入 ai对象 用于之后的行为控制</param>
    public void Init(IAIObj aiObj)
    {
        this.aiObj = aiObj;
    }

    /// <summary>
    /// 添加AI状态
    /// </summary>
    public void AddState(E_AI_State state)
    {
        switch (state)
        {
            case E_AI_State.Patrol:
                stateDic.Add(state, new PatrolState(this));
                break;
            case E_AI_State.Run:
                stateDic.Add(state, new RunState(this));
                break;
            case E_AI_State.Chase:
                stateDic.Add(state, new ChaseState(this));
                break;
            case E_AI_State.Atk:
                stateDic.Add(state, new AtkState(this));
                break;
        }
    }

    /// <summary>
    /// 改变状态
    /// </summary>
    public void ChangeState(E_AI_State state)
    {
        //如果当前处于另一个状态 就退出该状态
        if (nowState != null)
            nowState.QuitState();

        //如果存在该状态的逻辑出来对象 那么就进入该状态
        if(stateDic.ContainsKey(state))
        {
            nowState = stateDic[state];
            nowState.EnterState();
        }
    }

    /// <summary>
    /// 更新当前状态逻辑处理
    /// </summary>
    public void UpdateState()
    {
        if (nowState != null)
            nowState.UpdateState();
    }

    /// <summary>
    /// 检测是否切换到回归状态
    /// </summary>
    public void CheckChangeRun()
    {
        //在追逐过程中 发现超出了 我们的最大距离 就应该切换到回归的状态
        //目前我们处理的是利用ai对象和自己的出生点距离 进行最大距离判断
        //达到的效果是 ai对象一定要跑到边界 才甘心

        //其实还可以利用 目标对象和自己的出生点距离 + 自己攻击距离 来进行距离判断
        //达到的效果就是 目标达不到了 就没有必要追了
        if (Vector3.Distance(this.aiObj.nowPos, this.aiObj.bornPos) >= this.backDis)
        {
            ChangeState(E_AI_State.Run);
        }
    }
}

在状态机类中的AddState方法中,向字典中添加了对应的状态(把自己这个状态机类传进去供初始化),也就是说,当我在另一个脚本中调用状态机类的中的AddState,他就会在BaseState中自动关联上了状态机类 这个脚本,参考代码如下:

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

public class Monster : MonoBehaviour, IAIObj
{
    public GameObject bullet;

    //网格寻路组件
    private NavMeshAgent navMeshAgent;

    //在需要使用AI模块的对象当中 声明一个 AI状态机对象 用于开启AI功能
    private StateMachine aiStateMachine;

    private Vector3 nowObjPos;
    //对象当前的位置
    public Vector3 nowPos {
        get
        {
            nowObjPos = this.transform.position;
            //为了和我们AI模块的定位规则相同 没有考虑 Y上的位置 主要是在xz平面进行位移
            nowObjPos.y = 0;
            return nowObjPos;
        }
    }

    //出生位置
    public Vector3 bornPos
    {
        get;
        set;
    }

    //AI对象必须能够被AI模块获取到Transform 方便我们进行相关处理
    public Transform objTransform => this.transform;

    //自己的攻击范围(目前我们可以写死,以后 一般是通过配置表进行数据初始化
    //如果还有其他规则,自己实现对应的攻击范围规则即可)
    public float atkRange => 2;

    //用于测试用的目标对象
    //正常情况下,应该通过代码动态的再场景中寻找满足条件的目标 我们这里仅仅是测试
    //所以直接通过拖拽进行关联
    public Transform targetTransform;

    //由于我们现在还不用去考虑 目标 所以随便给一个目标位置
    public Vector3 targetObjPos
    {
        get
        {
            //注意:这里减去y方向的0.5 是因为我们用立方体举例子,它的y往上升了0.5
            //为了贴合地面 所以我们减去0.5
            return targetTransform.position - Vector3.up * 0.5f;
        }
    }

    private void Start()
    {
        navMeshAgent = this.GetComponent<NavMeshAgent>();

        //之所以把AI的重要初始化 放到对象类当中 主要原因
        //是因为不同对象 可能会存在不同的AI状态,不同的起始状态
        //这些往往在游戏中 都是配置表当中配置的 所以一般写在怪物创建处

        //注意:
        //大多数情况下 会放在 怪物管理器中的创建怪物的方法中,但是我们目前没有设计怪物管理器
        //因此,我们把这一块代码 放在了 怪物出生的生命周期函数中 也就是Start中(也可以放在Awake)

        //初始化AI模块的有限状态机对象
        aiStateMachine = new StateMachine();
        //把ai对象自己 传入其中进行初始化
        aiStateMachine.Init(this);

        //你需要什么AI状态 就动态添加(以后一般情况下 是通过配置表的配置去添加)
        //为AI添加巡逻状态
        aiStateMachine.AddState(E_AI_State.Patrol);
        aiStateMachine.AddState(E_AI_State.Chase);
        aiStateMachine.AddState(E_AI_State.Atk);
        aiStateMachine.AddState(E_AI_State.Run);

        //初始化完所有AI状态后 那就需要一个当前的AI状态
        //目前一开始就让对象时一个巡逻状态
        aiStateMachine.ChangeState(E_AI_State.Patrol);
          
        //出生位置 就是对象一开始所在的位置
        bornPos = this.transform.position;
    }

    private void Update()
    {
        //ai相关的更新 是由 ai对象的 帧更新函数 发起的 
        aiStateMachine.UpdateState();
    }


    public void Atk()
    {
        //暂时不写 之后写到攻击AI时 再去写它
        print("攻击");

        //动态创建自动 发射即可
        GameObject obj = Instantiate(bullet, this.transform.position + this.transform.forward + Vector3.up * 0.5f, this.transform.rotation);
        Destroy(obj, 5f);
    }

    public void ChangeAction(E_Action action)
    {
        print(action);
    }

    public void Move(Vector3 dirOrPos)
    {
        //结束停止移动
        navMeshAgent.isStopped = false;
        navMeshAgent.SetDestination(dirOrPos);
    }

    public void StopMove()
    {
        //该方法过时了
        //navMeshAgent.Stop();
        //停止移动
        navMeshAgent.isStopped = true;
    }
}

附带一个攻击状态的子类脚本作为示例:

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

public class AtkState : BaseState
{
    public override E_AI_State AIState => E_AI_State.Atk;

    //下一次攻击的时间
    private float nextAtkTime;

    //下次攻击等待的时间
    private float waitTime = 2f;

    public AtkState(StateMachine machine):base(machine)
    {

    }

    public override void EnterState()
    {
        Debug.Log("进入攻击状态了");
        //进入攻击状态时 认为此时此刻就要攻击
        nextAtkTime = Time.time;
    }

    public override void QuitState()
    {
        
    }

    public override void UpdateState()
    {
        //进入AI状态后 不停的让ai对象去攻击即可
        if (Time.time >= nextAtkTime)
        {
            stateMachine.aiObj.Atk();
            nextAtkTime = Time.time + waitTime;
        }

        //如果目标和我的距离过远了,我们应该去切换到追逐状态 ,追到了再继续打它
        if (Vector3.Distance(stateMachine.aiObj.nowPos, stateMachine.aiObj.targetObjPos) > stateMachine.aiObj.atkRange)
        {
            stateMachine.ChangeState(E_AI_State.Chase);
        }

        //我们可以利用向量和四元数相关知识 让ai对象看向目标对象 也可以简单粗暴的用LookAt
        //我们在这里只是举例子 就使用LookAt来节约一些事件 之后 大家可以根据自己的需求去进行制作
        stateMachine.aiObj.objTransform.LookAt(stateMachine.aiObj.targetObjPos + Vector3.up * 0.5f);


        //在追逐过程中 发现超出了 我们的最大距离 就应该切换到回归的状态
        stateMachine.CheckChangeRun();
    }
}

Unity动画状态机是一种用于控制游戏对象动画行为的工具。它基于状态机的概念,通过定义不同的状态和状态之间的转换来实现动画的播放和切换。 在Unity中,可以通过以下步骤来实现动画状态机: 1. 创建动画状态机:在Unity编辑器中,可以创建一个Animator Controller(动画控制器)作为动画状态机的容器。可以通过右键点击Assets面板,选择Create -> Animator Controller来创建。 2. 添加动画状态:在Animator Controller中,可以添加多个动画状态。每个动画状态代表一个特定的动画片段或动画行为。可以通过拖拽动画片段或者创建新的动画状态来添加。 3. 设置状态之间的转换:在Animator Controller中,可以设置不同状态之间的转换条件。转换条件可以是触发器、布尔值、整数等。当满足转换条件时,Animator会自动切换到下一个状态。 4. 添加过渡动画:在状态之间的转换过程中,可以添加过渡动画来实现平滑的过渡效果。可以设置过渡动画的淡入淡出时间、过渡曲线等参数。 5. 控制动画播放:通过代码或者其他方式,可以控制Animator组件的参数来触发状态之间的转换和动画的播放。例如,可以使用Animator.SetTrigger()方法来触发转换条件。 总结一下,Unity动画状态机通过定义不同的状态和状态之间的转换来实现动画的播放和切换。它是一种强大的工具,可以帮助开发者实现复杂的动画逻辑和交互效果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值