最近在玩一款飞行射击游戏,时常驾驶着自己的飞机在枪林弹雨中穿行,然后就快要通过关卡的时候,我奋力的击杀,却依然顶不住敌军猛烈的攻势。在敌机如蛇皮般的走位中,忍着快要抓狂的心情努力的把着鼠标,但我的准星却仍然无法瞄准到敌人。想想都很气!如果这时候,能在飞机上发射出跟踪弹~~~~~~,贼舒服!
首先梳理一下,一般导弹有以下两种模式。
①跟踪 | 发射之后尾随目标飞行,直到击中目标,BOOM! |
②预判 | 发射之后,判断目标的飞行方向,然后前去拦截,直到击中目标,BOOM! |
首先我们来说说跟踪。
一般想到的就是 transform.LookAt(Vector3 worldPos) 方法。这个方法可以让该 transform 看向这个 worldPos ,此时如果把 worldPos 改成需要追踪的目标的位置,放在 Update 中每帧调用,那么就会让 transform 在自己的生命周期中,始终看向目标。
代码如下:
using UnityEngine;
public class Rocket : MonoBehaviour
{
public Transform target; //跟踪目标
public GameObject FX; //爆炸特效
public float moveSpeed; //移动速度
public float rotateSpeed ; //旋转速度
private void Update()
{
if (target != null) //判断当前是否有跟踪目标 如果有的话 执行 以下跟踪逻辑
{
transform.LookAt(target);
// Vector3 tempDirV = target.position - transform.position; //计算出正确的转向
// Quaternion rightDirQ = Quaternion.LookRotation(tempDirV); //将转向 转化为 四元数
// transform.rotation = Quaternion.Lerp(transform.rotation, rightDirQ, rotateSpeed); //利用四元数插值方法 将方向赋给 transform.rotation
}
transform.position += transform.forward * moveSpeed * Time.deltaTime; //向前移动
}
//当 有物体进入到 自己的触发器时 调用一次
private void OnTriggerEnter(Collider other)
{
GameObject tempFX = Instantiate(FX, transform.position, transform.rotation); //生成一个爆炸特效 并给予位置和旋转信息
Destroy(gameObject); //销毁自己
Destroy(tempFX, 1); //等待 1秒 销毁刚才 创建生成的爆炸特效
}
}
上边是两种方法,除了lookAt 还有注释的另一种方法。
效果如图:
然后来说预判。
先来想想预判的逻辑是怎样的。所谓预判,就是拦截。我们得知道的数据:
target | 移动方向,移动速度 |
self | 移动速度 |
其中移动方向,有两种情况。因为逻辑是一帧一帧的运行,所以我们选取碰撞的前一帧为基准。
同向 | 反向 |
在target的后方发生碰撞 | 在target的脸前发生碰撞 |
如图所示:
同向 反向
其中 向量 Vab = target.position - rocket.position。
L3 = Vab.magnitude。
计算出 ∠α = Vector3.angle(Vab ,target.移动方向)。
L2 = Mathf.Cos(∠α * Mathf.Deg2Rad) * L3 。
L1 = Mathf.Sin(∠α * Mathf.Deg2Rad) * L3 。
根据勾股定理: 得出:
同向 | 反向 |
化简后可以得出:
看的出是一元二次三项式,解 。
这样就解出来时间 t 。在 t 秒后两者相遇。
从而依据 t ,Vb ,target.position ,计算出 Boom点(C点)。也就是当前的碰撞点。
其中主要的代码逻辑如下:
Vector3 AB= target.position - transform.position;
float angle = Vector3.Angle(AB, targetDir); //获取角度
float L1 = Mathf.Sin(angle * Mathf.Deg2Rad) * AB.magnitude; //计算L1
float L2 = Mathf.Cos(angle * Mathf.Deg2Rad) * AB.magnitude; //计算L2
float a = Vb* Vb- Va* Va; //计算 a
float b = 2 * Vb* L2; //计算 b
float c = L1 * L1 + L2 * L2; //计算 c
if(a == 0) //a = 0 的话,方程有唯一一解
{
rightTime = -(c / b);
}
else
{
if (Vector3.Dot(AB, targetDir) < 0) //判断 同向 还是 反向
{
b *= -1; //如果 反向 则b应该为相反数
}
float detla = MathTool.GetDetla(b, a, c); //计算 detla
if (detla < 0)
return; //判断 detla < 0 则方程无解
float time1 = (-b + Mathf.Sqrt(detla)) / (2 * a); //计算 正确的时间
float time2 = (-b - Mathf.Sqrt(detla)) / (2 * a);
rightTime = time1 > time2 ? time1 : time2;
}
Vector3 rightPos = targetDir * rightTime * Vb+ target.position; //计算 正确的 目标点
因为方便调用,所以我将方法封装在了一个类里。具体代码逻辑如下:
Rocket.cs
using UnityEngine;
public class Rocket : MonoBehaviour
{
public Transform target; //跟踪目标
public GameObject FX; //爆炸特效
public float selfSpeed; //移动速度
public float rotateSpeed; //旋转速度
private float lastTime;
private Vector3 lastPos;
private void Start()
{
if (target != null)
{
lastTime = Time.time; //首先初始化 lastTime 和 lastPos
lastPos = target.position;
}
}
// Update is called once per frame
void Update()
{
if (target != null)
{
float targetSpeed = MathTool.GetRightSpeed(lastPos, target.position, lastTime, Time.time);
Vector3 rightPos = MathTool.GetRightPos(targetSpeed, selfSpeed, lastPos, transform, target); //计算 正确的 目标点
if (rightPos.Equals(Vector3.zero)) //判断方程有解
return;
Quaternion rightDir = Quaternion.LookRotation(rightPos - transform.position); //转化四元数
transform.rotation = Quaternion.Lerp(transform.rotation, rightDir, Time.deltaTime * 25); //设置旋转
lastPos = target.position; //记录 当前帧的位置
lastTime = Time.time; //记录 当前帧的时间 供下一帧 使用
}
transform.position += transform.forward * Time.deltaTime * selfSpeed; //向前移动
}
//当 有物体进入到 自己的触发器时 调用一次
private void OnTriggerEnter(Collider other)
{
GameObject tempFX = Instantiate(FX, transform.position, transform.rotation); //生成一个爆炸特效 并给予位置和旋转信息
Destroy(gameObject); //销毁自己
Destroy(tempFX, 1); //等待 1秒 销毁刚才 创建生成的爆炸特效
}
}
MathTool.cs
using UnityEngine;
public class MathTool : MonoBehaviour
{
/// <summary>
/// 计算目标 当前帧的移动速度
/// </summary>
/// <param name="lastPos"></param>
/// <param name="currentPos"></param>
/// <param name="lastTime"></param>
/// <param name="currentTime"></param>
/// <returns></returns>
public static float GetRightSpeed(Vector3 lastPos, Vector3 currentPos, float lastTime, float currentTime)
{
if ((currentTime - lastTime) < 0)
return -1;
else if ((currentTime - lastTime) == 0)
return 0;
else
return Vector3.Distance(currentPos, lastPos) / (currentTime - lastTime);
}
/// <summary>
/// 计算目标 正确的移动方向
/// </summary>
/// <param name="lastPos"></param>
/// <param name="currentPos"></param>
/// <returns></returns>
public static Vector3 GetDirection(Vector3 lastPos, Vector3 currentPos)
{
return (currentPos - lastPos).normalized;
}
/// <summary>
/// 计算出 detla
/// </summary>
/// <param name="b"></param>
/// <param name="a"></param>
/// <param name="c"></param>
/// <returns></returns>
public static float GetDetla(float b, float a, float c)
{
return b * b - 4 * a * c;
}
/// <summary>
/// 计算目标 正确的碰撞时间
/// </summary>
/// <param name="b"></param>
/// <param name="detla"></param>
/// <param name="a"></param>
/// <param name="c"></param>
/// <returns></returns>
public static float GetRightTime(float b, float detla, float a, float c)
{
if (detla < 0) //判断 detla < 0 则方程无解
{
return -1;
}
else if (a == 0) //at^2 + bt +c = 0 当 a == 0 时 。
{
return -(b / c);
}
else if (detla == 0) //detla == 0 时。
{
return -(b / 2 * a);
}
else
{
float time1 = (-b + Mathf.Sqrt(detla)) / (2 * a);
float time2 = (-b - Mathf.Sqrt(detla)) / (2 * a);
return time1 > time2 ? time1 : time2;
}
}
/// <summary>
/// 计算目标 正确的目标移动位置
/// </summary>
/// <param name="targetSpeed"></param>
/// <param name="selfSpeed"></param>
/// <param name="lastPos"></param>
/// <param name="self"></param>
/// <param name="target"></param>
/// <returns></returns>
public static Vector3 GetRightPos(float targetSpeed, float selfSpeed, Vector3 lastPos, Transform self, Transform target)
{
if (targetSpeed == -1)
return Vector3.zero;
Vector3 targetDir = GetDirection(lastPos, target.position); //获取 目标 移动方向
Vector3 AB = target.position - self.position;
float angle = Vector3.Angle(AB, targetDir); //获取角度
float L1 = Mathf.Sin(angle * Mathf.Deg2Rad) * AB.magnitude; //计算L1
float L2 = Mathf.Cos(angle * Mathf.Deg2Rad) * AB.magnitude; //计算L2
float a = targetSpeed * targetSpeed - selfSpeed * selfSpeed; //计算 a
float b = 2 * targetSpeed * L2; //计算 b
float c = L1 * L1 + L2 * L2; //计算 c
if (Vector3.Dot(AB, targetDir) < 0) //判断 同向 还是 反向
{
b *= -1; //如果 反向 则b应该为相反数
}
float detla = GetDetla(b, a, c); //计算 detla
float rightTime = GetRightTime(b, detla, a, c); //计算 正确的时间
if (rightTime == -1)
return Vector3.zero;
Vector3 rightPos = targetDir * rightTime * targetSpeed + target.position; //计算 正确的 目标点
return rightPos;
}
}
这就是预判逻辑了。接下来看看运行效果。
同向 反向
示例项目源码工程我打到一个包里了。想一起学习的可以下载下来看看。点我点我!
本人原创,虽然小例,但如果文章有所引用,请标明出处。