法与移动目标发生碰撞。这种情况通常可以称为“击中移动目标”问题。这在塔防游戏或导弹指挥类游戏中尤为突出。我们可能需要创建一个 AI 或算法来计算敌人的动作并向其开火。
让我们看看如何解决这个特殊问题,这次是在 Unity 中。
1. 导弹指挥游戏
对于这个特定的教程,我们将考虑一个导弹指挥游戏。在游戏中,我们在地面上有一个炮塔,可以向来袭的小行星发射导弹。我们不应该让小行星撞击地面。
游戏是基于点击的,我们需要点击以瞄准炮塔。在人工协助下,游戏机制非常简单,因为炮塔只需要瞄准和开火。但是想象一下,如果炮塔需要自动向来袭的小行星射击。
自动射击人工智能的挑战
炮塔需要找出有多少小行星正在接近地面。一旦它拥有一组所有接近的小行星,它就需要进行威胁分析以确定要瞄准的小行星。与快速移动的小行星相比,缓慢移动的小行星威胁较小。此外,靠近地面的小行星也是迫在眉睫的威胁。
这些问题可以通过比较来袭小行星的速度和位置来解决。一旦我们确定了目标,我们就会遇到最复杂的问题。炮塔应该什么时候开火?它应该在哪个角度发射?导弹发射后应该什么时候爆炸?第三个问题变得相关,因为导弹爆炸也可以摧毁小行星并且具有更大的影响半径。
为了简化问题,炮塔可以决定立即开火。然后我们只需要计算出射击的角度和爆炸的距离。另外,小行星可能已经通过了可能被撞击的区域,这意味着没有解决方案!
您应该下载与本教程一起提供的统一源代码以查看实际的解决方案。我们将看到如何得出该解决方案。
2. 解决方案
为了找到解决方案,我们将复习一下我们的高中数学。它非常简单,涉及求解二次方程。二次方程看起来像 axˆ2 + bx + c = 0
,其中x
是要找到的变量,它以 2 的最高幂出现。
分析问题
让我们尝试用图表来表示我们的问题。
绿线显示了小行星要遵循的预测路径。当我们处理匀速运动时,小行星以恒定速度运动。我们的炮塔需要沿着蓝色路径旋转并发射导弹,以便它在未来与小行星相撞。
对于匀速运动,物体行进的距离是时间和物体速度的乘积,即D = T x S
代表D
距离,T
是行进所用的时间,是行进D
速度S
。假设我们的小行星和导弹肯定会相撞,我们可以从时间上求出导弹跟随的蓝线的距离t
。同时t
,我们的小行星也会到达同样的位置。
本质上,在同一时间t
,小行星将从当前位置到达碰撞位置,导弹也将在同一时间到达相同的碰撞位置t
。因此,有时t
小行星和导弹与炮塔的距离相同,因为它们会相互碰撞。
输入数学
我们可以将炮塔到小行星和导弹在未来这个时间的距离等同起来t
,以便推导出我们的二次方程与变量t
。(x1,y1)
考虑坐标为和的二维平面上的两个点(x2,y2)
。它们之间的距离D
可以使用下面的公式计算。
D^2 = (x2-x1)^2 + (y2-y1)^2
如果我们将炮塔位置记为(Tx,Ty)
,导弹速度记为s
,未知碰撞位置记为(X,Y)
,则上式可改写为:
D^2 = (X-Tx)^2 + (Y-Ty)^2;D = s * t;
导弹飞行距离所t
用的时间是多少D
?将两者相等,我们得到了未知数X
和 Y
另一个未知数的第一个方程t
。
s^2 * t^2 = (X-Tx)^2 + (Y-Ty)^2
我们知道小行星也在(X,Y)
同一时间到达同一个碰撞点t
,我们有以下方程,使用小行星速度矢量的水平和垂直分量。如果小行星的速度可以表示为(Vx,Vy)
,当前位置为(Ax,Ay)
,则未知X
和Y
可以如下求出。
X = t * Vx + Ax;Y = t * Vy + Ay;
将这些代入前面的方程中,我们得到了一个具有单个未知数的二次方程t
。
s^2 * t^2 = ((t * Vx + Ax)-Tx)^2 + ((t * Vy + Ay)-Ty)^2;
扩展和组合相似的术语:
s^2 * t^2 = (t * Vx + Ax)^2 + Tx^2 - 2*Tx*(t * Vx + Ax) +(t * Vy + Ay)^2 + Ty^2 - 2*Ty*(t * Vy + Ay);s^2 * t^2 = t^2 * Vx^2 + Ax^2 + 2*t*Vx*Ax + Tx^2 - 2*Tx*(t * Vx + Ax) +t^2 * Vy^2 + Ay^2 + 2*t*Vy*Ay + Ty^2 - 2*Ty*(t * Vy + Ay);s^2 * t^2 = t^2 * Vx^2 + Ax^2 + 2*t*Vx*Ax + Tx^2 - 2*Tx*t*Vx - 2*Tx*Ax +t^2 * Vy^2 + Ay^2 + 2*t*Vy*Ay + Ty^2 - 2*Ty*t*Vy - 2*Ty*Ay;0 = (Vx^2 + Vy^2 - s^2) * t^2 + 2* (Vx*Ax - Tx*Vx + Vy*Ay - Ty*Vy) *t + Ay^2 + Ty^2 - 2*Ty*Ay + Ax^2 + Tx^2 - 2*Tx*Ax; (Vx^2 +Vy^2 - s^2) * t^2 + 2* (Vx*(Ax - Tx) + Vy*(Ay - Ty)) *t + (Ay - Ty)^2 + (Ax - Tx)^2 = 0;
表示二的幂 asˆ2
和乘法符号 as*
可能使上面看起来像象形文字,但它本质上归结为最终的二次方程 axˆ2 + bx + c = 0
,其中x
是 变量t
,a
是 Vxˆ2 +Vyˆ2 - sˆ2
,b
是2* (Vx*(Ax - Tx) + Vy*(Ay - Ty))
,和c
是 (Ay - Ty)ˆ2 + (Ax - Tx)ˆ2
。我们在推导中使用了以下等式。
(a+b)^2 = a^2 + 2*a*b + b^2;(ab)^2 = a^2 - 2*a*b + b^2;
求解二次方程
为了求解二次方程,我们需要D
使用以下公式计算判别式:
D = b^2 - 4 * a * c;
如果判别式小于0
则没有解,如果是0
则只有一个解,如果是正数则有两个解。使用下面给出的公式计算解决方案。
t1 = (-b + sqrt(D))/ 2 * a;t2 = (-b - sqrt(D))/ 2 * a;
使用这些公式,我们可以找到未来t
发生碰撞的时间值。的负值t
意味着我们错过了开火的机会。未知数X
和Y
可以通过代入t
它们各自方程中的值来找到。
X = t * Vx + Ax;Y = t * Vy + Ay;
一旦我们知道了碰撞点,我们就可以转动炮塔发射导弹,这肯定会在t
几秒钟后击中移动的小行星。
3. 在 Unity 中实现
对于示例 Unity 项目,我使用了最新 Unity 版本的精灵创建功能来创建必要的占位符资产。这可以通过Create > Sprites >访问,如下所示。
我们有一个名为的游戏脚本MissileCmdAI
,它附加到场景摄像机。它包含对炮塔精灵、导弹预制件和小行星预制件的引用。我正在使用SimplePool
quill18 来维护导弹和小行星的对象池。它可以在 GitHub上找到。导弹和小行星的组件脚本附加到它们的预制件上,并在释放后处理它们的运动。
小行星
小行星在固定高度但随机水平位置随机生成,并以随机速度在地面上随机水平位置投掷。小行星产卵的频率使用AnimationCurve
. 脚本中的 SpawnAsteroid
方法MissileCmdAI
如下所示:
void SpawnAsteroid(){ GameObject asteroid=SimplePool.Spawn(asteroidPrefab,Vector2.one,Quaternion.identity); Asteroid asteroidScript=asteroid.GetComponent<Asteroid>(); asteroidScript.Launch(); SetNextSpawn(); }
类中的Launch
方法Asteroid
如下所示。
public void Launch(){//place the asteroid in top with random x & launch it to bottom with random x bl=Camera.main.ScreenToWorldPoint(new Vector2(10,0)); br=Camera.main.ScreenToWorldPoint(new Vector2(Screen.width-20,0)); tl=Camera.main.ScreenToWorldPoint(new Vector2(0,Screen.height)); tr=Camera.main.ScreenToWorldPoint(new Vector2(Screen.width,Screen.height)); transform.localScale=Vector2.one*(0.2f+Random.Range(0.2f,0.8f)); asteroidSpeed=Random.Range(asteroidMinSpeed,asteroidMaxSpeed); asteroidPos.x=Random.Range(tl.x,tr.x); asteroidPos.y=tr.y+1; destination.y=bl.y; destination.x=Random.Range(bl.x,br.x); Vector2 velocity= asteroidSpeed* ((destination-asteroidPos).normalized); transform.position=asteroidPos; asteroidRb.velocity=velocity;//set a velocity to rigidbody to set it in motion deployDistance=Vector3.Distance(asteroidPos,destination);//after traveling this much distance, return to pool } void Update () { if(Vector2.Distance(transform.position,asteroidPos)>deployDistance){//once we have traveled the set distance, return to pool ReturnToPool(); } } void OnTriggerEnter2D(Collider2D projectile) { if(projectile.gameObject.CompareTag("missile")){//check collision with missile, return to pool ReturnToPool(); } }
从该Update
方法中可以看出,一旦小行星到达地面预定距离deployDistance
,它将返回其对象池。从本质上讲,这意味着它与地面相撞。如果与导弹发生碰撞,它也会这样做。
瞄准
为了使自动瞄准起作用,我们需要经常调用相应的方法来查找和瞄准来袭的小行星。这是在MissileCmdAI
脚本中的 Start
方法中完成的。
InvokeRepeating("FindTarget",1,aiPollTime);//set ai code polling
该FindTarget
方法循环遍历场景中存在的所有小行星,以找到最近和最快的小行星。一旦找到,它就会调用该AcquireTargetLock
方法来应用我们的计算。
void FindTarget(){//find fastest & closest asteroid GameObject[] aArr=GameObject.FindGameObjectsWithTag("asteroid"); GameObject closestAsteroid=null; Asteroid fastestAsteroid=null; Asteroid asteroid; foreach(GameObject go in aArr){ if(go.transform.position.y<groundProximity){//find closest if(closestAsteroid==null){ closestAsteroid=go; }else if(go.transform.position.y<closestAsteroid.gameObject.transform.position.y){ closestAsteroid=go; } } asteroid=go.GetComponent<Asteroid>(); if(fastestAsteroid==null){//find fastest fastestAsteroid=asteroid; }else if(asteroid.asteroidSpeed>fastestAsteroid.asteroidSpeed){ fastestAsteroid=asteroid; } } //if we have a closest one target that, else target the fastest if(closestAsteroid!=null){ AcquireTargetLock(closestAsteroid); }else if(fastestAsteroid!=null){ AcquireTargetLock(fastestAsteroid.gameObject); } }
AcquireTargetLock
当我们应用我们的二次方程求解技巧来找到碰撞时间时,魔法就发生了t
。
void AcquireTargetLock(GameObject targetAsteroid){ Asteroid asteroidScript=targetAsteroid.GetComponent<Asteroid>(); Vector2 targetVelocity=asteroidScript.asteroidRb.velocity; float a=(targetVelocity.x*targetVelocity.x)+(targetVelocity.y*targetVelocity.y)-(missileSpeed*missileSpeed); float b=2*(targetVelocity.x*(targetAsteroid.gameObject.transform.position.x-turret.transform.position.x) +targetVelocity.y*(targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)); float c= ((targetAsteroid.gameObject.transform.position.x-turret.transform.position.x)*(targetAsteroid.gameObject.transform.position.x-turret.transform.position.x))+ ((targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)*(targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)); float disc= b*b -(4*a*c); if(disc<0){ Debug.LogError("No possible hit!"); }else{ float t1=(-1*b+Mathf.Sqrt(disc))/(2*a); float t2=(-1*b-Mathf.Sqrt(disc))/(2*a); float t= Mathf.Max(t1,t2);// let us take the larger time value float aimX=(targetVelocity.x*t)+targetAsteroid.gameObject.transform.position.x; float aimY=targetAsteroid.gameObject.transform.position.y+(targetVelocity.y*t); RotateAndFire(new Vector2(aimX,aimY));//now position the turret } } public void RotateAndFire(Vector2 deployPos){//AI based turn & fire float turretAngle=Mathf.Atan2(deployPos.y-turret.transform.position.y,deployPos.x-turret.transform.position.x)*Mathf.Rad2Deg; turretAngle-=90;//art correction turret.transform.localRotation=Quaternion.Euler(0,0,turretAngle); FireMissile(deployPos, turretAngle);//launch missile } void FireMissile(Vector3 deployPos, float turretAngle){ float deployDist= Vector3.Distance(deployPos,turret.transform.position);//how far is our target GameObject firedMissile=SimplePool.Spawn(missilePrefab,turret.transform.position,Quaternion.Euler(0,0,turretAngle)); Rigidbody2D missileRb=firedMissile.GetComponent<Rigidbody2D>(); Missile missileScript=firedMissile.GetComponent<Missile>(); missileScript.LockOn(deployDist); missileRb.velocity=missileSpeed*firedMissile.transform.up;//missile is rotated in necessary direction already }
一旦我们找到了撞击点,我们就可以很容易地计算出导弹为了撞击小行星而行进的距离,通过deployDist
变量传递LockOn
给导弹的方法。导弹以与小行星相同的方式行进此距离后,使用此值返回其对象池。在此之前,它肯定会撞击小行星,并且会触发碰撞事件。
结论
一旦我们实现它,结果看起来几乎是神奇的。通过降低该aiPollTime
值,我们可以使它成为一个无敌的 AI 炮塔,它可以击落任何小行星,除非小行星的速度接近或高于我们的导弹速度。我们所遵循的推导可用于解决各种类似问题,这些问题可以以二次方程的形式表示。
- 自动射击人工智能的挑战
- 分析问题
- 输入数学
- 求解二次方程
- 小行星
- 瞄准