• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

用于击中移动目标的 Unity 解决方案

法与移动目标发生碰撞。这种情况通常可以称为“击中移动目标”问题。这在塔防游戏或导弹指挥类游戏中尤为突出。我们可能需要创建一个 AI 或算法来计算敌人的动作并向其开火。 

让我们看看如何解决这个特殊问题,这次是在 Unity 中。

1. 导弹指挥游戏

对于这个特定的教程,我们将考虑一个导弹指挥游戏。在游戏中,我们在地面上有一个炮塔,可以向来袭的小行星发射导弹。我们不应该让小行星撞击地面。 

游戏是基于点击的,我们需要点击以瞄准炮塔。在人工协助下,游戏机制非常简单,因为炮塔只需要瞄准和开火。但是想象一下,如果炮塔需要自动向来袭的小行星射击。 

自动射击人工智能的挑战

炮塔需要找出有多少小行星正在接近地面。一旦它拥有一组所有接近的小行星,它就需要进行威胁分析以确定要瞄准的小行星。与快速移动的小行星相比,缓慢移动的小行星威胁较小。此外,靠近地面的小行星也是迫在眉睫的威胁。 

这些问题可以通过比较来袭小行星的速度和位置来解决。一旦我们确定了目标,我们就会遇到最复杂的问题。炮塔应该什么时候开火?它应该在哪个角度发射?导弹发射后应该什么时候爆炸?第三个问题变得相关,因为导弹爆炸也可以摧毁小行星并且具有更大的影响半径。

为了简化问题,炮塔可以决定立即开火。然后我们只需要计算出射击的角度和爆炸的距离。另外,小行星可能已经通过了可能被撞击的区域,这意味着没有解决方案!

您应该下载与本教程一起提供的统一源代码以查看实际的解决方案。我们将看到如何得出该解决方案。

2. 解决方案

为了找到解决方案,我们将复习一下我们的高中数学。它非常简单,涉及求解二次方程。二次方程看起来像 axˆ2 + bx + c = 0,其中x是要找到的变量,它以 2 的最高幂出现。 

分析问题

让我们尝试用图表来表示我们的问题。 

用于击中移动目标的 Unity 解决方案  第1张

绿线显示了小行星要遵循的预测路径。当我们处理匀速运动时,小行星以恒定速度运动。我们的炮塔需要沿着蓝色路径旋转并发射导弹,以便它在未来与小行星相撞。

对于匀速运动,物体行进的距离是时间和物体速度的乘积,即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),则未知XY可以如下求出。

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是 变量ta是 Vxˆ2 +Vyˆ2 - sˆ2b2* (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意味着我们错过了开火的机会。未知数XY可以通过代入t它们各自方程中的值来找到。

X = t * Vx + Ax;Y = t * Vy + Ay;

一旦我们知道了碰撞点,我们就可以转动炮塔发射导弹,这肯定会在t几秒钟后击中移动的小行星。

3. 在 Unity 中实现

对于示例 Unity 项目,我使用了最新 Unity 版本的精灵创建功能来创建必要的占位符资产。这可以通过Create > Sprites >访问,如下所示。

用于击中移动目标的 Unity 解决方案  第2张

我们有一个名为的游戏脚本MissileCmdAI,它附加到场景摄像机。它包含对炮塔精灵、导弹预制件和小行星预制件的引用。我正在使用SimplePoolquill18 来维护导弹和小行星的对象池。它可以在 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 炮塔,它可以击落任何小行星,除非小行星的速度接近或高于我们的导弹速度。我们所遵循的推导可用于解决各种类似问题,这些问题可以以二次方程的形式表示。 


文章目录
  • 1. 导弹指挥游戏
    • 自动射击人工智能的挑战
  • 2. 解决方案
    • 分析问题
    • 输入数学
    • 求解二次方程
  • 3. 在 Unity 中实现
    • 小行星
    • 瞄准
  • 结论