计算子弹命中目标所需的速度的2D游戏算法?

9
我有一个相当简单的2D游戏,其中塔形精灵通过向移动精灵发射子弹来防御进攻。我的问题是:如果子弹速度始终保持不变,我该如何计算所需的子弹速度,使其能够到达移动目标?
我使用JavaScript,并拥有这些精灵变量(以及其他变量):sprite.x、sprite.y、sprite.width、sprite.height、sprite.speedX(即速度)、sprite.speedY等等。因此,我有源精灵(originSprite)、目标精灵(targetSprite)和子弹精灵(bulletSprite)等对象,它们都具有这种类型的值,我需要设置正确的子弹精灵速度值。
为了让游戏效果更好,子弹可能会从源精灵的外部开始(或某个定义的半径,虽然我想从源精灵中心开始也可以),但其子弹中心应该试图命中目标精灵的中心或类似的位置。请注意,在这个世界里没有重力或任何其他影响。 (也许我应该让我的精灵变量使用角度和速度,但现在我正在使用speedX和speedY...)
非常感谢!

我不明白你的问题。你能给一个例子吗? - Sjoerd
我们讨论的是一个塔防克隆游戏,其中进攻目标可能在子弹飞行时拐过角落,还是假定目标会直线移动? - deceze
举个例子,Sjoerd,有一个静态的精灵,我们称之为射手,还有一个移动的精灵,我们称之为移动者。射手有一定的距离开始看到敌人移动者,并且“知道”敌人的速度,然后尝试开火。射手的子弹始终以恒定的速度运行,因此如果说射手正好在移动者下方,而移动者正在向下移动,那么子弹的speedX将为0,而bullet.speedY将为-1。 - Philipp Lenssen
Deceze,这个塔始终会假设目标沿着直线且以恒定速度移动(实际上目标可能偶尔会碰撞某处并改变方向,但是塔不会计算这一点或者“知道”这一点,即使在边界情况下,塔也可能错过其目标,但这没关系)。 - Philipp Lenssen
5个回答

6

将目标精灵视为二维空间中的一条直线,其中:

A(time) = (sprite.positionX + sprite.speedX * time, sprite.positionX + sprite.speedX * time)

由于您的子弹速度恒定,您也知道:
bullet.speedX^2 + bullet.speedY^2 = bullet.definedSpeed^2

然后您还可以为子弹计算一条直线:
B(time) = (bullet.positionX + bullet.speedX * time, bullet.positionX + bullet.speedX * time)

你知道这两条线都会在某个地方相交:

A(time) = B(time)

接下来,你需要用给定的数值解决这些方程,并寻找time的最小值。


1
如果在实时进行线性方程组的解决(这可能取决于同时飞行的子弹数量),消耗了太多的CPU时间,你可能可以预先计算一组解,并仅在实时中解决角落案例。假设目标将以已知速度移动,请根据它们可能从八个不同的方向靠近(N,NE,E等)计算结果。当射手检测到目标时,只需查找您是否为具有该速度和轴承的目标预先计算了结果。如果没有,则计算它。 - bta
1
@bta:在编写代码之前,当然有道理将那些已知变量视为常数,在纸上解决那些方程。 - Christian

5

一些物理见解

1)对于目标是“点对象”

因此,您需要解决矢量方程:

Positionbullet [ time=t1 > t0 ] == Positiontarget [ time=t1 > t0 ] -- (Eq 1)

其中位置由运动(也是矢量)方程给出:

Positionobject [ t ] = Positionobject [ t0 ] + Speedobject * ( t - t0 )

现在,子弹能够到达目标的条件是方程式1有x和y的解。让我们写出x的方程式:

Xbullet [ t0 ] + SpeedXbullet * ( t - t0 ) = Xtarget [ t0 ] + SpeedXtarget * ( t - t0 )

因此,对于碰撞时间,我们有:

( tCollision - t0 ) = (xtarget [ t 0 ] - xbullet [ t0 ] ) / (SpeedXbullet - SpeedXtarget) -- (Eq 2)

由于我们需要t>t0的解,这意味着对于有拦截的情况而言:

Sign ( xtarget[ t0 ] - xbullet[ t0 ] ) = Sign ( SpeedXbullet - SpeedXtarget ) -- (Eq 3)

这告诉我们一个明显的事实,如果一个物体比另一个物体移动得更快,并且在同一方向上,它们最终会碰撞。

从方程式2中,您可以看到对于给定的SpeedXtarget,存在无限个t和SpeedXbullet的解(如其他答案所指出的),因此我认为您的规格说明不完整。

我猜(正如我在另一个答案中所述的评论)您正在考虑一种“塔防”类型的游戏,其中您的子弹有限的射程。
因此,您还需要另一个限制条件:
距离 [Positiontarget[tCollision - t0] - Positionbullet[t0]] < BulletRange -- (Eq 4)
这仍然允许无限的解,但由于目标可能超出范围,上限值由碰撞时间确定。
此外,距离由以下公式给出:
距离[v,u]= +Sqrt[(Vx-Ux)^2 + (Vx-Vy)^2]
因此,方程4变为:
(Xtarget[tCollision - t0] - Xbullet[t0])2 + (Ytarget[tCollision - t0] - Ybullet[t0])2 < BulletRange2 -- (Eq 5)
请注意,{Xbullet[t0], Ybullet[t0]}是塔的位置。
现在,在方程5中替换目标位置的值:
(Xtarget[t0] + SpeedXtarget * (t-t0) - Xbullet[t0])2 + (Ytarget[t0] + SpeedYtarget * (t-t0) - Ybullet[t0])2 < BulletRange2 -- (Eq 6)
称初始距离为:
Dxt0 = Xtarget[t0] - Xbullet[t0]

Dyt0 = Ytarget[t0] - Ybullet[t0]
方程6变为:

(Dtx0 + SpeedXtarget * (t-t0) )2 + (Dty0 + SpeedYtarget * (t-t0))2 < BulletRange2 -- (Eq 7)

这是一个要在 t-t0 中解决的二次方程。正解将给出我们允许的最大碰撞时间。之后目标将超出范围。

现在调用

Speedtarget 2 = SpeedXtarget 2 + SpeedYtarget 2

H = Dtx0 * SpeedXtarget + Dty0 * SpeedYtarget


TCollision Max = t0 - ( H +/- Sqrt ( BulletRange2 * Speedtarget 2 - H2 ) ) / Speedtarget 2

因此,您需要在此时间之前产生碰撞。平方根的符号应该被取为时间大于 t0

After you select an appropriate flying time for your bullet from the visual 
effects point of view, you can calculate the SpeedX and SpeedY for the bullet 
from  

SpeedXbullet = ( Xtarget [ t0 ] - Xbullet [ t0 ] ) / ( tCollision - t0 ) + SpeedXtarget

SpeedYbullet = ( Ytarget [ t0 ] - Ybullet [ t0 ] ) / ( tCollision - t0 ) + SpeedYtarget

2) 对于目标和塔的 "广阔物体"

现在,将目标替换为半径为R的圆形,问题就变得简单了。您所得到的是子弹的“扩展射程”。这个扩展范围就是R。

所以,将BulletRange替换为(BulletRange + R),您会得到新的最大允许碰撞时间方程式。

如果您还想考虑炮塔的半径,同样的考虑也适用,可以得到一个“双倍扩展范围”:

NewBulletRange = BulletRange + RTarget + RTower

无尽子弹射程

如果您决定某些特殊子弹不应有射程(和探测)限制,仍然会受到屏幕边界的约束。但这需要更复杂的计算,请在评论中提出需求,我会尝试解答。


3
使用向量可以使得相关的数学问题变得更加简单。Sylvester似乎是JavaScript中一个很有前途的向量实现,但是为了我的示例,我会编写自己的向量函数。我还会假设.x / .y是从左上角开始测量的。
// this is a "constant"  - representing 10px motion per "time unit"
var bulletSpeed = 10; 
// calculate the vector from our center to their center
var enemyVec = vec_sub(targetSprite.getCenter(), originSprite.getCenter());
// measure the "distance" the bullet will travel
var dist = vec_mag(enemyVec);
// adjust for target position based on the amount of "time units" to travel "dist"
// and the targets speed vector
enemyVec = vec_add(enemyVec, vec_mul(targetSprite.getSpeed(), dist/bulletSpeed));
// calculate trajectory of bullet
var bulletTrajectory = vec_mul(vec_normal(enemyVec), bulletSpeed);
// assign values
bulletSprite.speedX = bulletTrajectory.x;  
bulletSprite.speedY = bulletTrajectory.y;  

// functions used in the above example:

// getCenter and getSpeed return "vectors"
sprite.prototype.getCenter = function() { 
  return {
    x: this.x+(this.width/2), 
    y: this.y+(this.height/2) 
  }; 
};

sprite.prototype.getSpeed = function() { 
  return {
    x: this.speedX, 
    y: this.speedY 
  }; 
};

function vec_mag(vec) { // get the magnitude of the vector
  return Math.sqrt( vec.x * vec.x + vec.y * vec.y); 
 }
function vec_sub(a,b) { // subtract two vectors
  return { x: a.x-b.x, y: a.y-b.y };
}
function vec_add(a,b) { // add two vectors
  return { x: a.x + b.x, y: a.y + b.y };
}
function vec_mul(a,c) { // multiply a vector by a scalar
  return { x: a.x * c, y: a.y * c };
}
function vec_div(a,c) { // divide == multiply by 1/c
  return vec_mul(a, 1.0/c);
}
function vec_normal(a) { // normalize vector
  return vec_div(a, vec_mag(a)); 
}

运行得非常好。 (对于其他人复制此内容的提示,在一个地方,您需要将veg_mag更改为vec_mag。)我必须尝试一下找到正确的bulletSpeed值,然后子弹准确地击中了它们移动的目标。谢谢!! - Philipp Lenssen

1

计算射手和目标之间的距离:dist = sqrt((xt - xs)^2 + (yt - ys)^2)
将x和y距离除以上面的值:nx = (xt - xs)/dist; ny = (yt - ys)/dist;(向量标准化)
通过乘以一个因子来得到每个时间单位的n像素,即每个方向的速度。它应该在所需的方向上给出一个恒定的速度。


这完全没有考虑到目标的速度,因此除非目标直接朝向或远离射手,否则会错过。 - Chris

0

我假设目标沿着匀速直线运动。

如果子弹的方向速度都是可变的(即您试图计算子弹的speedXspeedY),则有无限多个解。

如果您设置一个固定的方向,那么您只需相交子弹和目标的两条直线。通过目标当前位置与交点之间的距离(和目标的速度),您可以计算出目标到达此交点所需的时间。

通过子弹原点与交点之间的距离(和之前计算的时间),您可以计算出子弹的速度。


规格说明中缺少某些内容。我猜想这就是子弹的射程。如果操作员指定了射程,那么解决方案的数量也将是无限的,但是子弹将有一个最小速度(即与目标在该射程交点处相交的速度)。 - Dr. belisarius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接