Tirer sur une cible en mouvement - Thêta Tau Tau - 18-02-2020
Dans plein de jeux on doit faire en sorte qu'un projectile atteigne une cible en mouvement. Par exemple dans un tower defense, un STR, un jeu de chars, de vaisseaux spaciaux etc.
Si le projectile se déplace très vite, comme un tir de fusil ou un laser, on peut directement viser la cible.
Mais dans beaucoup de cas le projectile se déplace plus lentement, et la cible ne sera plus là quand le projectile arrivera.
Ajoutons à ça qu'on peut vouloir que le projectile soit affecté par la gravité. Par exemple si le projectile est une flèche, un obus ou un boulet.
Une façon facile de régler le problème est de faire en sorte que le projectile modifie sa trajectoire en cour de route. C'est une bonne solution pour les missiles à tête chercheuse et les projectiles magiques, mais dans la majorité des cas ça ne conviens pas.
L'autre solution, que je vais présenté ici, est de calculer un point où sera la cible quand elle sera atteinte par le projectile. On vise donc devant la cible, et non où elle est.
En ignorant la gravité, et en considérant que le projectile et la cible ont un mouvement rectiligne uniforme, et qu'on connais la vitesse du projectile, on se retrouve avec une équation de second degré, pas trop compliqué à résoudre.
Si l'équation a des solution, une de ses solution sera à t<0 (dans le passé donc) et pourra être ignorée. On a donc soit 0 soit 1 solution.
Si on ajoute la gravité ça complique beaucoup l'équation. Mais on peut simplifier ça en compensant la gravité en augmentant la vitesse verticale du projectile qu'on envoi. Comme on connais déjà la durée de la trajectoire le calcul est trivial. C'est pas exact car la vitesse du projectile sera plus grande que celle qu'on a précisé. Mais à part si on veut que la balistique du jeu soit réaliste, c'est pas grave.
Résultat :
A noter :
- Si la cible change de direction, dans les virages par exemple, ça ne marche plus.
- Un projectile lent tiré à grande portée sera tiré très très haut ce qui peut ne pas être ce qu'on veut (cf. screen suivant). On peut sans doute améliorer le script en fixant la hauteur de trajectoire qu'on veut et en réduisant la gravité en conséquence.
Code source (C#/Unity) :
/// <summary>
/// Interception between an interceptor moving with parabolic trajectory (gravity only)
/// and a target moving in uniform translation
/// The trajectory has been simplified : first, the interception point is estimated as if there is no gravity
/// Then vertical speed is added to compensate for gravity
/// </summary>
public struct Interception
{
public bool isPossible;
public Vector3 point;
public Vector3 interceptorVelocity;
public float time;
private static Vector3 SwapAxis(Vector3 v, int swap)
{
return new Vector3(v[swap % 3], v[(swap+1) % 3], v[(swap+2) % 3]);
}
/// <param name="interceptorOrigin">Position of the interceptor at t=0</param>
/// <param name="targetOrigin">Position of the target at t=0</param>
/// <param name="interceptorSpeed">Speed of the interceptor. Doesn't take into account gravity compensation.
/// Therefore interceptorVelocity is greather than this value. </param>
/// <param name="targetVelocity">Velocity of the target (supposed constant)</param>
/// <param name="g">gravitational acceleration (in the y axis, positive value)</param>
public Interception(Vector3 interceptorOrigin, Vector3 targetOrigin, float interceptorSpeed, Vector3 targetVelocity, float g = 0f)
{
if(interceptorOrigin == targetOrigin)
{
isPossible = true;
point = interceptorOrigin;
interceptorVelocity = Random.onUnitSphere * interceptorSpeed;
time = 0;
return;
}
//Prevent division by 0 if interceptorOrigin.x == targetOrigin.x by swaping axis
int swap = Mathf.Abs(interceptorOrigin.x - targetOrigin.x) > 0.01f ? 0 :
Mathf.Abs(interceptorOrigin.y - targetOrigin.y) > 0.01f ? 1 : 2;
interceptorOrigin = SwapAxis(interceptorOrigin, swap);
targetOrigin = SwapAxis(targetOrigin, swap);
targetVelocity = SwapAxis(targetVelocity, swap);
//Solve trajectory equation
float alpha = (targetOrigin.y - interceptorOrigin.y) / (interceptorOrigin.x - targetOrigin.x);
float beta = (targetOrigin.z - interceptorOrigin.z) / (interceptorOrigin.x - targetOrigin.x);
float gamma = targetVelocity.y + alpha * targetVelocity.x;
float epsilon = targetVelocity.z + beta * targetVelocity.x;
float a = 1 + alpha * alpha + beta * beta;
float b = -2 * (alpha * gamma + beta * epsilon);
float c = gamma * gamma + epsilon * epsilon - interceptorSpeed * interceptorSpeed;
float delta = b * b - 4 * a * c;
if(delta < 0)//no solution
{
isPossible = false;
point = interceptorVelocity = Vector3.zero;
time = 0f;
return;
}
float deltaSqrt = Mathf.Sqrt(delta);
float dx = (-b + deltaSqrt) / (2 * a);
float t = (interceptorOrigin.x - targetOrigin.x)/(targetVelocity.x-dx);
if(t < 0)//Choose other solution
{
dx = (-b - deltaSqrt) / (2 * a);
t = (interceptorOrigin.x - targetOrigin.x) / (targetVelocity.x - dx);
}
float dy = (targetOrigin.y - interceptorOrigin.y) / t + targetVelocity.y;
float dz = (targetOrigin.z - interceptorOrigin.z) / t + targetVelocity.z;
//un-swap axis
interceptorVelocity = SwapAxis(new Vector3(dx, dy, dz), 3-swap);
interceptorOrigin = SwapAxis(interceptorOrigin, 3 - swap);
//Compensate for gravity
interceptorVelocity += Vector3.up * g * t / 2f;
isPossible = true;
time = t;
point = interceptorOrigin + interceptorVelocity * time;
}
|