用动画状态机当AI状态机

写ai逻辑基本上都需要状态机。因为懒得手搓状态机,所以选择直接用动画状态机当逻辑状态机用。

AI的基本行为

  1. 不停检测视野范围内是否有敌人;
  2. 没发现敌人时,静止、沿设定路线巡逻或随意走动;
  3. 发现敌人后,进入警戒状态,并进入攻击状态,转向敌人,攻击;
  4. 丢失目标后,保持警戒一段时间,然后进入安全状态;
  5. 丢失目标后可以选择是否追击,追击则移动到;
  6. 受攻击后会进入警戒状态;
  7. 警戒状态随意走动,指定一个附近目的地,朝向它,前进,同时不停检测是否已经足够接近,接近了就停止,再指定目的地,循环;
  8. 警戒状态,环顾四周;
  9. 攻击状态朝向目标,开启瞄准约束,开枪一段时间,停火一段时间,然后检查没子弹就换弹,如此循环;

AI程序的特点是在一个检测状态的大循环之下,各状态有攻击、环顾、走动等小循环。

架构设计

因为敌人的根节点已经有一个animator控制动画,只能增加一个子对象AI,给它加一个animator指向逻辑“动画”状态机。还有一个脚本,用来放一些检测函数和动画事件函数。

状态图设计

所有状态都不停执行检测敌人的方法。

动画剪辑设置

动画剪辑添加一个无关紧要的属性(如Scale)来卡时间。重要的是在特定的时间执行动画事件。如在Safe一段时间后开始巡逻:

public void StartPatrolling(){
        enemyController.SetBool(patrolling,true);
    }

 攻击状态开枪几秒,停歇几秒,然后检查是否该换弹:

状态机行为脚本

主要用于在特定状态才每帧执行的代码。

在状态机行为脚本里满足某些条件时执行animator.SetXXX()改变动画参数,动画参数改变又引起状态转换,执行新的状态机行为脚本,可以达到状态机“自驱动”的效果。

public class EnemyAlert : StateMachineBehaviour
{
    Character1 myCharacter;
    MyNPCAI myAI;
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){
        myCharacter=animator.transform.parent.GetComponent<Character1>();
        myCharacter.PutAwayGun();
        myAI=animator.GetComponent<MyNPCAI>();
        myCharacter.PlayRandomClip(myAI.findAudio);
        myCharacter.UseRifle();
        myAI.StopLookingAround();
    }
    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){
        if(myAI.DetectEnemy()){
            animator.SetBool(MyNPCAI.foundEnemyPara,true);
        }
    }
}

但是每个状态机行为脚本进入时都要获取一遍组件。这很恶心。 

慢慢地我发现用协程好像也能替代状态机行为的功能,只需要状态机+动画事件+协程就

状态里的小状态

  1. Alert交替进行环顾四周、随机移动。环顾四周是一个过程,可以指定目标方向或旋转时间,移动也是类似的过程;
  2. 攻击要先转向目标,然后交替进行射击和停火,也是个子状态机;
  3. 其他状态要想更细,都会变成子状态机;

要把状态分成小状态吗?状态会多一点,多出来的转换就更多了。或者可以用协程做小状态?怎么用协程做个小状态机?可以在行为IEnumerator里开启下一个行为的协程,会不会有什么问题?

AI检测敌人

一开始我想让AI能发现前方扇形区域的敌人,没有扇形碰撞体,用代码写了一个。后来为了简单,直接在人物前方用球形范围检测了。

除了范围检测,还要用Physics.LineCast()检测中间有没有障碍物。 

bool DetectEnemy()
        {
            Vector3 center = transform.position + transform.forward * detectRadius;
            Collider[] colliders = Physics.OverlapSphere(center, detectRadius, MyGameManager.Instance.aiDetectLayers);
            for (int i = 0; i < colliders.Length; i++)
            {
                CharacterBase character;
                if (colliders[i].TryGetComponent(out character) && !CheckBarrier(character) && character.HP > 0)
                {
                    if (character.characterSide != myCharacter.characterSide)
                    {
                        target = character;
                        targetLastPosition=character.transform.position;
                        return true;
                    }
                }
            }
            return false;
        }

AI瞄准敌人

给AI的枪绑定对象加了AimConstraint使枪对准敌人,省去了写复杂的瞄准算法。

为了使AI不每枪必中,给AimVector加了随机误差。

void AddAimError(){
        gunAim.aimVector=Vector3.forward+
        UnityEngine.Random.Range(-aimErrorRange,aimErrorRange)*Vector3.up+
        UnityEngine.Random.Range(-aimErrorRange,aimErrorRange)*Vector3.right;
    }

 这容易造成AI身体没指向目标时就打开瞄准约束,造成很不自然的样子。

AI攻击流程

进入攻击状态后转向目标,面向目标后交替开火和停火。攻击过程中要一直朝向敌人,没有朝向敌人不能开火。

public void TurnTo(Vector3 target)
        {
            Vector3 aimVector = target - myCharacter.transform.position;
            aimVector = new Vector3(aimVector.x, 0, aimVector.z);
            if (aimVector == Vector3.zero)
            {
                return;
            }
            Quaternion targetRotation = Quaternion.LookRotation(aimVector);
        myCharacter.transform.rotation=Quaternion.Lerp(myCharacter.transform.rotation,
            targetRotation,.2f);
        }

AI环顾四周

先得到一个随机方向A,然后一帧帧转过去,和方向A的偏差小于一个值后再得到随机方向,循环。

效果演示 

Unity简单敌人逻辑演示:巡逻、发现、攻击、追击_演示

总结

  1. 使用状态机写AI,一开始会想分成安全、警戒、攻击等状态,但是很快会发现这样粗略的状态划分不够,大状态下必然有次级状态规定人物的行为;
  2. 人物的行为不是执行一个函数就完成,都需要持续一段时间,有的持续一段时间结束,有的持续中还要检测结束条件;

不用动画状态机写ai

首先还是状态基类、状态管理器,状态基类有进入、更新、退出。然后状态管理器持有状态的方式我选择直接让管理器持有每种状态作为字段,因为状态机不需要运行中添加移出状态,反而需要一开始就保证所有需要的状态都有了。

然后难搞的还是那几个问题:

  1. 大状态里的小状态,就拿人物攻击分为转向、开枪为例;
  2. 时间控制。以人物开枪0.6秒为例,动画状态机在动画剪辑里很容易控制时间,纯程序要么计时器,要么协程。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐