Solution Unity pour atteindre les cibles en mouvement

Ce que vous allez créer

Lors du développement de jeux qui impliquent un élément d’action, nous devons souvent trouver un moyen de se heurter à une cible en mouvement. De tels scénarios peuvent généralement être appelés un problème de 'frappe d'une cible en mouvement'. Ceci est particulièrement important dans les jeux de défense de tour ou de commande de missile comme les jeux. Nous aurons peut-être besoin de créer une IA ou un algorithme capable de comprendre le mouvement de l'ennemi et de tirer dessus. 

Voyons comment nous pouvons résoudre ce problème particulier, cette fois dans Unity.

1. Le jeu de commande de missile

Pour ce tutoriel particulier, nous allons considérer un jeu de commande de missile. Dans le jeu, nous avons une tourelle au sol qui tire des missiles sur un astéroïde entrant. Nous ne devrions pas permettre à l'astéroïde de heurter le sol. 

Le jeu est basé sur la frappe, où nous devons appuyer pour viser la tourelle. Avec une assistance humaine, les mécanismes de jeu sont assez simples car la tourelle a juste besoin de viser et de tirer. Mais imaginez si la tourelle doit automatiquement tirer sur les astéroïdes entrants. 

Les défis de l'IA automatique

La tourelle doit savoir combien d'astéroïdes s'approchent du sol. Une fois que tous les astéroïdes se rapprochent, il doit ensuite procéder à une analyse de la menace pour déterminer lequel cibler. Un astéroïde lent est une menace moindre qu'un astéroïde rapide. Un astéroïde plus proche du sol constitue également une menace imminente.. 

Ces problèmes peuvent être résolus en comparant la vitesse et la position des astéroïdes entrants. Une fois que nous avons déterminé lequel cibler, nous arrivons au problème le plus compliqué. Quand la tourelle devrait-elle tirer? A quel angle doit-il tirer? Quand le missile doit-il exploser après le tir? La troisième question est pertinente car l’explosion de missile peut également détruire l’astéroïde et avoir un rayon d’effet plus important..

Pour simplifier le problème, la tourelle peut décider de tirer immédiatement. Ensuite, nous devons seulement déterminer l’angle de tir et la distance de détonation. En outre, il peut arriver que l'astéroïde ait déjà dépassé la zone où il pourrait être touché, ce qui signifie qu'il n'y a pas de solution!

Vous devriez télécharger la source d'unité fournie avec ce tutoriel pour voir la solution en action. Nous verrons comment nous obtenons cette solution.

2. La solution

Nous allons faire un petit rappel de nos mathématiques au lycée afin de trouver la solution. C'est très simple et implique la résolution d'une équation quadratique. Une équation quadratique ressemble à axˆ2 + bx + c = 0, où X est la variable à trouver et se produit avec la plus grande puissance de 2. 

Analyser le problème

Essayons de représenter notre problème schématiquement. 

La ligne verte indique le chemin prévu que suivra l'astéroïde. Comme nous avons affaire à un mouvement uniforme, l'astéroïde se déplace à vitesse constante. Notre tourelle devra faire pivoter et tirer le missile le long du chemin bleu pour qu'il puisse entrer en collision avec l'astéroïde à un moment ultérieur..

Pour un mouvement uniforme, la distance parcourue par un objet est le produit du temps et de sa vitesse, c'est-à-dire. D = T x S, où représente la distance, T est le temps de voyager , et S est la vitesse de déplacement. En supposant que notre astéroïde et les missiles entrent définitivement en collision, nous pouvons trouver la distance de la ligne bleue suivie par le missile en termes de temps t. Dans le même temps t, notre astéroïde atteindra également la même position. 

Essentiellement, dans le même temps t, l'astéroïde atteindra la position de collision à partir de sa position actuelle et le missile atteindra également la même position de collision dans le même temps t. Donc à l'heure t, l'astéroïde et le missile seraient tous deux à la même distance de la tourelle qu'ils se heurteraient l'un à l'autre.

Entrez Math

Nous pouvons assimiler la distance de la tourelle à l'astéroïde et au missile à ce moment futur t afin de dériver notre équation du second degré avec la variable t. Considérons deux points sur un plan à deux dimensions avec des coordonnées (x1, y1) et (x2, y2). La distance entre eux peuvent être calculés en utilisant l'équation ci-dessous.

Dˆ2 = (x2-x1) ˆ2 + (y2-y1) ˆ2

Si nous notons la position de la tourelle comme (Tx, Ty), la vitesse du missile comme s et la position de collision inconnue comme (X, Y), alors l'équation ci-dessus peut être récrite comme suit:

Dˆ2 = (X-Tx) ˆ2 + (Y-Ty) ˆ2; D = s * t;

t est le temps pris par le missile pour parcourir la distance . En comparant les deux, nous obtenons notre première équation pour les inconnus X et Y avec un autre inconnu t.

sˆ2 * tˆ2 = (X-Tx) ˆ2 + (Y-Ty) ˆ2

Nous savons que l'astéroïde atteint également le même point de collision (X, Y) dans le même temps t, et nous avons les équations suivantes en utilisant les composantes horizontale et verticale du vecteur vitesse de l'astéroïde. Si la vitesse de l’astéroïde peut être indiquée par (Vx, Vy) et la position actuelle en tant que (Ax, Ay), alors l'inconnu X et Y peut être trouvé comme ci-dessous.

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

Leur substitution dans l’équation précédente nous donne une équation quadratique avec une seule inconnue t

sˆ2 * tˆ2 = ((t * Vx + Ax) -Tx) ˆ2 + ((t * Vy + Ay) -Ty) ˆ2;

Expansion et combinaison de termes similaires:

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 * Axe; (Vxˆ2 + Vyˆ2 - sˆ2) * tˆ2 + 2 * (Vx * (Ax - Tx) + Vy * (Ay - Ty)) * t + (Ay - Ty) ˆ2 + (Ax - Tx) ˆ2 = 0;

Représentant le pouvoir de deux comme ˆ2 et le symbole de multiplication comme * peut avoir fait ressembler ce qui précède à des hiéroglyphes, mais cela revient essentiellement à l’équation quadratique finale axˆ2 + bx + c = 0, où X est la variable t, une est Vxˆ2 + Vyˆ2 - sˆ2, b est 2 * (Vx * (Ax - Tx) + Vy * (Ay - Ty)), et c est (Ay - Ty) ˆ2 + (Ax - Tx) ˆ2. Nous avons utilisé les équations ci-dessous dans la dérivation.

(a + b) ˆ2 = aˆ2 + 2 * a * b + bˆ2; (a-b) ˆ2 = aˆ2 - 2 * a * b + bˆ2;

Résoudre l'équation quadratique

Pour résoudre une équation du second degré, nous devons calculer le discriminant en utilisant la formule:

D = bˆ2 - 4 * a * c;

Si le discriminant est inférieur à 0 alors il n'y a pas de solution, si c'est 0 alors il y a une solution unique, et s'il s'agit d'un nombre positif, il y a deux solutions. Les solutions sont calculées à l'aide des formules données ci-dessous.

t1 = (-b + sqrt (D)) / 2 * a; t2 = (-b - sqrt (D)) / 2 * a;

En utilisant ces formules, nous pouvons trouver des valeurs pour le futur t quand la collision aura lieu. Une valeur négative pour t signifie que nous avons manqué l'occasion de tirer. Les inconnus X et Y peut être trouvé en substituant la valeur de t dans leurs équations respectives.

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

Une fois que nous connaissons le point de collision, nous pouvons faire pivoter notre tourelle pour tirer le missile, qui heurterait définitivement l'astéroïde en mouvement après t les secondes.

3. Mise en œuvre dans l'unité

Pour l'exemple de projet Unity, j'ai utilisé la fonctionnalité de création de sprite de la dernière version d'Unity pour créer les actifs réservés. Ceci peut être consulté avec Créer> Sprites> comme indiqué ci-dessous.

Nous avons un script de jeu nommé MissileCmdAI qui est attaché à la caméra de la scène. Il contient la référence au sprite de la tourelle, au préfabriqué pour missile et au préfabriqué en astéroïde. j'utilise SimplePool by quill18 pour maintenir les pools d'objets pour les missiles et les astéroïdes. On peut le trouver sur GitHub. Il existe des scripts de composant pour missile et astéroïde qui sont attachés à leur préfabriqué et gèrent leur mouvement une fois publiés..

Les astéroïdes

Les astéroïdes sont engendrés aléatoirement à une hauteur fixe mais à une position horizontale aléatoire et sont projetés à une position horizontale aléatoire sur le sol avec une vitesse aléatoire. La fréquence de ponte des astéroïdes est contrôlée à l'aide d'un AnimationCurve. le SpawnAsteroid méthode dans le MissileCmdAI le script ressemble à celui ci-dessous:

void SpawnAsteroid () GameObject asteroid = SimplePool.Spawn (asteroidPrefab, Vector2.one, Quaternion.identity); Astéroïde asteroidScript = asteroid.GetComponent(); asteroidScript.Launch (); SetNextSpawn (); 

le lancement méthode dans le Astéroïde la classe est montrée ci-dessous.

public void Launch () // place l'astéroïde en haut avec x aléatoire et le lance en bas avec x xbl = Camera.main.ScreenToWorldPoint (new Vector2 (10,0)); br = Camera.main.ScreenToWorldPoint (nouveau Vector2 (Screen.width-20,0)); tl = Camera.main.ScreenToWorldPoint (nouveau Vector2 (0, Screen.height)); tr = Camera.main.ScreenToWorldPoint (nouveau Vector2 (Screen.width, Screen.height)); transform.localScale = Vector2.one * (0,2f + plage aléatoire (0,2f, 0,8f)); asteroidSpeed ​​= Random.Range (asteroidMinSpeed, asteroidMaxSpeed); astéroïdePos.x = Plage aléatoire (tl.x, tr.x); astéroïdePos.y = tr.y + 1; destination.y = bl.y; destination.x = plage aléatoire (bl.x, br.x); Vecteur vitesse2 = asteroidSpeed ​​* ((destination-asteroidPos) .normalized); transform.position = asteroidPos; asteroidRb.velocity = vélocité; // définit une vitesse sur un corps rigide pour le mettre en mouvement deployDistance = Vector3.Distance (asteroidPos, destination); // après avoir parcouru autant de distance, revenir au pool void Update () if (Vector2. Distance (transform.position, astéroïdePos)> deployDistance) // une fois que nous avons parcouru la distance définie, retournez dans le pool ReturnToPool ();  void OnTriggerEnter2D (projectile Collider2D) if (projectile.gameObject.CompareTag ("missile")) // contrôle la collision avec le missile, retourne au pool ReturnToPool (); 

Comme on le voit dans le Mettre à jour méthode, une fois que l'astéroïde a parcouru la distance prédéterminée jusqu'au sol, deployDistance, il reviendrait à son pool d'objets. Essentiellement, cela signifie qu’il est entré en collision avec le sol. Il en serait de même en cas de collision avec le missile.

Le ciblage

Pour que le ciblage automatique fonctionne, nous devons fréquemment appeler la méthode correspondante pour rechercher et cibler l'astéroïde entrant. Ceci est fait dans le MissileCmdAI script dans son Début méthode.

InvokeRepeating ("FindTarget", 1, aiPollTime); // définition de l'interrogation du code ai

le Trouver cible méthode parcourt tous les astéroïdes présents dans la scène pour rechercher les astéroïdes les plus proches et les plus rapides. Une fois trouvé, il appelle ensuite le AcquérirTargetLock méthode pour appliquer nos calculs.

void FindTarget () // trouve l'astéroïde le plus proche et le plus proche GameObject [] aArr = GameObject.FindGameObjectsWithTag ("asteroid"); GameObjectest le plus procheAsteroid = null; Astéroïde le plus rapideAstéroïde = null; Astéroïde astéroïde; foreach (GameObject va dans aArr) if (go.transform.position.y(); if (mostAsteroid == null) // trouve le plus rapideASTAsteroid = asteroid;  else if (asteroid.asteroidSpeed> greatestAsteroid.asteroidSpeed) mostAsteroid = asteroid;  // si nous avons une cible plus proche, sinon la cible la plus rapide if (la plus procheAsteroïde! = null) AcquireTargetLock (la plus procheAstéroïde);  else if (mostAsteroid! = null) AcquireTargetLock (mostAsteroid.gameObject); 

AcquérirTargetLock est l'endroit où la magie opère lorsque nous appliquons nos compétences en résolution d'équations quadratiques pour trouver le moment de la collision t.

void AcquireTargetLock (gameObject targetAsteroid) Astéroïde asteroidScript = targetAsteroid.GetComponent(); 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)) (cibleAsteroid.gameObject.transform.position.x. .transform.position.y-turret.transform.position.y) * (targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)); float disc = b * b - (4 * a * c); si (disque<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(); Missile missileScript = firedMissile.GetComponent(); missileScript.LockOn (deployDist); missileRb.velocity = missileSpeed ​​* firedMissile.transform.up; // le missile est déjà pivoté dans le sens nécessaire

Une fois que nous avons trouvé le point d’impact, nous pouvons facilement calculer la distance parcourue par le missile pour atteindre l’astéroïde, qui est transmis à travers le deployDist variable sur le Verrouiller méthode du missile. Le missile utilise cette valeur pour revenir dans son pool d'objets une fois qu'il a parcouru cette distance de la même manière que l'astéroïde. Avant que cela se produise, il aurait certainement frappé l'astéroïde et les événements de collision auraient été déclenchés.

Conclusion

Une fois que nous l'avons implémenté, le résultat semble presque magique. En réduisant le aiPollTime valeur, nous pouvons en faire une tourelle d'IA invincible qui abattrait n'importe quel astéroïde à moins que la vitesse de l'astéroïde ne devienne proche ou supérieure à la vitesse de notre missile. La dérivation que nous avons suivie peut être utilisée pour résoudre une variété de problèmes similaires qui pourraient être représentés sous la forme d’une équation quadratique.. 

J'aimerais que vous expérimentiez davantage en ajoutant l'effet de la gravité au mouvement de l'astéroïde et du missile. Cela changerait le mouvement en mouvement de projectile et les équations correspondantes changeraient. Bonne chance.

Notez également qu'Unity a une économie active. Il existe de nombreux autres produits qui vous aident à construire votre projet. La nature de la plate-forme en fait également une excellente option à partir de laquelle vous pouvez améliorer vos compétences. Quel que soit le cas, vous pouvez voir ce que nous avons disponible dans le marché Envato.