一种基于状态机的敌人AI系统简单实现

游戏开发中经常涉及怪物、敌人的行为设计,在战斗中为敌人设计合理、丰富的行为逻辑,可以使得敌人的反应更加真实,也更增加玩家对于游戏的沉浸感。而不同的怪物设定、游戏类型、设计需求会产生出各种各样的AI系统,其中解放方案也比较多,大致可以分为有限状态机和行为树两大类(考虑到稳定性和系统性能,基于机器学习、深度学习等更“智能”的实现方式尚未在大型商业游戏实现[1],而其广泛应用我觉得也是未来AI发展的趋势之一),而两者各有利弊,需要根据不同的业务场景选择不同的实现方案。

一般来说,行为树灵活性强,每个行为间的耦合度低;但是每次行为的决策都需要大量的条件判断;而对于状态需求较少的AI,使用状态机更为直观,其行为逻辑也更能满足我们的预期。这里记录一下我实现的一种基于状态机的AI系统,不依赖于任何插件,只用一定量的代码实现一套简单的魂like Boss战的AI行为逻辑,其效果和可扩展性我觉得还是不错的。这篇专栏主要是想记录下这个AI系统的实现过程(免得到时候忘了),同时如果能给你带来一些参考,那就再好不过了~

制作引擎:Unity3d 2022.1

开干!


设计

对战策划

本项目中Boss AI的设定是一个看守区域的使徒,可以使用剑进行近战攻击(包括砍击、三连挥剑、Combo连招)、使用魔法进行远程攻击(包括魔法阵、追踪火球、魔法弹),当玩家距离较远时需要使用寻路算法跑向玩家或者选择远程攻击,当足够远时脱离仇恨,同时为了增加难度,要求身上携带两瓶血瓶,每当自身血量小于一半时,与玩家拉开一定距离并且使用一瓶血瓶;同时为了不让Boss与玩家长时间二人转(保持相对近的距离但是不满足攻击的角度要求而相对静止),需要设定计时器定时跳出近距离。

状态机设计

总的对战设计就是这么多,虽然看着好像有些复杂,但是AI的状态只有四个:闲置(Idle)、追赶(Pursue)、战斗(Combat)、攻击(Attack),而为了丰富AI的行为,所有的AI行为都只是在这四个状态下扩展而来的。基础的框架[2]如下:

基础状态机

这个框架可以应用在很多通用的敌人AI系统中,根据不同的设定作出定制化的调整即可。这里介绍我的实现方式,为了丰富Boss不同的行为,我们需要根据距离进行判断,这里引入距离目标(玩家)远、中、近三种不同的距离状态,为了降低AI判断计算和状态转移所引入的性能消耗,考虑到游戏中玩家与AI的距离大多数时间都在改变,可以在距离状态改变时使AI做出不同的选择即可:

距离状态划分

而显然,距离状态的改变发生在AI的追赶(Pursue)状态下,我们只需要将上述的状态机框架做一点扩展即可:

扩展状态机

搞定了。这里加入了 Heal 状态表示敌人正在喝血瓶治疗,这个状态可以看作是从属于 Pursue状态下的(即追赶的时候进行治疗)。而这里的“满足一定概率”具体是根据进入不同的距离状态来决定的,这里可以看作是一颗小的行为树,每当AI的状态在 Pursue 且距离状态改变时触发一次,根据条件执行树叶节点的行为即可:

距离状态转移触发的行为树

上图只展示了距离状态为0,1时的树的执行情况,其他的状态由于篇幅原因请看后面的代码实现部分。而刚刚设计的远程魔法攻击和近战攻击等这里不再需要细分不同的战斗状态来实现,而是使用攻击招式配表的做法统一交给Attack状态进行判断。配表里包含攻击招式的动画、造成的伤害、允许攻击的距离和角度上下限等,Attack 状态下读取Boss的攻击表,在当前状态所允许的攻击招式里随机选择一种播放即可

攻击招式配置

到此整个AI系统的设计就介绍完了,当然在实现的部分还补充了很多的细节来使得AI的效果更好。

实现

前期准备

AI的动画Animator,在这里基本就是用来存放动画片段的,由AI的状态机控制状态的播放。存放的动画分为近战、远程魔法、受击、后退、治疗等(动画素材网上都可以找到)。

默认的Idle状态为一颗简单的blend tree,用Speed变量控制Boss静止、走路、奔跑这三种动画的播放。

行走blend tree

为Boss物体挂载Nav Mesh Agent组件,它可以方便地让AI进行寻路,只需要将地图在Navigation菜单进行烘培即可,Unity会自动生成可供Agent行走的导航网格。

挂载Nav Mesh Agent组件
生成导航网格(蓝色覆盖的部分,注意将地形物体在inspector勾选static)
招式配置

为了状态机能选择在当前距离和角度下的攻击招式,同时记录招式的伤害、动画片段等信息,这里将每段攻击招式用脚本进行了配置: 

[CreateAssetMenu(menuName = "AI Actions/Attack Action")]
public class Enemy3DAttackAction : Enemy3DAction
{
    public bool isSwordAttack = true; //是否为近战
    public int attackScore = 3;
    public float recoveryTime = 2;

    public float maxAttackAngle = 35;
    public float minAttackAngle = -35;

    public float minDistanceToAttack = 0;
    public float maxDistanceToAttack = 2;

    public float damage = 20;
}

然后就可以在菜单的"AI Actions/Attack Action"目录下创建各种招式配置信息了。

框架脚本

Editor部分准备好了,接下来进行代码编写。为Boss物体创建Enemy3DManager.cs脚本(为了区别于之前写的敌人脚本这里统一给Boss的脚本加了Enemy3D前缀),负责处理Boss的状态转移以及统一储存Boss AI所需要的各种参数。为了简化,这里暂不讨论Boss死亡处理、受伤、显示血条、背刺等行为的处理(或者可能后续会记一记x),专注讨论AI系统实现的部分。代码如下:

public class Enemy3DManager : MonoBehaviour
{
    [Header("Enemy")]
    public bool isInvulerable; //是否处于无敌状态
    public bool isSuperArmor; //是否处于霸体

    public float maxTrackDistance = 12; //脱离仇恨距离
    public float closeDistance = 1.2f;
    public float midDistance = 5;
    public float farDistance = 10;
    public int distanceState = -1; //距离状态,用于决定敌人ai采取哪种动作

    public Transform currentTarget; //储存当前跟踪的目标(这里就是玩家)
    public float currentRecoveryTime = 0;
    public State currentState;
    public float distanceFromTarget;

    public NavMeshAgent navMeshAgent;
    public Animator animator;
    public int navMeshMoveId; //行走blend tree动画参数对应的哈希值

    public bool isPreformingAction = false; //标记当前是否在播放动作
    public int healTime = 2; //治疗次数
    
    void Awake()
    {
        animator = GetComponent<Animator>();
        navMeshAgent = GetComponent<NavMeshAgent>();
        navMeshMoveId = Animator.StringToHash("Speed");
    }

    void Update()
    {
        HandleRecoverTimer();
        HandleInteract();
    }

    void FixedUpdate()
    {
        HandleStateMachine();
    }

    //处理AI状态机的核心方法
    void HandleStateMachine()
    {
        if(currentState != null)
        {
            State nextState = currentState.Tick(this);
            if(nextState != null)
            {
                currentState = state;
            }
        }
    }

    void HandleRecoverTimer()
    {
        if (currentRecoveryTime > 0)
        {
            currentRecoveryTime -= Time.deltaTime;
        }
    }

    void HandleInteract()
    {
        if(currentTarget != null)
        {
            distanceFromTarget = Vector3.Distance(transform.position, currentTarget.position);
        
            //判定距离状态
            if(distanceFromTarget < closeDistance)
            {
                distanceState = 0;
            }
            else if(closeDistance <= distanceFromTarget && distanceFromTarget < midDistance)
            {
                distanceState = 1;
            }
            else if(midDistance <= distanceFromTarget && distanceFromTarget < farDistance)
            {
                distanceState = 2;
            }
            else if(farDistance <= distanceFromTarget && distanceFromTarget < maxTrackDistance)
            {
                distanceState = 3;
            }
            else if(distanceFromTarget >= maxTrackDistance)
            {
                distanceState = 4;
            }
        }

        if (distanceFromTarget >= maxTrackDistance)
        {
            //too far
            if (currentState != null && currentState.GetType() == typeof(Enemy3DPursueState))
            {
                SwitchToNextState((currentState as Enemy3DPursueState).GetIdleState(this));
            }
        }
    }

    
}

这里State是一个状态机的基类,有个抽象方法Tick需要某个继承类实现。Tick是滴答意思,顾名思义就是脚本每次Update需要调用的方法,Enemy3DManager.cs脚本通过每次调用当前AI敌人的状态中的Tick方法,实现状态中的具体操作和返回下一个状态。这个使用脚本代表状态机的状态的做法大大增加了对每个状态设计的灵活度。

public abstract class State : MonoBehaviour
{
    public abstract State Tick(Enemy3DManager enemyManager);
}

创建五个子类继承State类(分别对应Idle、Pursue、Combat、Attack四种主状态,加上Heal状态,后续可以再考虑加入处理Boss死亡的Dead状态),并实现Tick各自的方法。这里拿Pursue状态举例,由状态机可知它可能会向Attack、Heal或Idle状态转移,因此需要声明并获取这三个状态的变量,类的框架如下:

public class Enemy3DPursueState : State
{
    public Enemy3DCombatState combatState;
    public Enemy3DIdleState idleState;
    public Enemy3DBuffState buffState; //Heal状态
    
    public override State Tick(Enemy3DManager enemyManager)
    {
        return this;
    }
}

在Boss物体下创建空物体代表不同的状态,挂载对应的状态脚本

此外,还需要创建一个定时器脚本Enemy3DStateTimer.cs处理过久保持同一个距离时的动作:

public class Enemy3DStateTimer : MonoBehaviour
{
    public Enemy3DAttackState attackState;
    public Enemy3DPursueState pursueState;
    public Enemy3DCombatState combatState;

    private Enemy3DManager enemyManager;
    private float closeTime;

    void Start()
    {
        enemyManager = GetComponentInParent<Enemy3DManager>();
    }

    private void Update()
    {
        BreakLongTimeClose();
    }
    
    void BreakLongTimeClose()
    {
        //若长时间近战无攻击,则跳出近战状态
        if((enemyManager.distanceState == 0 || enemyManager.distanceState == 1) 
            && !enemyManager.isPreformingAction && !enemyManager.animator.GetBool("isDead"))
        {
            closeTime -= Time.deltaTime;
        }
        else
        {
            closeTime = Random.Range(3, 5);
        }

        //倒计时结束
        if(closeTime < 0)
        {
            StartCoroutine(HandleSlideBack());
            closeTime = Random.Range(3, 5);
        }
    }

    public IEnumerator HandleSlideBack()
    {
        //倒计时结束还在近战之内,且没有攻击,跳开
        print("跳开!");
        GetComponentInParent<Enemy3DAnimationEventManager>().DisableDetection();
        enemyManager.animator.CrossFade("SlideBack", 0.01f);
        yield return new WaitForSeconds(0.02f);
        enemyManager.isPreformingAction = true;
        enemyManager.animator.applyRootMotion = true;
        enemyManager.currentState = pursueState;
    }

    public IEnumerator RunningTimer()
    {
        //一直处于跑步而没有发生距离状态转移时触发,用于暂停跑步,强制进行一段魔法攻击
        int delay = Random.Range(5, 8);
        yield return new WaitForSeconds(delay);
        if (pursueState.isRunning && enemyManager.currentState.GetType() == typeof(Enemy3DPursueState))
        {
            enemyManager.currentState = combatState;
        }
    }
}
状态细节

框架搭完了,下面就剩下实现每个状态下的细节了,也就是细化上面创建的状态子类,这一步也是如何让整个AI状态机运作起来的关键。

Idle状态中,需要射线检测玩家的Layer,判断其是否在自己的攻击范围中,如果发现目标,就转移至Pursue,否则无需转移状态。

public class Enemy3DIdleState : State
{
    public LayerMask detectionLayer; //目标所在的Layer
    public Enemy3DPursueState pursueState;

    //射线检测范围,分别为检测半径、角度范围
    public float detectionRadius = 10; 
    public float maxDetectionAngle = 50;
    public float minDetectionAngle = -50;

    public override State Tick(Enemy3DManager enemyManager)
    {
        //进行射线检测
        Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRadius, detectionLayer);
        if (colliders == null)
        {
            enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 0, 0.1f, Time.deltaTime);
            enemyManager.navMeshAgent.enabled = false;
        }
        else
        {
            for (int i = 0; i < colliders.Length; i++)
            {
                if (colliders[i].gameObject.CompareTag("PlayerBox"))
                {
                    Vector3 targetDir = colliders[i].transform.position - transform.position;
                    float viewableAngle = Vector3.Angle(targetDir, transform.forward);

                    if (viewableAngle > minDetectionAngle && viewableAngle < maxDetectionAngle)
                    {
                        enemyManager.currentTarget = colliders[i].transform;

                    }
                }
            }
        }

        //判断是否找到目标,进行状态转移
        if(enemyManager.currentTarget != null && !enemyManager.isPreformingAction)
        {
            return pursueState;
        }
        else { 
            return this; 
        }
    }

}

Pursue状态中,我们只需要处理两件事:让Boss跑到玩家的地方、距离状态转移时触发行为树更新Boss状态。当然这里需要处理很多细节:当Boss距离目标太远时,需要切换成跑步状态,没那么远的时候需要再切换成行走;当Boss进入跑步状态时,需要开启定时器记录Boss是否长时间在追赶等。Pursue状态类细化如下:

public class Enemy3DPursueState : State
{
    public Enemy3DCombatState combatState;
    public Enemy3DIdleState idleState;
    public Enemy3DBuffState buffState;

    public bool isRunning = false; //标志是否在跑步状态
    private bool inNewDisState = false; //是否进入新的距离状态
    private int lastDistate; //储存上次的距离状态
    private Enemy3DStateTimer timer;

    private void Start()
    {
        timer = GetComponentInParent<Enemy3DStateTimer>(); 
    }	

    public override State Tick(Enemy3DManager enemyManager)
    {
        // 如果Boss当前还在播放攻击动画,暂停进行追赶
        if (enemyManager.isPreformingAction)
        {
            enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 0, 0.1f, Time.deltaTime);
            return this;
        }

        if (enemyManager.distanceFromTarget <= enemyManager.navMeshAgent.stoppingDistance || enemyManager.distanceFromTarget >= enemyManager.maxTrackDistance)
        {
            //停止运动
            enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 0, 0.1f, Time.deltaTime);
            enemyManager.navMeshAgent.enabled = false;

            isRunning = false;
        }
        else if (enemyManager.distanceFromTarget > enemyManager.midDistance && enemyManager.distanceFromTarget < enemyManager.maxTrackDistance)
        {
            //进入跑步状态
            if (!isRunning)
            {
                //首次进入跑步状态,开启计时器
                StartCoroutine(timer.RunningTimer());
            }
            enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 2, 0.1f, Time.deltaTime);
            if(!enemyManager.animator.hasRootMotion)
                enemyManager.navMeshAgent.enabled = true;
            else
                enemyManager.navMeshAgent.enabled = false;
            enemyManager.navMeshAgent.SetDestination(enemyManager.currentTarget.transform.position);
            enemyManager.navMeshAgent.speed = 5.5f;
            isRunning = true;
        }
        else if(isRunning && enemyManager.distanceFromTarget >= enemyManager.midDistance / 2)
        {
            enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 2, 0.1f, Time.deltaTime);
            if (!enemyManager.animator.hasRootMotion)
                enemyManager.navMeshAgent.enabled = true;
            else
                enemyManager.navMeshAgent.enabled = false;
            enemyManager.navMeshAgent.SetDestination(enemyManager.currentTarget.transform.position);
            enemyManager.navMeshAgent.speed = 5.5f;
        }
        else if(!isRunning || (isRunning && enemyManager.distanceFromTarget < enemyManager.midDistance / 2))
        {
            //进入走路状态
            enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 1, 0.1f, Time.deltaTime);
            if (!enemyManager.animator.hasRootMotion)
                enemyManager.navMeshAgent.enabled = true;
            else
                enemyManager.navMeshAgent.enabled = false;
            enemyManager.navMeshAgent.SetDestination(enemyManager.currentTarget.transform.position);
            enemyManager.navMeshAgent.speed = 3;
            isRunning = false;
        }

        if(lastDistate != enemyManager.distanceState) 
        { 
            lastDistate = enemyManager.distanceState;
            inNewDisState = true;
        }
        else
        {
            inNewDisState = false;
        }

        if(inNewDisState)
        {
            //切换新的距离状态时触发,判断Boss接下来采取什么动作
            int rd = Random.Range(0, 100); //随机数决定AI下一步的动作
            switch (enemyManager.distanceState)
            {
                case 3: //远距离~脱离仇恨距离
                    return this;
                case 2: //中距离~远距离
                    return HandleFarDisState(rd, enemyManager);
                case 1: //近距离~中距离
                    return HandleMidDisState(rd, enemyManager);
                case 0: //近距离之内
                    return HandleCloseDisState(rd, enemyManager);
            }
        }

        if (enemyManager.distanceFromTarget < enemyManager.closeDistance)
        {
            return combatState;
        }
        return this;
    }

    //远距离时所采取的行为
    private State HandleFarDisState(int rd, Enemy3DManager enemyManager)
    {
        if(enemyManager.enemyCurrentHealth < enemyManager.enemyMaxHealth / 2 && enemyManager.healTime > 0)
        {
            //需要治疗
            if (rd < 30) return this; //继续走
            else if (rd >= 30 && rd < 90) return buffState; //治疗
            else return combatState;
        }
        else
        {
            //不能或不需要治疗
            if (rd < 50) return this; //继续走
            else return combatState;
        }
        
    }

    //中距离时所采取的行为
    private State HandleMidDisState(int rd, Enemy3DManager enemyManager)
    {
        if (enemyManager.enemyCurrentHealth < enemyManager.enemyMaxHealth / 2 && enemyManager.healTime > 0)
        {
            //需要治疗
            if (rd < 30) return this; //继续走
            else if (rd >= 30 && rd < 90) return buffState; //治疗
            else return combatState;
        }
        else
        {
            //不能或不需要治疗
            if (rd < 60) return this; //继续走
            else return combatState;
        }
    }

    //近距离时所采取的行为
    private State HandleCloseDisState(int rd, Enemy3DManager enemyManager)
    {
        if (enemyManager.enemyCurrentHealth < enemyManager.enemyMaxHealth / 2 && enemyManager.healTime > 0)
        {
            //需要治疗
            if (rd < 25) return combatState; //继续走
            else return buffState; //退后+治疗
        }
        else
        {
            return combatState;
        }
    }
   

}

这下当Boss的血量和距离在不一样的状态时,就会有不一样的行为发生了。

跳开回血

Combat状态就比较简单了,只需要判断是不是可以进行下一次攻击(通过currentRecoveryTime计时到0),进行状态转移即可。这里优化了朝向效果,关闭了Boss的导航Agent,让Boss使用Slerp插值转向玩家。

public class Enemy3DCombatState : State
{
    public Enemy3DAttackState attackState;
    public Enemy3DPursueState pursueState;
    public override State Tick(Enemy3DManager enemyManager)
    {
        enemyManager.navMeshAgent.enabled = false;
        // 检查攻击范围,如果在攻击范围内,返回攻击状态
        Vector3 targetDir = enemyManager.currentTarget.transform.position 
            - enemyManager.transform.position;
        targetDir.y = 0;
        enemyManager.transform.rotation = 
            Quaternion.Slerp(enemyManager.transform.rotation, 
                             Quaternion.LookRotation(targetDir), Time.deltaTime/0.1f);

        if (enemyManager.isPreformingAction)
        {
            enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 0, 0.1f, Time.deltaTime);
        }

        if (enemyManager.currentRecoveryTime <= 0 && !enemyManager.currentTarget.GetComponent<StarryState>().isDead)
        {
            return attackState;
        }
        else if(enemyManager.distanceFromTarget > enemyManager.closeDistance)
        {
            return pursueState;
        }
        else
        {
            return this;
        }
    }
}

Attack状态,需要在招式列表里根据当前状态随机选择一个可行的攻击招式进行播放。这里只和Pursue 状态发生转移,当选择了一个可行的招式后,播放招式,更新冷却时间计时currentRecoveryTime,转移至pursue即可。

public class Enemy3DAttackState : State
{
    public Enemy3DAttackAction[] enemyAttacks; //存储Boss的所有攻击招式
    public Enemy3DAttackAction currentAttack; //
    public Enemy3DCombatState combatState;
    public Enemy3DPursueState pursueState;
    
    private float damage; //攻击伤害

    public override State Tick(Enemy3DManager enemyManager)
    {
        // 从攻击列表中选择攻击动画,如果角度或者距离不对,则选择一个新的,
        // 如果可以,停止移动并攻击玩家,然后设置攻击恢复计时器,返回战斗状态
        if (enemyManager.isPreformingAction) //如果当前还在播放攻击,直接返回Pursue
            return pursueState;

        if(currentAttack != null)
        {
            //选择了招式后,播放招式,更新冷却时间计时currentRecoveryTime,转移至pursue
            if(enemyManager.distanceFromTarget < currentAttack.maxDistanceToAttack)
            {
                enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 0, 0.1f, Time.deltaTime);
                enemyManager.isPreformingAction = true;
                enemyManager.currentRecoveryTime = currentAttack.recoveryTime;
                enemyManager.animator.CrossFade(currentAttack.actionAnimation, 0.2f); //过渡到相应的动画片段
                enemyManager.currentAttack = currentAttack;
                currentAttack = null;
                enemyManager.animator.applyRootMotion = true;
                return pursueState;
            }
        }
        else
        {
            GetNewAttack(enemyManager, enemyManager.distanceFromTarget);
        }

        return pursueState;
    }

    //在招式列表里根据当前状态随机选择一个可行的攻击招式
    private void GetNewAttack(Enemy3DManager enemyManager, float distanceFromTarget)
    {
        Vector3 targetDir = enemyManager.currentTarget.transform.position - transform.position;
        float viewableAngle = Vector3.Angle(targetDir, enemyManager.transform.forward);

        //储存可行招式的列表
        List<Enemy3DAttackAction> canAction = new List<Enemy3DAttackAction>();
        for (int i = 0; i < enemyAttacks.Length; i++)
        {
            Enemy3DAttackAction action = enemyAttacks[i];
        	//读取该招式配置,判断当前状态(距离、角度)是否满足打出该招式的要求,
            //满足则加入可行招式列表
            if (distanceFromTarget <= action.maxDistanceToAttack && distanceFromTarget >= action.minDistanceToAttack &&
                viewableAngle <= action.maxAttackAngle && viewableAngle >= action.minAttackAngle)
            {
                canAction.Add(action);
            }
        }

        //随机选择一个可行招式
        int randomValue = Random.Range(0, canAction.Count);
        if (currentAttack != null || canAction.Count == 0)
        {
            return;
        }
        currentAttack = canAction[randomValue];
        damage = canAction[randomValue].damage;

    }
}

最后来看下Heal状态,这是一个从属于Pursue的子状态,转移到这个状态只用专心做一件事:拉开距离,然后喝药

public class Enemy3DBuffState : State
{
    public Enemy3DCombatState combatState;
    public Enemy3DPursueState pursueState;

    private bool hasHeal = false; //当前是否已经进行治疗了
    private bool backToPursue = false; //治疗完成,通知状态机可以返回主状态Pursue了

    public override State Tick(Enemy3DManager enemyManager)
    {
        if (!hasHeal)
        {
            hasHeal = true;
            StartCoroutine(PlayHealAction(enemyManager));
        }

        if(backToPursue)
        {
            hasHeal = false;
            return pursueState;
        }
        else return this;
    }

    IEnumerator PlayHealAction(Enemy3DManager enemyManager)
    {
        backToPursue = false;
        enemyManager.animator.SetFloat(enemyManager.navMeshMoveId, 0, 0.1f, Time.deltaTime);
        enemyManager.animator.applyRootMotion = true;
        
        if(enemyManager.distanceState == 0 || enemyManager.distanceState == 1)
        {
            //处于近战时,先后退,再进行治疗
            enemyManager.animator.CrossFade("SlideBack", 0.2f);
            yield return new WaitForSeconds(1f);
        }
        enemyManager.animator.CrossFade("Heal", 0.2f);
        enemyManager.isInvulerable = true; //治疗时开启霸体状态
        enemyManager.healTime--;
        yield return new WaitForSeconds(2f);
        backToPursue = true; //告知可以转移至Pursue,这里也可以通过事件Callback来做
    }
}
补充行为

写完上述的状态机后,Boss的AI基本也就完成了。不过为了丰富Boss的状态机的完成度,其实在完成上述的状态机后还需要做一些补充处理。我这边的处理时还加入转向动画让Boss寻路时的动画更自然一些:

//转身动画
public void HandleTurnAnim(Enemy3DManager enemyManager)
{
    float angle = Vector3.Angle(enemyManager.transform.forward, enemyManager.currentTarget.position - enemyManager.transform.position);
    Vector3 cross = Vector3.Cross(enemyManager.transform.forward, enemyManager.currentTarget.position - enemyManager.transform.position);
    if (angle < 90)
    {
        enemyManager.transform.rotation = Quaternion.Slerp(enemyManager.transform.rotation, 
            enemyManager.navMeshAgent.transform.rotation, 50 / Time.deltaTime);
    }
    else if(angle >= 90 && cross.y > 0)
    {
        enemyManager.animator.applyRootMotion = true;
        enemyManager.animator.SetBool("TurnR", true);
        print("向右转");
    }
    else if(angle >= 90 && cross.y < 0)
    {
        enemyManager.animator.applyRootMotion = true;
        enemyManager.animator.SetBool("TurnL", true);
        print("向左转");
    }
}

同时Boss死亡后,之间让他的状态转移到一个空状态Dead即可:

public class Enemy3DDeadState : State
{
    //Boss死亡后的状态转移到此
    public override State Tick(Enemy3DManager enemyManager)
    {
        return this;
    }
}

当然,一个完整的Boss除了AI系统,还需要其他很多的组件才能让他更像个Boss。除了刚刚提到的Boss死亡处理、受伤、血条、背刺,还有音效、特效、攻击检测、动画事件等等,如果是魂like的Boss,还需包括Boss房雾门处理、二阶段处理、死亡后处理、过场CG等。所有这些都需要根据不同的Boss设定进行对应的处理,才能给玩家以一场畅快淋漓的战斗。

小结

到此状态机实现AI系统的记录总算完成了,其实用到的所谓技术本质上无外乎是对一些数据进行if else的判断和处理而已,但是如何组建和实现这种数据处理的过程,不正是编程的意义所在吗,这也是我觉得有意义记录下来的地方。具体的效果可以移步至视频【25届】Unity个人游戏作品《StarryGo》演示_单机游戏热门视频。这里截几个片段看看效果:

距离状态转移时选择合适的招式攻击玩家

处于近战二人转时跳开循环

长时间处于跑步追赶的状态先发魔法攻击

你好,为什么不用Behavior Designer设计行为树来做AI呢?

别问,问就是做的时候还不知道有这个插件,写完才知道的(悲)

参考资料:

[1] 【唐老狮直播】Unity游戏开发直播回顾(2023年6月27日 )_哔哩哔哩_bilibili

[2] 【Unity教程】从0编程制作黑魂: 黑暗之魂DarkSouls复刻经典教程 ARPG DARK SOULS in Unity3d_哔哩哔哩_bilibili

  • 31
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值