Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 33 di 53
  • livello principiante
Indice lezioni

Collision Detection, spazi e trasparenze

Come individuare le sovrapposizioni di oggetti parzialmente trasparenti
Come individuare le sovrapposizioni di oggetti parzialmente trasparenti
Link copiato negli appunti

Per capire se ci troviamo in presenza di una collisione tra gli oggetti disegnati a schermo, è necessario andare alla ricerca di due pixel, appartenenti a due oggetti differenti,con la stessa posizione sullo schermo e con canale alpha diverso da zero (questo perchè ci interessano solo le sovrapposizioni fra pixel NON trasparenti, come nel caso B della figura sopra).

Per aiutarci in questo compito creiamo un metodo InterstectPixels che accetti come parametri due rettangoli (corrispondenti alla "silouette" di ciascuna texture) e le informazioni relative al colore di ciascuna texture, contenute nei due array dataA e dataB.

static bool IntersectPixels(Rectangle rectangleA, Color[] dataA,
                            Rectangle rectangleB, Color[] dataB)

Creiamo ora un terzo rettangolo, che rappresenti l'area di intersezione dei due rettangoli ricevuti come parametro.

Le dimensioni di questo nuovo rettangolo possono essere calcolate utilizzando alcune proprietà esposte dalla struttura Rectangle:

int top    = Math.Max(rectangleA.Top,    rectangleB.Top);
int bottom = Math.Min(rectangleA.Bottom, rectangleB.Bottom);
int left   = Math.Max(rectangleA.Left,   rectangleB.Left);
int right  = Math.Min(rectangleA.Right,  rectangleB.Right);

Per comprendere il perché di queste operazioni, osserviamo la figura seguente:

Figura 4. Area di sovrapposizione fra textures

(clic per ingrandire)

Area di sovrapposizione fra textures

Un rettangolo viene disegnato partendo dal vertice superiore sinistro (indicato dalle coordinate Rectangle.X e Rectangle.Y), e sempre nel vertice superiore sinistro, ma dello schermo, è collocata l'origine del sistema di assi cartesiani (0, 0). Quanto alle quattro proprietà di cui sopra, esse indicano le coordinate sull'asse Y del bordo superiore (top) e di quello inferiore (bottom), nonché le coordinate sull'asse delle X del bordo sinistro (left) e di quello destro (right).

Come abbiamo visto, per definire l'area del rettangolo creato dall'intersezione dei due rettangoli più grandi sono necessari solo 2 punti (nel grafico sono i 4 valori indicati come X3, Y3, X4 e Y4) che permettono di individuare tutti e quattro i vertici del rettangolo.

Applicando il codice che abbiamo visto a quanto disegnato nella figura, avremo che:

  1. la Y del lato superiore del rettangolo colorato sarà pari al maggiore dei valori di Y dei corrispondenti lati dei due rettangoli originari, ossia Y3;
  2. la Y del lato inferiore sarà pari al minore dei valori di Y dei corrispondenti lati dei due rettangoli originari, ossia Y2;
  3. la X del lato sinistro sarà pari al maggiore dei valori di X dei corrispondenti lati dei due rettangoli originari, cioè X3;
  4. la X del lato destro sarà pari al minore dei valori di X dei corrispondenti lati dei due rettangoli originari, ossia X2.

L'area così ottenuta rappresenta dunque l'insieme di punti in cui le due texture si sovrappongono. Usiamo adesso due cicli for annidati per "scorrere" ciascun punto ricompreso nell'area in questione:

// Check every point within the intersection bounds
for (int y = top; y < bottom; y++)
{
  for (int x = left; x < right; x++)
  {

Per ciascun punto, leggiamo le informazioni relative al colore e alla trasparenza delle due texture sovrapposte, nel rispettivo array. Queste informazioni sono state indicizzate in un due array lineari (uno per ciascuna texture), perciò per recuperarle sono necessarie alcune operazioni di "aggiustamento" che tengano conto del fatto che per ogni riga (ogni valore della Y) è necessario saltare tutti gli elementi memorizzati in quella riga. Per questo moltiplichiamo la Y per la larghezza del rettangolo e sommiamo poi la X:

Color colorA = dataA[(x - rectangleA.Left) +
	                     (y - rectangleA.Top) * rectangleA.Width];
    Color colorB = dataB[(x - rectangleB.Left) +
	                     (y - rectangleB.Top) * rectangleB.Width];

Se il canale alpha è diverso da zero in entrambe le immagini, allora vuol dire che abbiamo una collisione, nel qual caso il metodo ritornerà il valore true:

// If both pixels are not completely transparent,
    if (colorA.A != 0 && colorB.A != 0)
    {
      // then an intersection has been found
      return true;
    }

Al contrario, se al termine del ciclo annidato non avremo trovato collisioni, il metodo ritornerà il valore false:

return false;

Ma cosa accade se i due rettangoli non presentano aree sovrapposte? Ecco che qui entra in gioco quel "filtro" cui accennavamo in precedenza. In questo caso il codice che abbiamo visto produrrà sì un rettangolo, che però non rappresenterà l'area di sovrapposizione, ma l'area del rettangolo che separa le due texture.

Figura 5. Mancata collisione fra textures

(clic per ingrandire)

Mancata collisione fra textures

Ne consegue che questo nuovo rettangolo si presenterà "invertito" rispetto al primo, nel senso che il lato top sarà adesso quello inferiore, mentre bottom coinciderà con il lato superiore (e lo stesso vale per i lati destro e sinistro):

Chi si stesse chiedendo quali siano le conseguenze di tale "inversione", provi a ripercorrere mentalmente il ciclo annidato che abbiamo visto sopra:

for (int y = top; y < bottom; y++)
{
...

Come si può agevolmente dedurre dal disegno, adesso top è maggiore di bottom! Ne consegue che la condizione y < bottom è falsa, con conseguente uscita immediata dal ciclo (il che ci risparmia dal dover andare a confrontare il colore dei singoli pixel). Ecco dunque il nostro "filtro"!

Una volta predisposto il metodo IntersectPixels, possiamo utilizzarlo come "helper" method per determinare se vi sia una sovrapposizione rilevante fra due texture:

public bool Collides(Rectangle my_position, ColliderTexture other, Rectangle other_position)
    {
      return IntersectPixels(my_position, texture_data, other_position, other.texture_data);
    }

Volendo, possiamo rendere un pò più flessibile il nostro codice. In genere, infatti, le texture sono associate a un vettore che esprime la posizione dello sprite sullo schermo, piuttosto che ad un rettangolo. Per determinare le dimensioni del rettangolo occupato da ciascuna texture, possiamo utilizzare un metodo ulteriore che, ricevuta come parametro la posizione della texture, restituisca il relativo rettangolo:

public Rectangle GetRectangle(Vector2 position)
  {
    return new Rectangle((int)position.X - Texture.Width / 2, (int)position.Y - Texture.Height / 2,
	                     Texture.Width, Texture.Height);
  }

Ecco un esempio di come "combinare" i due metodi di cui sopra:

public bool Collides(Vector2 my_position, ColliderTexture other, Vector2 other_position)
    {
      return IntersectPixels(this.GetRectangle(my_position), texture_data, other.GetRectangle(other_position), other.texture_data);
    }

Ti consigliamo anche