Soluție de unitate pentru atingerea țintelor mutante

Ce veți crea

În timp ce dezvoltăm jocuri care implică un element de acțiune, trebuie adesea să găsim o modalitate de a se ciocni cu o țintă în mișcare. Astfel de scenarii pot fi denumite în mod obișnuit o problemă "atingerea unei ținte în mișcare". Acest lucru este deosebit de proeminent în jocurile de apărare din turn sau în comandarea de rachete, cum ar fi jocurile. S-ar putea să trebuiască să creăm un AI sau un algoritm care să dau seama de mișcarea inamicului și de focul asupra lui. 

Să vedem cum putem rezolva această problemă particulară, de data aceasta în Unitate.

1. Jocul de comandă a rachetelor

Pentru acest tutorial special, vom lua în considerare un joc de comandă a rachetelor. În joc avem o turelă pe teren care declanșează rachete la un asteroid care intră. Nu ar trebui să lăsăm asteroidul să atingă pământul. 

Jocul este bazat pe robinet, în cazul în care trebuie să atingem pentru a focaliza turela. Cu ajutorul uman, mecanica jocului este destul de simplă, așa cum turela trebuie doar să tintească și să tragă. Dar imaginați-vă dacă turela trebuie să tragă automat la asteroizii care intră. 

Provocările pentru AI cu auto-ardere

Turnul trebuie să afle câte asteroizi se apropie de sol. Odată ce are un set de asteroizi apropiați, va trebui apoi să facă o analiză a amenințărilor pentru a determina care dintre ele să fie vizată. Un asteroid cu mișcare lentă este o amenințare mai mică decât o mișcare rapidă. De asemenea, un asteroid care este mai aproape de sol este de asemenea o amenințare iminentă. 

Aceste probleme pot fi rezolvate prin compararea vitezei și poziției asteroizilor care intră. Odată ce am determinat pe cine să țintim, ajungem la cea mai complicată problemă. Când ar trebui focul turelei? La ce unghi ar trebui să se tragă? Când ar trebui racheta să explodeze după ardere? A treia întrebare devine relevantă deoarece explozia rachetelor poate distruge asteroidul și are și o rază mai mare de efect.

Pentru a simplifica problema, turela poate decide să declanșeze imediat. Apoi trebuie să ne dăm seama doar de unghiul de tragere și distanța de detonare. De asemenea, poate exista cazul în care asteroidul a trecut deja în zona în care ar putea fi lovit, ceea ce înseamnă că nu există nicio soluție!

Trebuie să descărcați sursa de unitate furnizată împreună cu acest tutorial pentru a vedea soluția în acțiune. Vom vedea cum derivăm această soluție.

2. Soluția

Vom face o mică perfecționare a matematicii noastre de liceu pentru a găsi soluția. Este foarte simplă și implică rezolvarea unei ecuații patrate. Apare o ecuație patratică ax2 + bx + c = 0, Unde X este variabila care se găsește și are loc cu cea mai mare putere de 2. 

Analizând problema

Să încercăm să reprezentăm problema noastră schematic. 

Linia verde arata calea previzionata care urmeaza sa fie urmata de asteroid. Deoarece avem de-a face cu mișcarea uniformă, asteroidul se mișcă cu viteză constantă. Turela noastră va trebui să se rotească și să tragă cu racheta de-a lungul căii albastre ca să se ciocnească cu asteroidul în viitor.

Pentru mișcarea uniformă, distanța parcursă de un obiect este produsul timpului și viteza obiectului, adică. D = T x S, Unde D înseamnă distanța, T este timpul necesar pentru a călători D, și S este viteza de deplasare. Presupunând că asteroidul nostru și rachetele s-ar ciocni cu siguranță, putem găsi distanța dintre linia albastră urmată de rachetă din punct de vedere al timpului T. În același timp T, asteroidul nostru va ajunge, de asemenea, în aceeași poziție. 

În esență, în același timp T, asteroidul va ajunge la poziția de coliziune din poziția sa actuală, iar racheta va atinge aceeași poziție de coliziune în același timp T. Deci, la timp T, atat asteroidul, cat si racheta ar fi la aceeasi distanta de turela, in timp ce se vor ciocni unul cu celalalt.

Introduceți matematica

Putem distanța distanța de la turelă la asteroid și rachetă în acest moment viitor T pentru a extrage ecuația quadratică cu variabila T. Luați în considerare două puncte pe un plan bidimensional cu coordonate (X1, y1) și (X2, y2). Distanta D între ele pot fi calculate folosind ecuația de mai jos.

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

Dacă denotăm poziția turelei ca (Tx, Ty), viteza rachetelor ca s și poziția de coliziune necunoscută (X Y), atunci ecuația de mai sus poate fi rescrisă ca:

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

Unde T este timpul necesar ca racheta să călătorească la distanță D. Ecuând ambele, obținem prima noastră ecuație pentru necunoscute X și Y cu alt necunoscut T.

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

Știm că asteroidul ajunge și la același punct de coliziune (X Y) în același timp T, și avem următoarele ecuații folosind componentele orizontale și verticale ale vectorului de viteză al asteroidului. Dacă viteza asteroidului poate fi marcată de (Vx, Vy) și poziția actuală ca (Ax, Ay), apoi necunoscutul X și Y pot fi găsite ca mai jos.

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

Înlocuirea acestora în ecuația anterioară ne oferă o ecuație patratică cu un singur necunoscut T

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

Extinderea și combinarea termenilor similari:

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 * Ty * (t * Vy + Ay); s2 * t2 = t2 * Vx2 + Ax2 + 2 * t * Vx * Ax + Tx2 - 2 * Tx * t * Vx - 2 * Ty * t * Vy - 2 * Ty * Ay; 0 = (Vx2 + Vy2 - s2) * t2 + 2 * (Vx * Ax - Tx * Vx + Vy * Ay - Ty * Vy) * t + Ay2 + Ty2 - Tx * Ax; (Vx2 + Vy2 - s2) * t2 + 2 * (Vx * (Ax - Tx) + Vy * (Ay - Ty)) * + (Ax - Tx) 2 = 0;

Reprezentând puterea a două ca 2 și simbolul de multiplicare ca * poate că arăta ca hieroglifele de mai sus, dar în esență se reduce la ecuația quadratică finală ax2 + bx + c = 0, Unde X este variabila T, A este Vx2 + Vy2 - s2, b este 2 * (Vx * (Ax-Tx) + Vy * (Ay-Ty)), și c este (Ay - Ty) 2 + (Ax - Tx) 2. Am folosit ecuațiile de mai jos în derivare.

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

Rezolvarea ecuației patratice

Pentru a rezolva o ecuație patratică, trebuie să calculăm diferența D folosind formula:

D = b2 - 4 * a * c;

Dacă discriminantul este mai mic decât 0 atunci nu există nici o soluție, dacă este 0 atunci există o singură soluție, iar dacă este un număr pozitiv atunci există două soluții. Soluțiile se calculează folosind formulele de mai jos.

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

Folosind aceste formule, putem găsi valori pentru viitorul timp T când se va produce ciocnirea. O valoare negativă pentru T înseamnă că am pierdut ocazia de a trage. Cele necunoscute X și Y poate fi găsit prin înlocuirea valorii T în ecuațiile lor respective.

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

Odată ce cunoaștem punctul de coliziune, putem să ne răsturnăm turela pentru a trage racheta, care ar lovi cu siguranță asteroidul în mișcare după T secunde.

3. Punerea în aplicare în unitate

Pentru proba Unity proiect, am folosit caracteristica de creare a sprite de ultima versiune Unity pentru a crea activele de înlocuire necesare. Acest lucru poate fi accesat cu Creați> Sprites> așa cum se arată mai jos.

Avem un script numit MissileCmdAI care este atașat camerei de scenă. Acesta deține referința la sprite de turelă, prefabricat de rachete și prefab asteroizic. eu folosesc SimplePool de către quill18 pentru a menține bazinele de obiecte pentru rachete și asteroizi. Acesta poate fi găsit pe GitHub. Există scripturi componente pentru rachete și asteroid care sunt atașate la prefabricatele lor și se ocupă de mișcarea lor odată eliberată.

Asteroizii

Asteroizii sunt născuți la întâmplare la o înălțime fixă, dar sunt poziționați aleatoriu pe orizontală și sunt aruncați într-o poziție aleatorie orizontală pe teren cu o viteză aleatorie. Frecvența reproducerii asteroizilor este controlată cu ajutorul unei AnimationCurve.  SpawnAsteroid metodă în MissileCmdAI script-ul arată mai jos:

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

Lansa metodă în Asteroid clasa este prezentată mai jos.

public void Lansare () // așezați asteroidul în partea de sus cu X și lansați-l în partea de jos cu aleatorie x bl = Camera.main.ScreenToWorldPoint (nou Vector2 (10,0)); br = Camera.main.ScreenToWorldPoint (nou Vector2 (Screen.width-20,0)); tl = Camera.main.ScreenToWorldPoint (nou Vector2 (0, Screen.height)); tr = Camera.main.ScreenToWorldPoint (nou 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 viteza = asteroidSpeed ​​* ((destinație-asteroidPos). Normalizat); transform.position = asteroidPos; asteroidRb.velocity = viteza; // a seta o viteza pe corpul rigid pentru ao seta in miscare deployDistance = Vector3.Distance (asteroidPos, destinatie); // dupa ce a parcurs aceasta distanta mare, reveniti la pool void Update () if (Vector2. Distanta (transform.position, asteroidPos)> deployDistance) // odata ce am parcurs distanta setata, vom reveni la pool ReturnToPool ();  void OnTriggerEnter2D (proiector Collider2D) if (projectile.gameObject.CompareTag ("rachetă")) // verificați coliziunea cu racheta, întoarceți-vă în pool ReturnToPool (); 

După cum se vede în Actualizați , după ce asteroidul a parcurs distanța predeterminată față de sol, deployDistance, ar reveni la piscina obiectului. În esență, aceasta înseamnă că sa ciocnit cu solul. Ar fi făcut același lucru în cazul coliziunii cu racheta.

Direcționarea

Pentru ca auto-direcționarea să funcționeze, trebuie să apelăm frecvent metoda corespunzătoare pentru a găsi și viza asteroidul primit. Acest lucru se face în MissileCmdAI scenariu în start metodă.

InvokeRepeating ("FindTarget", 1, aiPollTime); / / set a polling-ul de coduri

Găsește ținta metoda buclele prin toate asteroizii prezenți în scenă pentru a găsi cei mai apropiați și cei mai rapizi asteroizi. Odată găsită, ea îl sună pe AcquireTargetLock metoda de a aplica calculele noastre.

void FindTarget () // găsiți cel mai rapid și cel mai apropiat asteroid GameObject [] aArr = GameObject.FindGameObjectsWithTag ("asteroid"); GameObject closestAsteroid = null; Asteroidul cel mai rapidAsteroid = nul; Asteroid asteroizi; foreach (GameObject mergeți în aArr) if (go.transform.position.y(); dacă (fastestAsteroid == null) // găsi cel mai rapid cel mai rapidAsteroid = asteroid;  altfel dacă (asteroid.asteroidSpeed> fastestAsteroid.asteroidSpeed) fasttestAsteroid = asteroid;  // dacă avem o țintă cea mai apropiată, altfel țintă cea mai rapidă dacă (closestAsteroid! = null) AcquireTargetLock (cea mai apropiatăAsteroidă);  altfel dacă (fastestAsteroid! = null) AcquireTargetLock (fastestAsteroid.gameObject); 

AcquireTargetLock este locul unde magia se întâmplă pe măsură ce aplicăm abilitățile noastre de rezolvare a ecuațiilor patrate pentru a găsi timpul de coliziune T.

void AcquireTargetLock (GameObject targetAsteroid) Asteroid asteroidScript = targetAsteroid.GetComponent(); Vector2 targetVelocity = asteroidScript.asteroidRb.velocity; flotați a = (targetVelocity.x * targetVelocity.x) + (targetVelocity.y * targetVelocity.y) - (rachetăSpeed ​​* rachetăSpeed); flotare 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)); flotor disc = b * b - (4 * a * c); dacă (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 de racheteScript = firedMissile.GetComponent(); missileScript.LockOn (deployDist); missileRb.velocity = rachetaSpeed ​​* firedMissile.transform.up; // racheta este rotită în direcția necesară deja

Odată ce găsim punctul de impact, putem calcula cu ușurință distanța pentru ca racheta să călătorească pentru a lovi asteroidul, care este transmis prin deployDist variabilă pe Blocare pe metoda rachetei. Racheta utilizează această valoare pentru a reveni la piscina obiectului după ce a parcurs această distanță în același mod ca și asteroidul. Înainte de aceasta, ar fi lovit cu siguranță asteroidul, iar evenimentele de coliziune ar fi fost declanșate.

Concluzie

Odată ce am implementat-o, rezultatul pare aproape magic. Prin reducerea aiPollTime , putem să-l transformăm într-o trusă invizibilă AI care ar împușca orice asteroid, cu excepția cazului în care viteza asteroidului se apropie sau este mai mare decât viteza noastră de rachetă. Determinarea pe care am urmat-o poate fi utilizată pentru a rezolva o varietate de probleme similare care ar putea fi reprezentate sub forma unei ecuații patratice. 

Aș dori să experimentați în continuare adăugând efectul gravitației la mișcarea asteroidului și a rachetei. Aceasta ar schimba mișcarea spre mișcarea proiectilului, iar ecuațiile corespunzătoare s-ar schimba. Mult noroc.

Rețineți, de asemenea, că Unitatea are o economie activă. Există multe alte produse care vă ajută să vă construiți proiectul. Natura platformei o face de asemenea o opțiune excelentă din care vă puteți îmbunătăți abilitățile. Indiferent de situație, puteți vedea ce avem la dispoziție în piața Envato Marketplace.