Enhetslösning för att slå rörliga mål

Vad du ska skapa

Samtidigt som vi utvecklar spel som involverar ett handlingselement, behöver vi ofta hitta ett sätt att kollidera med ett rörligt mål. Sådana scenarier kan typiskt kallas ett "träffande rörligt mål" -problem. Detta är särskilt framträdande i tornförsvarsspel eller missilkommando som spel. Vi kan behöva skapa en AI eller algoritm som kan räkna ut fiendens rörelse och eld på den. 

Låt oss se hur vi kan lösa detta problem, den här gången i Unity.

1. Missile Command Game

För denna speciella handledning kommer vi att överväga ett missilkommandospel. I spelet har vi ett torn på marken som bränder missiler vid en inkommande asteroid. Vi borde inte låta asteroiden träffa marken. 

Spelet är kranbaserat, där vi måste knacka för att sikta turret. Med mänskligt bistånd är spelmekanikerna ganska enkla eftersom tornet bara behöver sikta och skjuta. Men tänk om tornet automatiskt ska skjuta på inkommande asteroider. 

Utmaningarna för auto-firande AI

Tornet behöver ta reda på hur många asteroider som närmar sig marken. När den har en uppsättning av alla närmar sig asteroider skulle det då behöva göra en hotanalys för att bestämma vilken som ska riktas mot. En slow-moving asteroid är ett mindre hot än en snabb rörelse. Dessutom är en asteroid som ligger närmare marken ett överhängande hot också. 

Dessa problem kan lösas genom att jämföra hastigheten och positionen för inkommande asteroider. När vi väl har bestämt vilken som ska riktas, når vi det mest komplicerade problemet. När ska tornet elda? Vid vilken vinkel ska den skjuta? När ska missilen sätta sig för att explodera efter bränning? Den tredje frågan blir relevant eftersom missil explosionen också kan förstöra asteroiden och har också en större effektradie.

För att förenkla problemet kan tornet besluta att elda direkt. Då behöver vi bara räkna ut vinkeln för avfyring och avstånd från detonation. Det kan också vara fallet där asteroiden redan har passerat området där det kunde drabbas, vilket betyder att det inte finns någon lösning!

Du bör ladda ner enhetskällan som följer med denna handledning för att se lösningen i åtgärd. Vi ska se hur vi härleder den lösningen.

2. Lösningen

Vi ska göra en liten uppdatering av vår matematik för att hitta lösningen. Det är mycket enkelt och innebär att man löser en kvadratisk ekvation. En kvadratisk ekvation ser ut ax2 + bx + c = 0, var x är variabeln att hitta och den uppstår med högsta effekten av 2. 

Analysera problemet

Låt oss försöka representera vårt problem schematiskt. 

Den gröna linjen visar den förutspådda vägen som ska följas av asteroiden. När vi hanterar enhetlig rörelse, rör sig asteroiden med konstant hastighet. Vårt torn måste rotera och skjuta missilen längs den blå vägen för att den ska kollidera med asteroiden i en framtida tid.

För enhetlig rörelse är avståndet som reste av ett objekt tidens produkt och objektets hastighet, dvs. D = T x S, var D står för avståndet, T är det dags att resa D, och S är färdhastigheten. Om vi ​​antar att vår asteroid och missiler definitivt skulle kollidera, kan vi hitta avståndet till den blå linjen följt av missilen när det gäller tid t. På samma gång t, vår asteroid kommer också att nå samma position. 

I huvudsak, på samma gång t, asteroiden kommer att nå kollisionspositionen från sin nuvarande position, och missilen kommer också att nå samma kollisionsposition samtidigt t. Så i tid t, både asteroiden och missilen skulle ligga i samma avstånd från tornet som de skulle kollidera med varandra.

Ange matematik

Vi kan jämföra avståndet från tornet till asteroiden och missilen vid denna framtida tid t för att härleda vår kvadratiska ekvation med variabeln t. Tänk på två punkter på ett tvådimensionellt plan med koordinater (X1, y1) och (X2, y2). Avståndet D mellan dem kan beräknas med hjälp av ekvationen nedan.

D2 = (x2-x1) 2 + (y2-yl) 2

Om vi ​​betecknar tornställningen som (Tx, Ty), rakets hastighet som s och den okända kollisionspositionen som (X, Y), då kan ovanstående ekvation omskrivas som:

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

var t är den tid det tar för missilen att färdas avståndet D. Att jämföra båda får vi vår första ekvation för okända X och Y med en annan okänd t.

s2 * t2 = (X-Tx) 2 + (Y-Ty) 2

Vi vet att asteroiden också når samma kollisionspunkt (X, Y) på samma gång t, och vi har följande ekvationer med hjälp av de horisontella och vertikala komponenterna i asteroidens hastighetsvektor. Om hastigheten på asteroiden kan betecknas med (Vx, Vy) och nuvarande position som (Ax, Ay), då det okända X och Y kan hittas enligt nedan.

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

Att ersätta dessa i den tidigare ekvationen ger oss en kvadratisk ekvation med en enda okänd t

s2 * t2 = ((t * Vx + Ax) -Tx) 2 + ((t * Vy + Ay) -Ty) 2;

Utöka och kombinera liknande termer:

s2 * t2 = (t * Vx + Ax) 2 + Tx2 - 2 * Tx * (t * Vx + Ax) + (t * Vy + Ay) 2 + Ty2 - 2 * Ty * (t * Vy + Ay); s2 * t2 = t2 * Vx2 + Ax2 + 2 * t * Vx * Ax + Tx2 - 2 * Tx * (t * Vx + Ax) + t2 * Vy2 + Ay2 + 2 * t * Vy * Ay + Ty2 - 2 * Ty * (t * Vy + Ay); s2 * t2 = t2 * Vx2 + Ax2 + 2 * t * Vx * Ax + Tx2 - 2 * Tx * t * Vx - 2 * Tx * Ax + t2 * Vy2 + Ay2 + 2 * t * Vy * Ay + Ty2 - 2 * Ty * t * Vy - 2 * Ty * Ay; 0 = (Vx2 + Vy2 - s2) * t2 + 2 * (Vx * Ax - Tx * Vx + Vy * Ay - Ty * Vy) * t + Ay2 + Ty2 - 2 * Ty * Ay + Ax2 + Tx2 - 2 * tx * Ax; (Vx2 + Vy2 - s2) * t2 + 2 * (Vx * (Ax-Tx) + Vy * (Ay-Ty)) * t + (Ay-Ty) 2 + (Ax-Tx) 2 = 0;

Representerar kraften för två som 2 och multiplikationssymbolen som * kan ha gjort ovanstående ser ut som hieroglyfer, men det kollar i huvudsak ner till den slutliga kvadratiska ekvationen ax2 + bx + c = 0, var x är variabeln t, en är Vx2 + Vy2 - s2, b är 2 * (Vx * (Ax-Tx) + Vy * (Ay-Ty)), och c är (Ay-Ty) 2 + (Ax-Tx) 2. Vi använde ekvationerna nedan i avledningen.

(a + b) 2 = a2 + 2 * a * b + b2; (a-b) 2 = a2 - 2 * a * b + b2;

Lösa den kvadratiska ekvationen

För att lösa en kvadratisk ekvation måste vi beräkna diskriminanten D med hjälp av formeln:

D = b2 - 4 * a * c;

Om diskriminanten är mindre än 0 då finns det ingen lösning, om det är 0 då finns det en enda lösning, och om det är ett positivt tal finns det två lösningar. Lösningarna beräknas med hjälp av formlerna nedan.

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

Med hjälp av dessa formler kan vi hitta värden för framtiden t när kollisionen kommer att hända. Ett negativt värde för t betyder att vi har missat chansen att branda. De okända X och Y kan hittas genom att ersätta värdet av t i deras respektive ekvationer.

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

När vi känner till kollisionspunkten kan vi rotera tornet för att avfyra missilen, vilket definitivt skulle slå den rörliga asteroiden efter t sek.

3. Genomförande i enhet

För Projekt Unity-projektet har jag använt sprite-skapningsfunktionen i den senaste Unity-versionen för att skapa nödvändiga platshållare tillgångar. Detta kan nås med Skapa> Sprites> enligt nedanstående.

Vi har ett spelskript som heter MissileCmdAI som är ansluten till scenkameran. Den hänvisar till turretspritet, missil prefab, och asteroid prefab. jag använder SimplePool av quill18 för att behålla objektpoolerna för missiler och asteroider. Den finns på GitHub. Det finns komponentskript för missil och asteroid som är fästade på sina prefabs och hantera deras rörelser när de släppts.

Asteroiderna

Asteroider kastas slumpmässigt i fast höjd men slumpmässigt horisontellt läge och slungas i slumpmässigt horisontellt läge på marken med slumpmässig hastighet. Frekvensen av asteroidgytning styrs med användning av en AnimationCurve. De SpawnAsteroid metod i MissileCmdAI Skriptet ser ut som nedan:

void SpawnAsteroid () GameObject asteroid = SimplePool.Spawn (asteroidPrefab, Vector2.one, Quaternion.identity); Asteroid asteroidScript = asteroid.GetComponent(); asteroidScript.Launch (); SetNextSpawn (); 

De Lansera metod i Asteroid klassen visas nedan.

public void Launch () // placera asteroiden i topp med slumpmässig x och lansera den till botten med slumpmässig x bl = Camera.main.ScreenToWorldPoint (ny Vector2 (10.0)); br = Camera.main.ScreenToWorldPoint (ny Vector2 (Screen.width-20,0)); tl = Camera.main.ScreenToWorldPoint (ny Vector2 (0, Screen.height)); tr = Camera.main.ScreenToWorldPoint (ny Vector2 (Screen.width, Screen.height)); transform.localScale = Vector2.one * (0.2f + Random.Range (0.2f, 0.8f)); asteroidSpeed ​​= Random.Range (asteroidMinSpeed, asteroidMaxSpeed); asteroidPos.x = Random.Range (tl.x, tr.x); asteroidPos.y = tr.y + 1; destination.y = bl.y; destination.x = Random.Range (bl.x, br.x); Vector2 hastighet = asteroidSpeed ​​* ((destination-asteroidPos). Normaliserad); transform.position = asteroidPos; asteroidRb.velocity = hastighet; // ställa in en hastighet på styvkroppen för att ställa in det i rörelse deployDistance = Vector3.Distance (asteroidPos, destination); // efter att ha rest det här avståndet, återgå till poolen void Update () if (Vector2. Avstånd (transform.position, asteroidPos)> deployDistance) // när vi har rest det inställda avståndet, återgå till poolen ReturnToPool ();  void OnTriggerEnter2D (Collider2D projektil) if (projectile.gameObject.CompareTag ("missil")) // kolla kollision med missil, återvänd till pool ReturnToPool (); 

Som ses i Uppdatering metod, när asteroiden har rest det förutbestämda avståndet till marken, deployDistance, det skulle återvända till sin objektpool. I huvudsak betyder det att det har kolliderat med marken. Det skulle göra samma sak vid kollision med missilen.

Targeting

För att den automatiska inriktningen ska fungera måste vi ofta ringa motsvarande metod för att hitta och rikta in den inkomende asteroiden. Detta görs i MissileCmdAI script i dess Start metod.

InvokeRepeating ("FindTarget", 1, aiPollTime); // set ai code polling

De Hitta målet metod loopar genom alla asteroiderna närvarande i scenen för att hitta de närmaste och snabbaste asteroiderna. När den hittades, kallar den sedan AcquireTargetLock metod för att tillämpa våra beräkningar.

void FindTarget () // hitta snabbast och närmaste asteroid GameObject [] aArr = GameObject.FindGameObjectsWithTag ("asteroid"); GameObject closestAsteroid = null; Asteroid snabbestAsteroid = null; Asteroid asteroid; foreach (GameObject gå in aArr) om (go.transform.position.y(); om (snabbastAsteroid == null) // hitta snabbasteAsteroid = asteroid;  annars om (asteroid.asteroidSpeed> snabbestAsteroid.asteroidSpeed) fastestAsteroid = asteroid;  // om vi har ett närmaste mål som annars riktar sig till den snabbaste om (closestAsteroid! = null) AcquireTargetLock (closestAsteroid);  annars om (snabbastAsteroid! = null) AcquireTargetLock (snabbastAsteroid.gameObject); 

AcquireTargetLock är där magiken händer när vi tillämpar våra kvadratiska ekvationslösningsförmåga för att hitta kollisionstiden t.

void AcquireTargetLock (GameObject targetAsteroid) Asteroid asteroidScript = targetAsteroid.GetComponent(); Vector2 targetVelocity = asteroidScript.asteroidRb.velocity; flyta 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)) + ((targetAsteroid.gameObject .transform.position.y-turret.transform.position.y) * (targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)); flytskiva = b * b - (4 * a * c); if (skiva<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(); Missil missileScript = firedMissile.GetComponent(); missileScript.LockOn (deployDist); missileRb.velocity = missileSpeed ​​* firedMissile.transform.up; // missil roteras i nödvändig riktning redan

När vi har hittat punkten, kan vi enkelt beräkna avståndet för missilen att resa för att slå asteroiden, vilken passeras genom deployDist variabel på Lås På metod för missilen. Missilen använder det här värdet för att återvända till objektpoolen när den har rest detta avstånd på samma sätt som asteroiden. Innan detta händer skulle det definitivt ha slagit på asteroiden, och kollisionshändelserna skulle ha blivit utlöst.

Slutsats

När vi implementerar det ser resultatet nästan magiskt ut. Genom att minska aiPollTime värde kan vi göra det till ett oövervinnligt AI-torn som skulle skjuta ner någon asteroid om inte asteroidhastigheten blir nära eller högre än vår missilhastighet. Den avledning vi följde kan användas för att lösa en mängd liknande problem som kan representeras i form av en kvadratisk ekvation. 

Jag skulle vilja att du ska experimentera vidare genom att lägga till tyngdkraftseffekten till asteroidens och missilens rörelse. Detta skulle ändra rörelsen till projektil rörelse, och motsvarande ekvationer skulle förändras. Lycka till.

Observera också att Unity har en aktiv ekonomi. Det finns många andra produkter som hjälper dig att bygga upp ditt projekt. Plattformens karaktär gör det också till ett bra alternativ som du kan förbättra dina färdigheter. Oavsett fall kan du se vad vi har tillgängligt på Envato Marketplace.