2d игра: огонь по движущейся цели путем прогнозирования пересечения снаряда и блока
хорошо, все это происходит в хорошем и простом 2D мире... :)
предположим, что у меня есть статический объект A в положении Apos, и линейно движущийся объект B в Bpos с bVelocity, и боеприпасы со скоростью Avelocity...
Как я узнаю угол, который A должен стрелять, чтобы попасть в B, учитывая линейную скорость B и скорость боеприпасов A ?
прямо сейчас цель находится в текущем положении объекта, что означает, что к тому времени мой снаряд попадает туда, блок переместился на более безопасные позиции:)
11 ответов
сначала поверните оси так, чтобы AB был вертикальным (путем вращения)
теперь разделите вектор скорости B на компоненты x и y (скажем, Bx и By). Вы можете использовать это для вычисления X и y компонентов вектора, в который нужно стрелять.
B --> Bx
|
|
V
By
Vy
^
|
|
A ---> Vx
вам нужно Vx = Bx
и Sqrt(Vx*Vx + Vy*Vy) = Velocity of Ammo
.
Это должно дать вам вектор нужен в новой системе. Преобразуйте обратно в старую систему ,и вы закончите (сделав поворот в другой направление.)
я написал подпрограмму прицеливания для xtank некоторое время назад. Я попытаюсь объяснить, как я это сделал.
отказ от ответственности: я, возможно, сделал одну или несколько глупых ошибок где-нибудь здесь; я просто пытаюсь восстановить рассуждения с моими ржавыми математическими навыками. Однако сначала я перейду к погоне, так как это программирование Q&A вместо математического класса :-)
как это делается
это сводится к решению квадратичного уравнения форма:
a * sqr(x) + b * x + c == 0
обратите внимание, что по sqr
я имею в виду площади, в отличие от квадратного корня. Используйте следующие значения:
a := sqr(target.velocityX) + sqr(target.velocityY) - sqr(projectile_speed)
b := 2 * (target.velocityX * (target.startX - cannon.X)
+ target.velocityY * (target.startY - cannon.Y))
c := sqr(target.startX - cannon.X) + sqr(target.startY - cannon.Y)
теперь мы можем посмотреть на дискриминант, чтобы определить, есть ли у нас возможное решение.
disc := sqr(b) - 4 * a * c
если дискриминант меньше 0, забудьте о попадании в цель - ваш снаряд никогда не сможет попасть туда вовремя. В противном случае посмотрите на два решения-кандидата:
t1 := (-b + sqrt(disc)) / (2 * a)
t2 := (-b - sqrt(disc)) / (2 * a)
обратите внимание, что если disc == 0
затем t1
и t2
равны.
если нет других соображений, таких как промежуточные препятствия, просто выберите меньшее положительное значение. (Отрицательно t значения потребуют стрельбы назад во времени, чтобы использовать!)
заменить выбранный t
значение обратно в уравнения положения цели, чтобы получить координаты ведущей точки, вы должны быть направлены на:
aim.X := t * target.velocityX + target.startX
aim.Y := t * target.velocityY + target.startY
вывод
в момент времени T, снаряд должен находиться на расстоянии (евклидовом) от пушки, равном прошедшему времени, умноженному на скорость снаряда. Это дает уравнение для окружности, параметрическое за прошедшее время.
sqr(projectile.X - cannon.X) + sqr(projectile.Y - cannon.Y)
== sqr(t * projectile_speed)
аналогично, в момент времени T цель переместилась вдоль своего вектора на время, умноженное на ее скорость:
target.X == t * target.velocityX + target.startX
target.Y == t * target.velocityY + target.startY
снаряд может поразить цель, когда его расстояние от пушки соответствует расстоянию снаряда.
sqr(projectile.X - cannon.X) + sqr(projectile.Y - cannon.Y)
== sqr(target.X - cannon.X) + sqr(target.Y - cannon.Y)
замечательно! Заменяющий выражения для целевой.X и целевой.Y дает
sqr(projectile.X - cannon.X) + sqr(projectile.Y - cannon.Y)
== sqr((t * target.velocityX + target.startX) - cannon.X)
+ sqr((t * target.velocityY + target.startY) - cannon.Y)
подставляя другую сторону уравнения дает следующее:
sqr(t * projectile_speed)
== sqr((t * target.velocityX + target.startX) - cannon.X)
+ sqr((t * target.velocityY + target.startY) - cannon.Y)
... вычитая sqr(t * projectile_speed)
С обеих сторон и переворачивать его вокруг:
sqr((t * target.velocityX) + (target.startX - cannon.X))
+ sqr((t * target.velocityY) + (target.startY - cannon.Y))
- sqr(t * projectile_speed)
== 0
... теперь разрешите результаты возведения в квадрат подвыражений ...
sqr(target.velocityX) * sqr(t)
+ 2 * t * target.velocityX * (target.startX - cannon.X)
+ sqr(target.startX - cannon.X)
+ sqr(target.velocityY) * sqr(t)
+ 2 * t * target.velocityY * (target.startY - cannon.Y)
+ sqr(target.startY - cannon.Y)
- sqr(projectile_speed) * sqr(t)
== 0
... и группировать похожие термины ...
sqr(target.velocityX) * sqr(t)
+ sqr(target.velocityY) * sqr(t)
- sqr(projectile_speed) * sqr(t)
+ 2 * t * target.velocityX * (target.startX - cannon.X)
+ 2 * t * target.velocityY * (target.startY - cannon.Y)
+ sqr(target.startX - cannon.X)
+ sqr(target.startY - cannon.Y)
== 0
... тогда объедините их ...
(sqr(target.velocityX) + sqr(target.velocityY) - sqr(projectile_speed)) * sqr(t)
+ 2 * (target.velocityX * (target.startX - cannon.X)
+ target.velocityY * (target.startY - cannon.Y)) * t
+ sqr(target.startX - cannon.X) + sqr(target.startY - cannon.Y)
== 0
... давая стандартная квадратичное уравнение в t. Нахождение положительных вещественных нулей этого уравнения дает (ноль, один или два) возможные места попадания, что можно сделать с помощью квадратичной формулы:
a * sqr(x) + b * x + c == 0
x == (-b ± sqrt(sqr(b) - 4 * a * c)) / (2 * a)
+1 на отличный ответ здесь Джеффри Hantin по. Я погуглил и нашел решения, которые были либо слишком сложными, либо не были конкретно о случае, который меня интересовал (простой снаряд с постоянной скоростью в 2D-пространстве.) Это было именно то, что мне нужно для создания автономного решения JavaScript ниже.
один момент, который я бы добавил, заключается в том, что есть несколько особых случаев, которые вы должны наблюдать в дополнение к дискриминантному существу отрицательно:
- "a == 0": происходит, если цель и снаряд движутся с одинаковой скоростью. (решение линейное, а не квадратичное)
- "a == 0 и b == 0": если цель и снаряд неподвижны. (нет решения, если c == 0, т. е. src & dst-одна и та же точка.)
код:
/**
* Return the firing solution for a projectile starting at 'src' with
* velocity 'v', to hit a target, 'dst'.
*
* @param Object src position of shooter
* @param Object dst position & velocity of target
* @param Number v speed of projectile
* @return Object Coordinate at which to fire (and where intercept occurs)
*
* E.g.
* >>> intercept({x:2, y:4}, {x:5, y:7, vx: 2, vy:1}, 5)
* = {x: 8, y: 8.5}
*/
function intercept(src, dst, v) {
var tx = dst.x - src.x,
ty = dst.y - src.y,
tvx = dst.vx,
tvy = dst.vy;
// Get quadratic equation components
var a = tvx*tvx + tvy*tvy - v*v;
var b = 2 * (tvx * tx + tvy * ty);
var c = tx*tx + ty*ty;
// Solve quadratic
var ts = quad(a, b, c); // See quad(), below
// Find smallest positive solution
var sol = null;
if (ts) {
var t0 = ts[0], t1 = ts[1];
var t = Math.min(t0, t1);
if (t < 0) t = Math.max(t0, t1);
if (t > 0) {
sol = {
x: dst.x + dst.vx*t,
y: dst.y + dst.vy*t
};
}
}
return sol;
}
/**
* Return solutions for quadratic
*/
function quad(a,b,c) {
var sol = null;
if (Math.abs(a) < 1e-6) {
if (Math.abs(b) < 1e-6) {
sol = Math.abs(c) < 1e-6 ? [0,0] : null;
} else {
sol = [-c/b, -c/b];
}
} else {
var disc = b*b - 4*a*c;
if (disc >= 0) {
disc = Math.sqrt(disc);
a = 2*a;
sol = [(-b-disc)/a, (-b+disc)/a];
}
}
return sol;
}
Джеффри Hantin есть хорошее решение для этой проблемы, хотя его деривации является чрезмерно сложным. Вот более чистый способ получения его с некоторым результирующим кодом внизу.
я буду использовать x.y для представления векторного точечного продукта, и если векторное количество в квадрате, это означает, что я ставлю его с собой.
origpos = initial position of shooter
origvel = initial velocity of shooter
targpos = initial position of target
targvel = initial velocity of target
projvel = velocity of the projectile relative to the origin (cause ur shooting from there)
speed = the magnitude of projvel
t = time
мы знаем, что положение снаряда и цели в отношении t
время можно описать с некоторыми уравнения.
curprojpos(t) = origpos + t*origvel + t*projvel
curtargpos(t) = targpos + t*targvel
мы хотим, чтобы они были равны друг другу в какой-то точке (точке пересечения), поэтому давайте установим их равными друг другу и решим для свободной переменной, projvel
.
origpos + t*origvel + t*projvel = targpos + t*targvel
turns into ->
projvel = (targpos - origpos)/t + targvel - origvel
давайте забудем о понятии происхождения и целевой позиции/скорости. Вместо этого давайте работать в относительных терминах, поскольку движение одной вещи относительно другой. В этом случае то, что у нас сейчас есть, это relpos = targetpos - originpos
и relvel = targetvel - originvel
projvel = relpos/t + relvel
мы не знаем, что projvel
есть, но мы знаем, что мы хотим projvel.projvel
будет равна speed^2
, так что мы будем квадрат с обеих сторон, и мы получим
projvel^2 = (relpos/t + relvel)^2
expands into ->
speed^2 = relvel.relvel + 2*relpos.relvel/t + relpos.relpos/t^2
теперь мы видим, что единственная свободная переменная-это время,t
, а затем мы будем использовать t
решить projvel
. Мы решим для t
С квадратичной формулы. Сначала разделите его на a
, b
и c
, а затем решить для корней.
перед решением, однако, помните, что мы хотим лучшее решение, где t
это маленький, но мы должны убедиться, что t
не отрицательно (вы не можете ударить что-то в прошлом)
a = relvel.relvel - speed^2
b = 2*relpos.relvel
c = relpos.relpos
h = -b/(2*a)
k2 = h*h - c/a
if k2 < 0, then there are no roots and there is no solution
if k2 = 0, then there is one root at h
if 0 < h then t = h
else, no solution
if k2 > 0, then there are two roots at h - k and h + k, we also know r0 is less than r1.
k = sqrt(k2)
r0 = h - k
r1 = h + k
we have the roots, we must now solve for the smallest positive one
if 0<r0 then t = r0
elseif 0<r1 then t = r1
else, no solution
теперь, если у нас есть t
значением, мы можем подключить t
обратно в исходное уравнение и решить для projvel
projvel = relpos/t + relvel
теперь, чтобы стрелять снаряд, результирующее глобальное положение и скорость для снаряда
globalpos = origpos
globalvel = origvel + projvel
и ты молодец!
моя реализация моего решения в Lua, где vec*vec представляет векторный точечный продукт:
local function lineartrajectory(origpos,origvel,speed,targpos,targvel)
local relpos=targpos-origpos
local relvel=targvel-origvel
local a=relvel*relvel-speed*speed
local b=2*relpos*relvel
local c=relpos*relpos
if a*a<1e-32 then--code translation for a==0
if b*b<1e-32 then
return false,"no solution"
else
local h=-c/b
if 0<h then
return origpos,relpos/h+targvel,h
else
return false,"no solution"
end
end
else
local h=-b/(2*a)
local k2=h*h-c/a
if k2<-1e-16 then
return false,"no solution"
elseif k2<1e-16 then--code translation for k2==0
if 0<h then
return origpos,relpos/h+targvel,h
else
return false,"no solution"
end
else
local k=k2^0.5
if k<h then
return origpos,relpos/(h-k)+targvel,h-k
elseif -k<h then
return origpos,relpos/(h+k)+targvel,h+k
else
return false,"no solution"
end
end
end
end
Ниже приведен полярный координатный код наведения в C++.
для использования с прямоугольными координатами вам нужно сначала преобразовать относительную координату целей в угол / расстояние, а скорость целей x/y в угол/Скорость.
вход "скорость" - это скорость снаряда. Единицы скорости и targetSpeed не имеют значения, так как при расчете используется только отношение скоростей. Выходной угол снаряда должен быть обстрелян и расстояние до места столкновения.
алгоритм из исходного кода доступен вhttp://www.turtlewar.org/ .
// C++
static const double pi = 3.14159265358979323846;
inline double Sin(double a) { return sin(a*(pi/180)); }
inline double Asin(double y) { return asin(y)*(180/pi); }
bool/*ok*/ Rendezvous(double speed,double targetAngle,double targetRange,
double targetDirection,double targetSpeed,double* courseAngle,
double* courseRange)
{
// Use trig to calculate coordinate of future collision with target.
// c
//
// B A
//
// a C b
//
// Known:
// C = distance to target
// b = direction of target travel, relative to it's coordinate
// A/B = ratio of speed and target speed
//
// Use rule of sines to find unknowns.
// sin(a)/A = sin(b)/B = sin(c)/C
//
// a = asin((A/B)*sin(b))
// c = 180-a-b
// B = C*(sin(b)/sin(c))
bool ok = 0;
double b = 180-(targetDirection-targetAngle);
double A_div_B = targetSpeed/speed;
double C = targetRange;
double sin_b = Sin(b);
double sin_a = A_div_B*sin_b;
// If sin of a is greater than one it means a triangle cannot be
// constructed with the given angles that have sides with the given
// ratio.
if(fabs(sin_a) <= 1)
{
double a = Asin(sin_a);
double c = 180-a-b;
double sin_c = Sin(c);
double B;
if(fabs(sin_c) > .0001)
{
B = C*(sin_b/sin_c);
}
else
{
// Sin of small angles approach zero causing overflow in
// calculation. For nearly flat triangles just treat as
// flat.
B = C/(A_div_B+1);
}
// double A = C*(sin_a/sin_c);
ok = 1;
*courseAngle = targetAngle+a;
*courseRange = B;
}
return ok;
}
вот пример, где я разработал и реализовал решение проблемы предсказательного таргетинга с использованием рекурсивного алгоритма:http://www.newarteest.com/flash/targeting.html
Мне придется опробовать некоторые из других представленных решений, потому что кажется более эффективным вычислить его за один шаг, но решение, которое я придумал, состояло в том, чтобы оценить целевую позицию и вернуть этот результат в алгоритм, чтобы сделать новую более точную оценку, повторяется несколько раз.
для первой оценки я "стреляю" по текущей позиции цели, а затем использую тригонометрию, чтобы определить, где будет цель, когда выстрел достигнет позиции, в которую стреляли. Затем на следующей итерации я "стреляю" в эту новую позицию и определяю, где цель будет на этот раз. После 4 повторов я вам в пиксель точностью.
Я только что взломал эту версию для прицеливания в 2d-пространстве, я еще не тестировал ее очень тщательно, но, похоже, работает. Идея такая:
создайте вектор, перпендикулярный вектору, указывающему от дула к цели. Чтобы произошло столкновение, скорости цели и снаряда вдоль этого вектора (оси) должны быть одинаковыми! Используя довольно простой Косинус, я пришел к этому коду:
private Vector3 CalculateProjectileDirection(Vector3 a_MuzzlePosition, float a_ProjectileSpeed, Vector3 a_TargetPosition, Vector3 a_TargetVelocity)
{
// make sure it's all in the horizontal plane:
a_TargetPosition.y = 0.0f;
a_MuzzlePosition.y = 0.0f;
a_TargetVelocity.y = 0.0f;
// create a normalized vector that is perpendicular to the vector pointing from the muzzle to the target's current position (a localized x-axis):
Vector3 perpendicularVector = Vector3.Cross(a_TargetPosition - a_MuzzlePosition, -Vector3.up).normalized;
// project the target's velocity vector onto that localized x-axis:
Vector3 projectedTargetVelocity = Vector3.Project(a_TargetVelocity, perpendicularVector);
// calculate the angle that the projectile velocity should make with the localized x-axis using the consine:
float angle = Mathf.Acos(projectedTargetVelocity.magnitude / a_ProjectileSpeed) / Mathf.PI * 180;
if (Vector3.Angle(perpendicularVector, a_TargetVelocity) > 90.0f)
{
angle = 180.0f - angle;
}
// rotate the x-axis so that is points in the desired velocity direction of the projectile:
Vector3 returnValue = Quaternion.AngleAxis(angle, -Vector3.up) * perpendicularVector;
// give the projectile the correct speed:
returnValue *= a_ProjectileSpeed;
return returnValue;
}
Я видел много способов решить эту проблему математически, но это был компонент, относящийся к проекту, который мой класс должен был делать в средней школе, и не у всех в этом классе программирования был фон с исчислением или даже векторами, поэтому я создал способ решить эту проблему с помощью более программного подхода. Точка пересечения будет точной, хотя она может попасть на 1 кадр позже, чем в математическом вычисления.
считаем:
S = shooterPos, E = enemyPos, T = targetPos, Sr = shooter range, D = enemyDir
V = distance from E to T, P = projectile speed, Es = enemy speed
в стандартной реализации этой задачи [S, E,P,Es, D] все Даны, и вы решаете либо найти T, либо угол, под которым стрелять, чтобы вы попали в T в нужное время.
основным аспектом этого метода решения проблемы является рассмотрение дальности действия стрелка как окружности, охватывающей все возможные точки, которые могут быть сняты в любой момент времени. Радиус этой окружности равен кому:
Sr = P*time
где время вычисляется как итерация цикла.
таким образом, чтобы найти расстояние, которое проходит враг, учитывая итерацию времени, мы создаем вектор:
V = D*Es*time
теперь, чтобы фактически решить проблему, мы хотим найти точку, в которой расстояние от цели (T) до нашего стрелка (ов) меньше, чем диапазон нашего стрелка (Sr). Вот несколько реализации псевдокод этого уравнения.
iteration = 0;
while(TargetPoint.hasNotPassedShooter)
{
TargetPoint = EnemyPos + (EnemyMovementVector)
if(distanceFrom(TargetPoint,ShooterPos) < (ShooterRange))
return TargetPoint;
iteration++
}
Я сделал общедоступную функцию Unity C# здесь:
http://ringofblades.com/Blades/Code/PredictiveAim.cs
Это для 3D, но вы можете легко изменить это для 2D, заменив Vector3s на Vector2s и используя свою ось вниз для гравитации, если есть гравитация.
в случае, если теория вас интересует, я прохожу через вывод математики here:
http://www.gamasutra.com/blogs/KainShin/20090515/83954/Predictive_Aim_Mathematics_for_AI_Targeting.php
в принципе, концепция пересечения на самом деле не нужна здесь, поскольку вы используете движение снаряда, вам просто нужно ударить под определенным углом и создать экземпляр во время съемки, чтобы вы получили точное расстояние вашей цели от источника, а затем, как только у вас есть расстояние, вы можете рассчитать соответствующую скорость, с которой он должен стрелять, чтобы поразить цель.
следующая ссылка делает концепцию ясной и считается полезной, может помощь: движение снаряда, чтобы всегда поражать движущуюся цель
Я схватил одно из решений отсюда, но ни одно из них не учитывает движение стрелка. Если ваш стрелок движется, вы можете принять это во внимание (поскольку скорость стрелка должна быть добавлена к скорости вашей пули при стрельбе). На самом деле все, что вам нужно сделать, это вычесть скорость вашего стрелка из скорости цели. Поэтому, если вы используете код broofa выше (который я бы рекомендовал), измените строки
tvx = dst.vx;
tvy = dst.vy;
to
tvx = dst.vx - shooter.vx;
tvy = dst.vy - shooter.vy;
и вы должны быть готовы.