需要编写脚本控制敌人的行为,完成包括巡逻、射击、追击、逃跑四个功能
考虑机器人的行为受一个决策树的影响,编写有限状态自动机形成决策树,通过条件分支语句来对机器人的行为进行限制和控制。
本文的解决方案借鉴了以下博文,侵权删
有限状态自动机介绍以及框架编写方法
有限状态机,英语:Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。它反映从系统开始到现在时刻的输入变化,转移指示状态变更,并且用必须满足来确使转移发生的条件来描述它;动作是在给定时刻要进行的活动的描述。
巡逻功能借助于Unity3D自带的导航系统,利用导航系统代理,来规划机器人的巡逻,具体的流程见文章
Unity导航系统
那么在我的设置中,在地图中设置了4个空物体,利用循环数组来实现机器人在这四个地方一直巡逻,直到触发事件
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class partorl : MonoBehaviour
{
public Transform target;
private Vector3 targetPoint;
private NavMeshAgent agent;
private List<Vector3> dstPoints;
private int nextIdx=0;
private float calcdist = 5f;
// Start is called before the first frame update
void Start()
{
//生成各个导航点的坐标
dstPoints = new List<Vector3>();
dstPoints.Add(GameObject.Find("LactionMarkingFlag_F4_Buff").transform.position);
dstPoints.Add(GameObject.Find("LactionMarkingFlag_bule_spawn").transform.position);
dstPoints.Add(GameObject.Find("LactionMarkingFlag_red_spawn").transform.position);
dstPoints.Add(GameObject.Find("LactionMarkingFlag_F5_Buff").transform.position);
agent = this.transform.GetComponent<NavMeshAgent>();
}
// Update is called once per frame
void Update()
{
if(Vector3.Distance(this.transform.position,dstPoints[nextIdx])< calcdist)//通过误差值并且识别是否到达目的地
{
if (nextIdx == dstPoints.Count - 1)
{
nextIdx = 0;
}
else
{
nextIdx++;
}
}
agent.SetDestination(dstPoints[nextIdx]);
}
}
巡逻功能是比较简单的,完成了巡逻,下面来考虑对自动机器人的开火和血条的脚本代码进行重编写
但是需要思考一点是,巡逻功能和以下这几个功能如何配合?
我们想到,巡逻这个是作为entry(入口状态),以下的这几个功能都是只有当AI机器人遇到了我的机器人才有可能发生的,因此
巡逻这个功能的真正作用是作为一个触发器,用来检测判断我的机器人是否遇上了AI机器人,并且需要触发条件函数了
自动射击的功能实现:初步预想为,当遇上敌人并且进入射程的时候,就进入这个状态,这个状态的实现分为两步:
第一步:机器人云台枪口转换位置对准敌人的中心(可以调整误差加减2左右)为开火范围,开火
第二步:做碰撞检测,对AI机器人的弹药量进行调整,以及对玩家机器人进行扣血的判定
首先追击敌人这个状态需要进行判断,如果自身的血量大于500而且弹药量大于30的时候就判定可以追击,否则遇到敌人就进入逃跑状态
实际上这个功能与自动射击功能应当是具有一个先后关系的,只有追击到了敌人才能开枪,因此追击的状态实现可以分为两步
第一步:在视野中发现敌人(考虑实现),停止普通巡逻状态
第二步:进入警戒状态,根据新的寻路规向敌人逼近
这个功能与追击是对立的,那么触发条件当然也是对立的,自身血量小于50而且弹药量小于30的时候就逃跑,逃跑的路线规划与追击是不同的,逃跑要求尽可能地远离敌人,这就要求需要动态规划逃跑路线了
这里采用了一个带有isTrigger的碰撞体作为敌人视野的检测(也就是跟随镜头的的一个区域),来作为触发器来进行检测,来判断玩家是否进入了视野了 ,由于触发器的知识已经学习过了,因此很快能写出代码
bool isFirst = false;
public float fileldOfView = 100f;//视野宽度
public bool playerInsight = false;//玩家是否在视野内的标志量
public Vector3 playerLastSight;//玩家最后一次在视野中出现的位置
//public Vector3 resetPos = GameObject.Find("LactionMarkingFlag_red_spawn").transform.position;//重置位置为红方老家
private BoxCollider col;//得到视野检测的碰撞体
//colArr[0]:AIRobot的碰撞体
//colArr[1]:AIRobot的视野检测碰撞体(trigger)
//colArr[2]:AIRobot的射程检测碰撞体(trigger)
private GameObject player;
void Start()
{
Debug.Log("检测视野的脚本正常运行中");
BoxCollider[] colArr = GetComponents<BoxCollider>();
col = colArr[1];//第二个碰撞体才作为视野检测的碰撞体
player = GameObject.FindWithTag("PlayerRobot");
//playerLastSight = resetPos;
}
private void OnTriggerStay(Collider other)
{
if (other.gameObject == player)
{
playerInsight = false;
Vector3 dir = other.transform.position - transform.position;//形成一条AI机器人到玩家机器人的射线
float angle = Vector3.Angle(dir,transform.forward);//计算这条直线和AI机器人朝向的夹角
if(angle < this.fileldOfView*0.5)
{
Debug.Log("进入了机器人视野中");
//那么也就是说,如果没有障碍物遮挡的情况下,那么就可以认定为被发现了
//利用射线来对此进行判定是否存在障碍物
RaycastHit raycastHit;
if (Physics.Raycast(transform.position + transform.up, dir.normalized, out raycastHit, col.size.z))
{
if(raycastHit.collider.gameObject == player)
{
playerInsight = true;
playerLastSight = player.transform.position;
Debug.Log("没有障碍物遮挡");
}
}
}
}
}
private void OnTriggerExit(Collider other)
{
if (isFirst == false)
{
isFirst = true;
return;
}
if (other.gameObject == player)
{
playerInsight = false;
Debug.Log("玩家离开了视野");
}
}
void Update()
{
}
添加成员变量
/*第一部分:巡逻所需要用到的变量*/
public Transform target;
private Vector3 targetPoint;
private NavMeshAgent agent;
private List<Vector3> dstPoints;
private int nextIdx = 0;
private float calcdist = 5f;
/****************************/
/*第二部分:添加追踪状态和射击状态的成员变量*/
//执行追踪的成员变量
public float chaseSpeed = 15f; //追踪速度
public float chaseWait = 5f; //追踪上限时间
private float chaseTimer = 0f; //追踪计时器
public float sqrPlayerDist = 4f; //与玩家的距离
private bool isChasing = false; //当前是否处于追踪的状态
//执行攻击的成员变量
public float shootRotSpeed =4f;//视野中有敌人后视角进行转移
public float shootFreeTime = 2f;//攻击间隔
private float shootTimer = 0f;//攻击间隔计算器
private AIRobotSight aiRobotSight;//敌人的视野对象
private Transform player;//玩家信息
/***************************************/
/*第三部分:机器人发射子弹的成员变量*/
public GameObject bullet;//定义一个public成员,用来指代子弹
public Transform muzzle;//同理,定义一个指代枪口的对象
public Text ammorText;//子弹文本
private float speed;//定义私有成员变量,指代的是子弹的速度
public int ammorCnt;
private string robotType;
private string UIType;
public Slider slider;
/********************************/
攻击函数的逻辑
/*攻击函数的封装*/
void shooting()
{
Vector3 lookPos = player.position;//玩家的位置,先保存下将要朝向的位置信息
lookPos.y = transform.position.y;//设置高度,避免存在高度差而误判
Vector3 targetDir = lookPos - transform.position;//设置真正的距离向量
//根据距离向量来设置AI机器人的朝向
transform.rotation = Quaternion.Slerp(transform.rotation,Quaternion.LookRotation(targetDir),Mathf.Min(1f,Time.deltaTime*shootRotSpeed));
agent.isStopped = true;//停止寻路,发起攻击
//做一个是否朝向敌人,至少不会马枪的角度判断
if (Vector3.Angle(transform.forward,targetDir) <2)//偏差少于两度,认为大概率不会马枪
{
//在这里写发射子弹的功能
//1.首先避免一直发射子弹,需要判断是否在冷却时间
if(shootTimer > shootFreeTime)
{
shootTimer = 0f;//计时器置零
GameObject bulletClone;//新建一个子弹的克隆体,因为每次都会发射新的子弹
bulletClone = Instantiate(bullet, muzzle.position,Quaternion.LookRotation(player.position-muzzle.position));
//要保持开火的位置与云台的位置一致
//Vector3 fwd = transform.TransformDirection(Vector3.forward * speed);
bulletClone.AddComponent<destory>();
bulletClone.GetComponent<Rigidbody>().velocity = transform.TransformDirection(Vector3.forward * speed);
ammorCnt--;
ammorText.text = "子弹余量:" + ammorCnt + "/300";
slider.value = ammorCnt;
}
shootTimer += Time.deltaTime;
}
}
追击函数的逻辑
/*追击函数的封装*/
void chasing()
{
agent.isStopped = false;//启用导航代理,追击敌人
Vector3 sightDelPos = aiRobotSight.playerLastSight-transform.position;//计算玩家上次出现的位置和我当前位置
if(sightDelPos.sqrMagnitude > sqrPlayerDist)
{
agent.destination = aiRobotSight.playerLastSight;
}
agent.speed = chaseSpeed;
if(agent.remainingDistance < agent.stoppingDistance)
{
chaseTimer += Time.deltaTime;
if (chaseTimer > chaseWait)
{
//返回巡逻状态
isChasing = false;
chaseTimer = 0f;
}
}
else
{
chaseTimer = 0f;
}
}
逃跑函数的逻辑
void runaway()
{
//借用了地图上的巡逻点,作为逃跑规划点,打擂算法,找出距离敌人最远的一个巡逻点进行逃跑
Vector3 playerPos = player.position - dstPoints[0];
float minDst = playerPos.sqrMagnitude;
int minIdx=0;
for(int i = 1; i < dstPoints.Count; i++)
{
Vector3 temp = player.position - dstPoints[i];
if (minDst < temp.sqrMagnitude)
{
minIdx = i;
minDst = temp.sqrMagnitude;
}
}
//最终决定去这个巡逻点
agent.speed = 20f;
agent.SetDestination(dstPoints[minIdx]);
}