Soluzione di unità per colpire bersagli mobili

Cosa starai creando

Mentre sviluppiamo giochi che comportano un elemento d'azione, spesso abbiamo bisogno di trovare un modo per scontrarsi con un bersaglio in movimento. Tali scenari possono essere in genere definiti un problema di "colpire un bersaglio mobile". Questo è particolarmente importante nei giochi di difesa della torre o nel comando di missili come i giochi. Potremmo aver bisogno di creare un'intelligenza artificiale o un algoritmo in grado di capire il movimento del nemico e spararci sopra. 

Vediamo come possiamo risolvere questo particolare problema, questa volta in Unity.

1. Il gioco del comando missilistico

Per questo particolare tutorial, prenderemo in considerazione un gioco di comando missilistico. Nel gioco abbiamo una torretta a terra che spara missili contro un asteroide in arrivo. Non dovremmo permettere all'asteroide di colpire il suolo. 

Il gioco è basato sul tap, dove dobbiamo toccare per mirare alla torretta. Con l'assistenza umana, le meccaniche di gioco sono piuttosto semplici in quanto la torretta ha solo bisogno di mirare e sparare. Ma immagina se la torretta ha bisogno di sparare automaticamente agli asteroidi in arrivo. 

Le sfide per l'IA con auto-accensione

La torretta ha bisogno di scoprire quanti asteroidi si avvicinano al terreno. Una volta che ha un set di tutti gli asteroidi che si avvicinano, dovrebbe quindi eseguire un'analisi delle minacce per determinare quale targetizzare. Un asteroide che si muove lentamente è una minaccia minore rispetto a uno in rapido movimento. Inoltre, un asteroide che è più vicino al suolo è una minaccia imminente. 

Questi problemi possono essere risolti confrontando la velocità e la posizione degli asteroidi in arrivo. Una volta determinato quale target raggiungere, arriveremo al problema più complicato. Quando dovrebbe sparare la torretta? In quale angolo dovrebbe sparare? Quando dovrebbe esplodere il missile dopo lo sparo? La terza domanda diventa rilevante perché l'esplosione del missile può anche distruggere l'asteroide e ha anche un raggio di effetto maggiore.

Per semplificare il problema, la torretta può decidere di sparare immediatamente. Quindi dobbiamo solo capire l'angolo di fuoco e la distanza di detonazione. Inoltre, potrebbe esserci il caso in cui l'asteroide abbia già superato l'area in cui potrebbe essere colpito, il che significa che non c'è soluzione!

Dovresti scaricare la sorgente dell'unità fornita insieme a questo tutorial per vedere la soluzione in azione. Vedremo come deriviamo questa soluzione.

2. La soluzione

Faremo un piccolo aggiornamento della nostra matematica delle scuole superiori per trovare la soluzione. È molto semplice e comporta la risoluzione di un'equazione quadratica. Sembra un'equazione quadratica ax2 + bx + c = 0, dove X è la variabile da trovare e si verifica con la massima potenza di 2. 

Analizzando il problema

Cerchiamo di rappresentare il nostro problema in modo schematico. 

La linea verde mostra il percorso previsto per essere seguito dall'asteroide. Dato che abbiamo a che fare con un moto uniforme, l'asteroide si muove a velocità costante. La nostra torretta dovrà ruotare e lanciare il missile lungo il percorso blu per farlo scontrare con l'asteroide in un momento futuro.

Per il movimento uniforme, la distanza percorsa da un oggetto è il prodotto del tempo e della velocità dell'oggetto, cioè. D = T x S, dove D sta per la distanza, T è il tempo necessario per viaggiare D, e S è la velocità del viaggio. Supponendo che il nostro asteroide e i missili si collocherebbero sicuramente, possiamo trovare la distanza della linea blu seguita dal missile in termini di tempo t. Allo stesso tempo t, anche il nostro asteroide raggiungerà la stessa posizione. 

In sostanza, nello stesso tempo t, l'asteroide raggiungerà la posizione di collisione dalla sua posizione corrente e il missile raggiungerà la stessa posizione di collisione nello stesso tempo t. Quindi alla volta t, sia l'asteroide che il missile si troverebbero alla stessa distanza dalla torretta in quanto si sarebbero scontrati l'uno con l'altro.

Inserisci Math

Possiamo equiparare la distanza dalla torretta all'asteroide e al missile in questo momento futuro t per ricavare la nostra equazione quadratica con la variabile t. Prendi in considerazione due punti su un piano bidimensionale con coordinate (X1, y1) e (X2, y2). La distanza D tra di loro può essere calcolato utilizzando l'equazione di seguito.

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

Se denotiamo la posizione della torretta come (Tx, Ty), la velocità del missile come S e la posizione di collisione sconosciuta come (X, Y), allora l'equazione sopra può essere riscritta come:

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

dove t è il tempo impiegato dal missile per percorrere la distanza D. Adattando entrambi, otteniamo la nostra prima equazione per le incognite X e Y con un altro sconosciuto t.

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

Sappiamo che l'asteroide raggiunge anche lo stesso punto di collisione (X, Y) allo stesso tempo t, e abbiamo le seguenti equazioni usando le componenti orizzontali e verticali del vettore di velocità dell'asteroide. Se la velocità dell'asteroide può essere indicata da (Vx, Vy) e la posizione corrente come (Ax, Ay), quindi l'ignoto X e Y può essere trovato come sotto.

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

Sostituendoli nell'equazione precedente otteniamo un'equazione quadratica con un singolo sconosciuto t

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

Espansione e combinazione di termini simili:

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;

Rappresentando la potenza di due come 2 e il simbolo di moltiplicazione come * potrebbe aver fatto apparire il geroglifico come sopra, ma essenzialmente si riduce all'equazione quadratica finale ax2 + bx + c = 0, dove X è la variabile t, un è Vx2 + Vy2 - s2, B è 2 * (Vx * (Ax - Tx) + Vy * (Ay - Ty)), e c è (Ay - Ty) 2 + (Ax - Tx) 2. Abbiamo usato le equazioni sottostanti nella derivazione.

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

Risolvere l'equazione quadratica

Per risolvere un'equazione quadratica, dobbiamo calcolare il discriminante D usando la formula:

D = b2 - 4 * a * c;

Se il discriminante è inferiore a 0 allora non c'è soluzione, se lo è 0 allora c'è un'unica soluzione, e se è un numero positivo allora ci sono due soluzioni. Le soluzioni sono calcolate utilizzando le formule indicate di seguito.

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

Usando queste formule, possiamo trovare valori per il tempo futuro t quando la collisione avverrà. Un valore negativo per t significa che abbiamo perso l'opportunità di sparare. Le incognite X e Y può essere trovato sostituendo il valore di t nelle loro rispettive equazioni.

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

Una volta che conosciamo il punto di collisione, possiamo ruotare la nostra torretta per sparare il missile, che sicuramente colpirebbe l'asteroide in movimento dopo t secondi.

3. Implementazione in unità

Per il progetto Unity di esempio, ho utilizzato la funzione di creazione sprite dell'ultima versione di Unity per creare le risorse segnaposto necessarie. Questo è accessibile con Crea> Sprites> come mostrato di seguito.

Abbiamo uno script di gioco chiamato MissileCmdAI che è collegato alla telecamera di scena. Contiene il riferimento alla torretta, al missile prefabbricato e al prefabbricato di asteroidi. sto usando SimplePool da quill18 per mantenere i pool di oggetti per missili e asteroidi. Può essere trovato su GitHub. Ci sono script di componenti per missili e asteroidi che sono collegati ai loro prefabbricati e gestiscono il loro movimento una volta rilasciato.

Gli asteroidi

Gli asteroidi vengono generati casualmente ad altezza fissa ma in posizione orizzontale casuale e vengono lanciati in una posizione orizzontale casuale sul terreno con una velocità casuale. La frequenza di generazione degli asteroidi è controllata usando un AnimationCurve. Il SpawnAsteroid metodo nel MissileCmdAI lo script appare come di seguito:

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

Il Lanciare metodo nel Asteroide la classe è mostrata sotto.

public void Launch () // posiziona l'asteroide in alto a caso con x e lo avvia in basso con random x bl = Camera.main.ScreenToWorldPoint (new Vector2 (10,0)); br = Camera.main.ScreenToWorldPoint (nuovo Vector2 (Screen.width-20,0)); tl = Camera.main.ScreenToWorldPoint (new Vector2 (0, Screen.height)); tr = Camera.main.ScreenToWorldPoint (nuovo 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); Velocity Vector2 = asteroidSpeed ​​* ((destination-asteroidPos) .normalized); transform.position = asteroidPos; asteroidRb.velocity = velocity; // imposta una velocità su rigidbody per impostarla in motion deployDistance = Vector3.Distance (asteroidPos, destination); // dopo aver percorso questa distanza, torna al pool void Update () if (Vector2. Distance (transform.position, asteroidPos)> deployDistance) // una volta percorsa la distanza impostata, tornare al pool ReturnToPool ();  void OnTriggerEnter2D (Collider2D projectile) if (projectile.gameObject.CompareTag ("missile")) // verifica collisione con il missile, torna al pool ReturnToPool (); 

Come visto in Aggiornare metodo, una volta che l'asteroide ha percorso la distanza predeterminata a terra, deployDistance, ritornerebbe al suo pool di oggetti. Essenzialmente questo significa che è entrato in collisione con il terreno. Farebbe lo stesso in caso di collisione con il missile.

Il targeting

Affinché l'auto-targeting funzioni, dobbiamo chiamare frequentemente il metodo corrispondente per trovare e individuare l'asteroide in arrivo. Questo è fatto nel MissileCmdAI script nel suo Inizio metodo.

InvokeRepeating ("FindTarget", 1, aiPollTime); // imposta il polling del codice ai

Il Trova il bersaglio il metodo scorre attraverso tutti gli asteroidi presenti nella scena per trovare gli asteroidi più vicini e più veloci. Una volta trovato, chiama quindi AcquireTargetLock metodo per applicare i nostri calcoli.

void FindTarget () // trova l'asteroide più veloce e più vicino GameObject [] aArr = GameObject.FindGameObjectsWithTag ("asteroide"); GameObject closestAsteroid = null; Asteroide più veloceAsteroide = null; Asteroide asteroide; foreach (GameObject go in aArr) if (go.transform.position.y(); if (fastestAsteroid == null) // trova il più veloce più veloceAsteroid = asteroide;  else if (asteroid.asteroidSpeed> fastestAsteroid.asteroidSpeed) fastestAsteroid = asteroide;  // se abbiamo un target più vicino che, altrimenti target il più veloce if (closestAsteroid! = null) AcquireTargetLock (closestAsteroid);  else if (fastestAsteroid! = null) AcquireTargetLock (fastestAsteroid.gameObject); 

AcquireTargetLock è dove avviene la magia mentre applichiamo le nostre abilità di risoluzione delle equazioni quadratiche per trovare il tempo di collisione t.

void AcquireTargetLock (GameObject targetAsteroid) Asteroid 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)) + ((targetAsteroid.gameObject .transform.position.y-turret.transform.position.y) * (targetAsteroid.gameObject.transform.position.y-turret.transform.position.y)); float disc = b * b - (4 * a * c); if (disco<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; // il missile è già ruotato nella direzione necessaria

Una volta trovato il punto d'impatto, possiamo facilmente calcolare la distanza che il missile deve percorrere per colpire l'asteroide, che viene trasmesso attraverso il deployDist variabile sul Blocco su metodo del missile. Il missile usa questo valore per tornare al suo pool di oggetti una volta che ha percorso questa distanza allo stesso modo dell'asteroide. Prima che ciò accadesse, avrebbe sicuramente colpito l'asteroide e gli eventi di collisione sarebbero stati attivati.

Conclusione

Una volta implementato, il risultato sembra quasi magico. Riducendo il aiPollTime valore, possiamo renderlo una invincibile torretta di intelligenza artificiale che distruggerebbe qualsiasi asteroide a meno che la velocità dell'asteroide non sia prossima o più alta della nostra velocità di lancio. La derivazione che abbiamo seguito può essere utilizzata per risolvere una varietà di problemi simili che potrebbero essere rappresentati sotto forma di un'equazione quadratica. 

Vorrei che tu sperimentassi ulteriormente aggiungendo l'effetto della gravità al moto dell'asteroide e del missile. Ciò cambierebbe il moto al movimento del proiettile e le equazioni corrispondenti cambierebbero. In bocca al lupo.

Nota anche che Unity ha un'economia attiva. Ci sono molti altri prodotti che ti aiutano a costruire il tuo progetto. La natura della piattaforma lo rende anche un'ottima opzione da cui puoi migliorare le tue abilità. In ogni caso, puoi vedere ciò che abbiamo a disposizione nel Marketplace Envato.