用动画状态机当AI状态机
写ai逻辑基本上都需要状态机。因为懒得手搓状态机,所以选择直接用动画状态机当逻辑状态机用。
AI的基本行为
- 不停检测视野范围内是否有敌人;
- 没发现敌人时,静止、沿设定路线巡逻或随意走动;
- 发现敌人后,进入警戒状态,并进入攻击状态,转向敌人,攻击;
- 丢失目标后,保持警戒一段时间,然后进入安全状态;
- 丢失目标后可以选择是否追击,追击则移动到;
- 受攻击后会进入警戒状态;
- 警戒状态随意走动,指定一个附近目的地,朝向它,前进,同时不停检测是否已经足够接近,接近了就停止,再指定目的地,循环;
- 警戒状态,环顾四周;
- 攻击状态朝向目标,开启瞄准约束,开枪一段时间,停火一段时间,然后检查没子弹就换弹,如此循环;
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);
}
}
}
但是每个状态机行为脚本进入时都要获取一遍组件。这很恶心。
慢慢地我发现用协程好像也能替代状态机行为的功能,只需要状态机+动画事件+协程就
状态里的小状态
- Alert交替进行环顾四周、随机移动。环顾四周是一个过程,可以指定目标方向或旋转时间,移动也是类似的过程;
- 攻击要先转向目标,然后交替进行射击和停火,也是个子状态机;
- 其他状态要想更细,都会变成子状态机;
要把状态分成小状态吗?状态会多一点,多出来的转换就更多了。或者可以用协程做小状态?怎么用协程做个小状态机?可以在行为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的偏差小于一个值后再得到随机方向,循环。
效果演示
总结
- 使用状态机写AI,一开始会想分成安全、警戒、攻击等状态,但是很快会发现这样粗略的状态划分不够,大状态下必然有次级状态规定人物的行为;
- 人物的行为不是执行一个函数就完成,都需要持续一段时间,有的持续一段时间结束,有的持续中还要检测结束条件;
NavMesh相关
给使用大型Terrain的场景生成NavMesh
收集对象不要选All Game Objects,否则容易卡死。选Current Object Hierarchy。好像如果层级里包含巨大的水面就会卡死,可以通过Layer把水面排除。


使用动画根运动的人物配合navMesh实现追击
navMesh.SetDestination()可以让ai寻路移动,但是ai使用动画根运动怎么按寻路移动?
navMesh.SetDestination()的移动本质上是把位置不断设置为navMeshAgent.nextPosition。就把NavMeshAgent.nextPosition作为移动的目的地。
AI.NavMeshAgent-nextPosition - Unity 脚本 API
但是如果让人物转向navMeshAgent.nextPosition,可能出现和当前位置重叠而提示
![]()
9489

被折叠的 条评论
为什么被折叠?



