Unity人工智能学习 — 确定性AI算法之追踪算法二

2015年08月17日 13:22 0 点赞 0 评论 更新于 2025-11-21 18:48

作者及原文信息

作者:风晓融冰 原文链接:点击查看

更自然的追踪方式

在上一篇文章中,我们介绍了追踪算法的简单形式,但这种形式看上去比较假,因为AI控制的对象会过于精确地跟踪目标。为了实现更自然的追踪,我们可以让跟踪者的方向矢量与从跟踪目标的中心到跟踪者的中心所定义的方向矢量靠拢。

算法思路概述

假设AI控制的追踪者和追踪目标分别具有以下属性:

  • 追踪者属性
  • 位置:(tracker.x, tracker.y)
  • 速度:(tracker.vx, tracker.vy)
  • 追踪目标属性
  • 位置:(target.x, target.y)
  • 速度:(target.vx, target.vy)

调整追踪者速度向量的逻辑步骤

  1. 计算从跟踪者到跟踪目标的向量并归一化: 首先计算向量 TV=(target.x - tracker.x, target.y - tracker.y)=(tvx, tvy),然后对 TV 进行归一化。归一化的目的是得到一个单位向量,方便计算它与坐标轴的角度。归一化的计算方法是 sqrt(x^2 + y^2)
  2. 调整追踪者当前的速度向量: 给追踪者的速度向量加上一个按 rate 比例缩放过的 TV,代码如下:
    tracker.x += rate * tvx;
    tracker.y += rate * tvy;
    

    这一步是关键,它使得追踪过程不再是直接紧密追踪,而是会有一个变轨迹的过程。当 rate 接近1时,跟踪向量会合得更快,跟踪算法对目标的跟踪更紧密,并能更快地修正目标的运动。

  3. 设置速度上限: 跟踪者的速度向量修改过后,有可能向量的速度会溢出最大值。因为跟踪者一旦锁定了目标的方向就会继续沿着该方向加速,所以需要设置一个上限,让追踪者的速度在某处慢下来。

Unity 5.1.1 实现效果图

在Unity 5.1.1中实现该算法的效果图如下(此处可插入效果图)。

导弹追踪方向修正

在实现过程中,我们还需要调整导弹的追踪方向 rotation 的变化。在3D空间中,可以直接使用 lookAt 方法来使得导弹的运动方向始终朝向目标,但在2D平面上没有这么方便的方法,因此我们需要自己编写算法。

未加方向修正算法的效果

如果不加这个方向修正算法,导弹的运行会显得非常生硬,运动的轨迹和导弹头的朝向并不一致(此处可插入未修正效果图)。

修正导弹头方向的代码及思路

void LookAtTarget()
{
float zAngles;
if (moveVy == 0)
{
zAngles = moveVx >= 0 ? -90 : 90;
}
else
{
zAngles = Mathf.Atan(moveVx / moveVy) * (-180 / Mathf.PI);
if (moveVy < 0)
{
zAngles = zAngles - 180;
}
}
Vector3 tempAngles = new Vector3(0, 0, zAngles);
Quaternion tempQua = this.transform.rotation;
tempQua.eulerAngles = tempAngles;
this.transform.rotation = tempQua;
}

算法思路

  • 这个平面上的角度主要是Z轴的角度变化,Z轴的角度是导弹头方向直线与y轴的夹角,且坐标的顶点位于屏幕的左上角。
  • 根据导弹的运动速度矢量来调整导弹头的方向。导弹的速度矢量为x和y方向的矢量和,通过反三角函数来计算出导弹与屏幕坐标y轴的夹角。
  • 需要特别注意 moveVy 为0的情况,不考虑这个会导致计算反三角时分母为零而溢出报错;以及 moveVy 小于0的情况,不考虑这个会使得方向刚好相反。

完整代码实现

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class AITrackAdvanced : MonoBehaviour
{
public Image target;
public float target_moveSpeed;
public float MIN_trackingRate; // 最小的追踪向量改变率
public float MIN_TrackingDis;
public float MAX_trackingVel;
public float moveVx; // x方向的速度
public float moveVy; // y方向的速度

// Use this for initialization
void Start()
{

}

// Update is called once per frame
void Update()
{
Debug.Log((Mathf.Atan(moveVx / moveVy) * (-180 / Mathf.PI)));

// LookAtTarget();
// this.transform.position += new Vector3(moveVx * Time.deltaTime, moveVy * Time.deltaTime, 0);
MoveTarget();
Track_AIAdvanced();
CheckMoveBoundary();
}

void LookAtTarget()
{
float zAngles;
if (moveVy == 0)
{
zAngles = moveVx >= 0 ? -90 : 90;
}
else
{
zAngles = Mathf.Atan(moveVx / moveVy) * (-180 / Mathf.PI);
if (moveVy < 0)
{
zAngles = zAngles - 180;
}
}
Vector3 tempAngles = new Vector3(0, 0, zAngles);
Quaternion tempQua = this.transform.rotation;
tempQua.eulerAngles = tempAngles;
this.transform.rotation = tempQua;
}

/// <summary>
/// 通过键盘来控制移动目标
/// </summary>
void MoveTarget()
{
float x = Input.GetAxis("Horizontal") * 100;
float y = Input.GetAxis("Vertical") * 100;
// 如果超出屏幕范围则让它出现在另一面
target.transform.Translate(x * Time.deltaTime * target_moveSpeed, y * Time.deltaTime * target_moveSpeed, 0);
if (target.transform.position.x >= Screen.width)
{
// 使用了Image的target.rectTransform.lossyScale.x来表示显示的图片宽度
target.transform.position = new Vector3(-target.rectTransform.lossyScale.x, target.transform.position.y, 0);
}
else if (target.transform.position.x < -target.rectTransform.lossyScale.x)
{
target.transform.position = new Vector3(Screen.width, target.transform.position.y, 0);
}
if (target.transform.position.y >= Screen.height)
{
target.transform.position = new Vector3(target.transform.position.x, -target.rectTransform.lossyScale.y, 0);
}
else if (target.transform.position.y < -target.rectTransform.lossyScale.y)
{
target.transform.position = new Vector3(target.transform.position.x, Screen.height, 0);
}
}

/// <summary>
/// 追踪算法
/// </summary>
void Track_AIAdvanced()
{
// 计算与追踪目标的方向向量
float vx = target.transform.position.x - this.transform.position.x;
float vy = target.transform.position.y - this.transform.position.y;

float length = PointDistance_2D(vx, vy);
// 如果达到距离就追踪
if (length < MIN_TrackingDis)
{
vx = MIN_trackingRate * vx / length;
vy = MIN_trackingRate * vy / length;
moveVx += vx;
moveVy += vy;

// 增加一点扰动
if (Random.Range(1, 10) == 1)
{
vx = Random.Range(-1, 1);
vy = Random.Range(-1, 1);
moveVx += vx;
moveVy += vy;
}
length = PointDistance_2D(moveVx, moveVy);

// 如果导弹飞的速度太快就让它慢下来
if (length > MAX_trackingVel)
{
// 让它慢下来
moveVx *= 0.75f;
moveVy *= 0.75f;
}
}
// 如果不在追踪范围内,随机运动
else
{
if (Random.Range(1, 10) == 1)
{
vx = Random.Range(-2, 2);
vy = Random.Range(-2, 2);
moveVx += vx;
moveVy += vy;
}
length = PointDistance_2D(moveVx, moveVy);

// 如果导弹飞的速度太快就让它慢下来
if (length > MAX_trackingVel)
{
// 让它慢下来
moveVx *= 0.75f;
moveVy *= 0.75f;
}
}

this.transform.position += new Vector3(moveVx * Time.deltaTime, moveVy * Time.deltaTime, 0);
}

/// <summary>
/// 计算从零点到这个点的距离
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
float PointDistance_2D(float x, float y)
{
// 使用了泰勒展开式来计算,有3.5%的误差,直接使用开方计算会比较慢,但是测试了我的电脑好像没有什么变化可能是数据量不大体现不出来
/*x = Mathf.Abs(x);
y = Mathf.Abs(y);
float mn = Mathf.Min(x, y); // 获取x,y中最小的数
float result = x + y - (mn / 2) - (mn / 4) + (mn / 8);*/

float result = Mathf.Sqrt(x * x + y * y);
return result;
}

void CheckMoveBoundary()
{
// 检测是否超出了边界
if (this.transform.position.x >= Screen.width)
{
this.transform.position = new Vector3(-this.GetComponent<Image>().rectTransform.lossyScale.x, 0, 0);
}
else if (this.transform.position.x < -this.GetComponent<Image>().rectTransform.lossyScale.x)
{
this.transform.position = new Vector3(Screen.width, this.transform.position.y, 0);
}
if (this.transform.position.y >= Screen.height)
{
this.transform.position = new Vector3(this.transform.position.x, -this.GetComponent<Image>().rectTransform.lossyScale.y, 0);
}
else if (this.transform.position.y < -this.GetComponent<Image>().rectTransform.lossyScale.y)
{
this.transform.position = new Vector3(this.transform.position.x, Screen.height, 0);
}
}
}

资源下载与转载说明

最后附上工程的下载地址,里面是我用Unity 5.1.1写的如图的演示程序,还包括之前两篇文章中的演示程序。点击打开链接

CSDN个人博客地址:凯尔八阿哥栏,转载请注明出处。

作者信息

洞悉

洞悉

共发布了 3994 篇文章