لعبة 2D: حريق هدف متحرك من خلال التنبؤ تقاطع القذائف والوحدة
-
20-09-2019 - |
سؤال
حسنا، كل هذا يحدث في عالم لطيف وبسيط 2D ... :)
لنفترض أن لدي كائن ثابت في موقع APOS في الموضع، وكائن متحرك خطي B في BPOS مع أوراق الاستلام، وجولة الذخيرة مع سرعة Avelocity ...
كيف يمكنني معرفة الزاوية التي يجب أن تطلق النار عليها، لضرب ب، مع الأخذ في الاعتبار السرعة الخطية B وسرعة الذخيرة؟
الآن الهدف في الموضع الحالي للكائن، مما يعني أنه بحلول الوقت الذي يحصل فيه القذيفة على هناك الوحدة قد انتقلت إلى مواقع أكثر أمانا :)
المحلول
قم أولا بتدوير المحاور بحيث يكون AB عموديا (عن طريق القيام بالتناوب)
الآن، قم بتقسيم متجه السرعة من B إلى مكونات X و Y (قل BX واو). يمكنك استخدام هذا لحساب مكونات X و Y من المتجه الذي تحتاجه لإطلاق النار عليه.
B --> Bx
|
|
V
By
Vy
^
|
|
A ---> Vx
انت تحتاج Vx = Bx
و Sqrt(Vx*Vx + Vy*Vy) = Velocity of Ammo
.
هذا يجب أن يمنحك المتجه الذي تحتاجه في النظام الجديد. تحويل إلى النظام القديم وأنت يتم (عن طريق القيام بالتناوب في الاتجاه الآخر).
نصائح أخرى
كتبت روتين تهدف ل Xtank. لفترة سبقت. سأحاول وضع كيف فعلت ذلك.
عدم اعطاء رأي: ربما أكون قد ارتكبت أخطاء واحدة أو أكثر في أي مكان هنا؛ أنا فقط أحاول إعادة بناء المنطق مع مهاراتي الرياضيات الصدئة. ومع ذلك، سأقطع إلى المطاردة أولا، لأن هذا هو برمجة سؤال وجواب بدلا من فئة الرياضيات :-)
كيف افعلها
إنه يتلخص لحل المعادلة التربيعية للنموذج:
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
القيمة مرة أخرى في معادلات موقف الهدف للحصول على إحداثيات النقطة الرائدة يجب أن تهدف إلى:
aim.X := t * target.velocityX + target.startX
aim.Y := t * target.velocityY + target.startY
الاشتقاق
في الوقت المناسب، يجب أن يكون القذيفة مسافة (Euclidean) المسافة من المدفع يساوي الوقت المنقضي مضروب من سرعة القذيفة. هذا يعطي معادلة لدائرة، حدوث حدودي في الوقت المنقضي.
sqr(projectile.X - cannon.X) + sqr(projectile.Y - cannon.Y)
== sqr(t * projectile_speed)
وبالمثل، في الوقت الراهن، انتقل الهدف على طول ناقلها بواسطة الوقت المضروب في سرعته:
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)
رائع! استبدال التعبيرات للمستهدف. يعطي الهدف
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
... إعطاء المعادلة التربيعية القياسية في ب. وبعد العثور على الأصفار الحقيقية الإيجابية لهذه المعادلة يعطي المواقع (صفر أو واحد أو واحد أو سنتين)، والتي يمكن القيام بها مع الصيغة التربيعية:
a * sqr(x) + b * x + c == 0
x == (-b ± sqrt(sqr(b) - 4 * a * c)) / (2 * a)
+1 على إجابة جيفري هانتين الممتازة هنا. أنا غاضب حولها وعثرت على حلول كانت إما معقدة للغاية أو غير محددة على وجه التحديد حول هذا القضية التي كنت مهتما بها (قذيفة السرعة المستمرة البسيطة في مساحة ثنائية الأبعاد.) كان له بالضبط ما أحتاجه لإنتاج حل جافا سكريبت ذاتيا أدناه.
النقطة الوحيدة التي سأضيفها هي أن هناك حالات خاصة زوجين عليك أن تشاهدها بالإضافة إلى كونها سلبية للغاية:
- "A == 0": يحدث إذا كان الهدف والقذيفة يسافرون بنفس السرعة. (الحل خطي، وليس من الدرجة الثانية)
- "A == 0 و B == 0": إذا كانت كل من الهدف والقذيفة ثابتة. (لا يوجد حل ما لم تكن ج == 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;
}
لدى جيفري هانتين حل لطيف لهذه المشكلة، على الرغم من أن اشتقاقه معقد للغاية. إليك طريقة نظافة لاستكشافها مع بعض الكود الناتج في الأسفل.
سأؤدي باستخدام XY لتمثيل منتج Vector DOT، وإذا تربيع كمية متجه، فهذا يعني أنني تنقيطها بنفسها.
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 منتج Vector DOT:
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 سرعة الزاوية / السرعة.
المدخلات "السرعة" هي سرعة القذيفة. وحدات السرعة والهدفين غير صحيحة، حيث يتم استخدام نسبة السرعات فقط في الحساب. الإخراج هو الزاوية يجب أن يتم إطلاق القذيفة والمسافة إلى نقطة الاصطدام.
الخوارزمية هي من شفرة المصدر المتاحة في 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 تكرر، أحصل على ضمن دقة بكسل.
لقد اخترقت للتو هذا الإصدار للحصول على مساحة ثنائية الأبعاد، لم أقم باختبارها جيدا تماما ولكن يبدو أنها تعمل. الفكرة وراء ذلك هي:
قم بإنشاء ناقلات عموديا على الموجه الذي يشير من كمامة إلى الهدف. للحصول على تصادم يحدث، يجب أن تكون سرية الهدف والقذيفة على طول هذا المتجه (المحور) هو نفسه! باستخدام أشياء جيب التمام البسيطة إلى حد ما وصلت إلى هذا الرمز:
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;
}
لقد رأيت العديد من الطرق لحل هذه المشكلة رياضيا، ولكن هذا كان مكونا ذو صلة بمشروع مطلب من صفي أن أفعله في المدرسة الثانوية، وليس كل شخص في فئة البرمجة هذه خلفية مع حساب التفاضل والتكامل، أو حتى ناقلات لهذه المسألة ، لذلك قمت بإنشاء طريقة لحل هذه المشكلة مع أكثر من نهج البرمجة. ستكون نقطة التقاطع دقيقة، على الرغم من أنها قد تصل إلى إطار واحد في وقت لاحق من الحسابات الرياضية.
يعتبر:
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] كلها جيفينات وأنت تحل إما للعثور على ر أو الزاوية التي لا تطلقها حتى تطلق النار على التوقيت المناسب.
الجانب الرئيسي لهذه الطريقة لحل المشكلة هو النظر في مجموعة مطلق النار كدائرة تشمل جميع النقاط الممكنة التي يمكن إطلاق النار عليها في أي وقت معين. دائرة نصف قطر هذه الدائرة تساوي:
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++
}
أنا جعلت وحدة المجال العام C # وظيفة هنا:
http://ringoflades.com/blades/code/predictiveaim.cs.
إنه ل 3 ثلاثي الأبعاد، ولكن يمكنك بسهولة تعديل هذا لمدة 2D عن طريق استبدال المتجهات 3S مع Vector2s واستخدام محورك المنخفض من الاختيار للجاذبية إذا كان هناك جاذبية.
في حالة الاهتمام بالنظرية، أمشي عبر اشتقاق الرياضيات هنا:
http://www.gamasutra.com/blogs/kainshin/20090515/83954/predictive_aim_mathematics_for_ai_targeting.php.
أساسا، لم يكن هناك حاجة إلى مفهوم التقاطع هنا بالفعل، بقدر ما تستخدم حركة قذيفة، تحتاج فقط إلى الضغط على زاوية معينة وثباتة في وقت التصوير حتى تحصل على المسافة الدقيقة لهدفك من المصدر ثم بمجرد أن تكون لديك المسافة، يمكنك حساب السرعة المناسبة التي يجب أن تسطلق بها من أجل ضرب الهدف.
الرابط التالي يجعل مفهوم Teh واضح ويعتبر مفيدا، قد يساعد:حركة قذيفة تصل دائما إلى هدف متحرك
أمسكت بأحد الحلول من هنا، لكن لا أحد منهم يأخذ في الاعتبار حركة مطلق النار. إذا كان مطلق النار الخاص بك يتحرك، فقد ترغب في أخذ ذلك في الاعتبار (حيث يجب إضافة سرعة مطلق النار إلى سرعة رصاصة عند إطلاق النار). حقا كل ما عليك فعله هو طرح سرعة مطلق النار الخاص بك من سرعة الهدف. لذلك إذا كنت تستخدم رمز Briooda أعلاه (الذي أوصي به)، فقم بتغيير الخطوط
tvx = dst.vx;
tvy = dst.vy;
ل
tvx = dst.vx - shooter.vx;
tvy = dst.vy - shooter.vy;
ويجب أن تكون كل مجموعة.