Enhetsløsning for å treffe flytende mål

Hva du skal skape

Mens du utvikler spill som involverer et handlingselement, må vi ofte finne ut en måte å kollidere med et bevegelig mål. Slike scenarier kan typisk kalles et "trekkende mål" -problem. Dette er spesielt fremtredende i tårnforsvarsspill eller missilkommando som spill. Vi må kanskje lage en AI eller algoritme som kan finne ut fiendens bevegelse og brann på den. 

La oss se hvordan vi kan løse dette problemet, denne gangen i Unity.

1. Missile Command Game

For denne spesielle opplæringen vil vi vurdere et missilkommandospill. I spillet har vi et tårn på bakken som brenner missiler på en innkommende asteroide. Vi bør ikke tillate at asteroiden treffer bakken. 

Spillspillet er trykkbasert, der vi må trykke for å sikte tårnet. Med menneskelig hjelp er spillmekanikkene ganske enkle da tårnet bare trenger å sikte og brenne. Men tenk hvis tårnet trenger å automatisk brann ved innkommende asteroider. 

Utfordringene for Auto-Firing AI

Turret trenger å finne ut hvor mange asteroider nærmer seg bakken. Når den har et sett med alle nærmer seg asteroider, vil det da trenge en trusselanalyse for å bestemme hvilken som skal målrettes. En sakte asteroid er en mindre trussel enn en rask bevegelse. Også, en asteroide som er nærmere bakken er også en overhengende trussel. 

Disse problemene kan løses ved å sammenligne hastigheten og posisjonen til de innkommende asteroider. Når vi har bestemt hvilken som skal målrettes, når vi det mest kompliserte problemet. Når skal tårnet brann? I hvilken vinkel skal den brann? Når skal missilet sette seg til å eksplodere etter avfyring? Det tredje spørsmålet blir relevant fordi missileksplosjonen også kan ødelegge asteroiden og har en større effektradius.

For å forenkle problemet kan tårnet bestemme seg for å brann med en gang. Da trenger vi bare å finne ut vinkelen for avfyring og avstand for detonasjon. Det kan også være tilfelle hvor asteroiden allerede har passert området der det kan bli rammet, noe som betyr at det ikke finnes noen løsning!

Du bør laste ned enhetskilden som følger med denne opplæringen for å se løsningen i aksjon. Vi vil se hvordan vi får den løsningen.

2. Løsningen

Vi skal gjøre en liten oppdatering av videregående matematikk for å finne løsningen. Det er veldig grei og innebærer å løse en kvadratisk ligning. En kvadratisk ligning ser ut som ax2 + bx + c = 0, hvor x er variabelen å finne og den oppstår med høyest mulig kraft på 2. 

Analyserer problemet

La oss prøve å representere vårt problem skjematisk. 

Den grønne linjen viser den spådde banen som skal følges av asteroiden. Når vi arbeider med uniform bevegelse, beveger asteroiden seg med konstant hastighet. Vår turret må rotere og brenne missilet langs den blå banen for å kollidere med asteroiden i en fremtidig tid.

For jevn bevegelse er avstanden som er reist av et objekt, produktet av tid og objektets hastighet, dvs.. D = T x S, hvor D står for avstanden, T er tiden det tar å reise D, og S er reisens hastighet. Forutsatt at våre asteroider og missiler ville definitivt kollidere, kan vi finne avstanden til den blå linjen etterfulgt av raketten i form av tid t. På samme tid t, vår asteroid vil også nå samme posisjon. 

I hovedsak, på samme tid t, asteroiden kommer til kollisjonsposisjonen fra sin nåværende posisjon, og missilet vil også nå samme kollisjonsposisjon samtidig t. Så til tider t, både asteroiden og missilet ville være i samme avstand fra tårnet som de ville kollidere med hverandre.

Skriv inn matematikk

Vi kan likestille avstanden fra tårnet til asteroiden og raketten i denne fremtidige tiden t for å utlede vår kvadratiske ligning med variabelen t. Tenk på to punkter på et todimensjonalt plan med koordinater (X1, y1) og (X2, y2). Avstanden D mellom dem kan beregnes ved hjelp av ligningen nedenfor.

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

Hvis vi betegner tårnposisjonen som (Tx, Ty), rakets hastighet som s og den ukjente kollisjonsposisjonen som (X, Y), så kan ligningen ovenfor omskrives som:

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

hvor t er tiden det tar for missilet å reise avstanden D. Ligger begge, får vi vår første ligning for ukjente X og Y med en annen ukjent t.

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

Vi vet at asteroiden også når samme kollisjonspunkt (X, Y) på samme tid t, og vi har følgende ligninger ved hjelp av de horisontale og vertikale komponentene til asteroidens hastighetsvektor. Hvis hastigheten til asteroiden kan betegnes av (Vx, Vy) og nåværende posisjon som (Ax, Ay), så det ukjente X og Y kan bli funnet som nedenfor.

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

Ved å erstatte disse i den tidligere ligningen får vi en kvadratisk ligning med en enkelt ukjent t

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

Utvide og kombinere lignende termer:

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

Representerer kraften til to som 2 og multiplikasjonssymbolet som * kan ha gjort ovennevnte til å ligne hieroglyfer, men det koker i hovedsak ned til den endelige kvadratiske ligningen ax2 + bx + c = 0, hvor x er variabelen t, en er Vx2 + Vy2 - s2, b er 2 * (Vx * (Aks - Tx) + Vy * (Ay - Ty)), og c er (Ay - Ty) 2 + (Ax-Tx) 2. Vi brukte ligningene under i avledningen.

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

Løse den kvadratiske ligningen

For å løse en kvadratisk ligning må vi beregne diskriminanten D bruker formelen:

D = b2 - 4 * a * c;

Hvis diskriminanten er mindre enn 0 så er det ingen løsning, hvis det er det 0 så er det en enkelt løsning, og hvis det er et positivt tall så er det to løsninger. Løsninger beregnes ved å bruke formlene nedenfor.

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

Ved å bruke disse formlene kan vi finne verdier for fremtidens tid t når kollisjonen vil skje. En negativ verdi for t betyr at vi har savnet muligheten til å brenne. De ukjente X og Y kan bli funnet ved å erstatte verdien av t i deres respektive ligninger.

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

Når vi kjenner kollisjonspunktet, kan vi rotere tårnet for å brenne missilet, som definitivt ville slå den bevegelige asteroiden etter t sek.

3. Implementering i enhet

For prøven Unity-prosjektet har jeg brukt sprite-opprettingsfunksjonen til den nyeste Unity-versjonen for å skape de nødvendige plasseringsegenskapene. Dette kan nås med Lag> Sprites> som vist under.

Vi har et spillskript kalt MissileCmdAI som er festet til scenekameraet. Den inneholder referansen til tårnens sprite, missil prefab, og asteroid prefab. jeg bruker SimplePool av quill18 for å opprettholde objektbassene for missiler og asteroider. Den finnes på GitHub. Det er komponentskript for rakett og asteroide som er festet til prefabsene og håndterer bevegelsen en gang utgitt.

Asteroider

Asteroider spres tilfeldig i fast høyde, men tilfeldig horisontal posisjon og kastes i tilfeldig horisontal posisjon på bakken med en tilfeldig hastighet. Frekvensen av asteroide gyting styres ved hjelp av en AnimationCurve. De SpawnAsteroid metode i MissileCmdAI Skriptet ser ut som nedenfor:

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

De Launch metode i Asteroid klassen er vist nedenfor.

Offentlig tomgang Launch () // plasser asteroiden i topp med tilfeldig x og start den til bunn med tilfeldig 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 ​​* ((destinasjon-asteroidPos). Normalisert); transform.position = asteroidPos; asteroidRb.velocity = hastighet; // sett en hastighet på stivlegemet for å sette den i bevegelse deployDistance = Vector3.Distance (asteroidPos, destinasjon); // etter å ha reist denne lange avstanden, returner til bassenget void Update () if (Vector2. Avstand (transform.position, asteroidPos)> deployDistance) // når vi har reist den angitte avstanden, returner til bassenget ReturnToPool ();  ugyldig OnTriggerEnter2D (Collider2D prosjektil) if (projectile.gameObject.CompareTag ("missil")) // kontroller kollisjon med missil, returner til pool ReturnToPool (); 

Som sett i Oppdater metode, når asteroiden har reist den forutbestemte avstanden til bakken, deployDistance, det ville gå tilbake til objektbassenget. I hovedsak betyr dette at det har kollidert med bakken. Det ville gjøre det samme ved kollisjon med missilet.

Målretting

For at den automatiske målrettingen skal virke, må vi ofte ringe den tilsvarende metoden for å finne og målrette den innkommende asteroiden. Dette gjøres i MissileCmdAI skript i sin Start metode.

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

De Finn mål Metode looper gjennom alle asteroider tilstede i scenen for å finne de nærmeste og raskeste asteroider. En gang funnet, kaller det deretter AcquireTargetLock metode for å bruke beregningene våre.

void FindTarget () // finn raskeste og nærmeste asteroide GameObject [] aArr = GameObject.FindGameObjectsWithTag ("asteroid"); GameObject closestAsteroid = null; Asteroid raskestAsteroid = null; Asteroid asteroide; foreach (GameObject gå i aArr) if (go.transform.position.y(); hvis (raskestAsteroid == null) // finn raskestAsteroid = asteroide;  ellers hvis (asteroid.asteroidSpeed> raskestAsteroid.asteroidSpeed) fastestAsteroid = asteroid;  // hvis vi har et nærmest mål som ellers målretter mot den raskeste hvis (closestAsteroid! = null) AcquireTargetLock (closestAsteroid);  annet hvis (raskestAsteroid! = null) AcquireTargetLock (raskesteAsteroid.gameObject); 

AcquireTargetLock er hvor magien skjer når vi bruker våre kvadratiske likningsløsning ferdigheter for å finne tidspunktet for kollisjon t.

void AcquireTargetLock (GameObject targetAsteroid) Asteroid asteroidScript = targetAsteroid.GetComponent(); Vector2 targetVelocity = asteroidScript.asteroidRb.velocity; flyte a = (targetVelocity.x * targetVelocity.x) + (targetVelocity.y * targetVelocity.y) - (missileSpeed ​​* missileSpeed); flyte 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)); float disk = b * b - (4 * a * c); if (disc<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; // missil roteres i nødvendig retning allerede

Når vi finner punktet for innflytelse, kan vi enkelt beregne avstanden for raketten å reise for å slå asteroiden, som sendes videre gjennom deployDist variabel på Lockon Metoden for raketten. Missilen bruker denne verdien til å returnere til objektbassenget når den har reist denne avstanden på samme måte som asteroiden. Før dette skjer, ville det definitivt ha slått asteroiden, og kollisjonshendelsene ville vært utløst.

Konklusjon

Når vi implementerer det, ser resultatet nesten magisk ut. Ved å redusere aiPollTime verdi, kan vi gjøre det til et uovervinnelig AI-tårn som ville skyte ned noen asteroider, med mindre asteroidhastigheten blir nær eller høyere enn vår missilhastighet. Den avledning vi fulgte kan brukes til å løse en rekke liknende problemer som kunne representeres i form av en kvadratisk ligning. 

Jeg vil at du skal eksperimentere ytterligere ved å legge til tyngdekraftenes effekt på asteroiden og raketten. Dette ville endre bevegelsen til prosjektilbevegelse, og de tilsvarende ligningene ville endres. Lykke til.

Legg også merke til at Unity har en aktiv økonomi. Det er mange andre produkter som hjelper deg med å bygge opp prosjektet ditt. Plattformens natur gjør det også til et flott alternativ som du kan forbedre dine ferdigheter. Uansett, kan du se hva vi har tilgjengelig på Envato Marketplace.