- Muoversi lungo un poligono convesso
e controllare se un punto è esterno o interno -
|
|||
COSA SERVE PER QUESTO TUTORIAL | |||
Download | Chiedi sul FORUM | Glossario | cognizioni basiche di C# e sul framework XNA | ||
Come impostare un "for poligonale" | |||
POLIGONI CONVESSI PER IL RILEVAMENTO DELLE COLLISIONI Perché è utile poter lavorare in maniera efficiente con poligoni convessi.
In questo tutorial ci occuperemo di poligoni convessi, ovvero con angoli
interni inferiori ai 180°. Anche se probabilmente la cosa non è
immediatamente evidente, essere in grado di maneggiare questo tipi di
figure agevolmente è fondamentale nella creazione di un videogioco, in
particolare per un'efficiente rilevamento delle collisioni. Infatti, nel
metodo
precedentemente
presentato si effettuavano controlli di collisione anche su parti di
intersezione dove non
potevano certamente esserci, limitandosi a lavorare su rettangoli. In sostanza ci
concentreremo su come sia possibile creare un ciclo che invece di
muoversi lungo un rettangolo si muova lungo un arbitrario poligono,
purché convesso. Se ci si sta chiedendo perché non trattiamo i poligoni
concavi, non è per eccessiva difficoltà, ma semplicemente perché
richiedono metodi diversi e più dispendiosi di risorse, che, ad esempio,
per il rilevamento delle collisioni sarebbero sprecate.
MUOVERSI LUNGO L'AREA DI UN POLIGONO CONVESSO Vediamo per prima cosa come prendiamo i vertici del poligono. public class ConvexPolygonGame : Microsoft.Xna.Framework.Game { // [...] // Memorizziamo lo stato del mouse di volta in volta per // poter rilevare l'evento di rilascio di un tasto private MouseState lastMouseState; // Lista dei vertici del poligono private List<Vector2> vertexes = new List<Vector2>(); // [...] protected override void Update(GameTime gameTime) { // [...] MouseState ms = Mouse.GetState(); // Se nel precedente Update il tasto sinistro del mouse era premuto e ora non lo è più if (lastMouseState.LeftButton == ButtonState.Pressed && ms.LeftButton == ButtonState.Released) { // Punto in cui si è fatto click Vector2 ptCurrent = new Vector2(ms.X, ms.Y); // Se non è già presente tra i vertici if (!vertexes.Contains(ptCurrent)) { // Aggiungiamo il punto cliccato alla lista dei vertici vertexes.Add(ptCurrent); } } // [...] // Memorizziamo lo stato del mouse corrente lastMouseState = ms; base.Update(gameTime); } // [...] }
Nel metodo Update, come già fatto in precedenti articoli rileviamo
quando viene rilasciato il pulsante del mouse in modo che non vengano
generati un numero eccessivo di vertici dovuti alla accidentale
pressione del tasto del mouse per un tempo troppo prolungato. Tutti i
vertici sono memorizzati nella variabile vertexes. È
importante notare capire subito che i vertici vanno in questo caso
forniti dall'utente in senso orario, per convenzione; in un prossimo
articolo ci occuperemo di vedere in dettaglio come è possibile ordinare
dei vertici forniti in ordine casuale in senso orario, operazione non
del tutto banale. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); // Disegniamo il poligono DrawPolygon(); // Disegnamo i vertici for (int CVertex = 0; CVertex < vertexes.Count; CVertex++) { spriteBatch.Draw(redSquare, new Rectangle((int)vertexes[CVertex].X, (int)vertexes[CVertex].Y, 5, 5), Color.White); } // [...] base.Draw(gameTime); } Dapprima viene chiamato il metodo DrawPolygon, vero e proprio cuore del tutorial, e poi vengono disegnati tutti vertici come quadratini rossi di 5x5 pixel. Arriviamo dunque al metodo DrawPolygon, a cominciare dalle variabili che utilizza: private void DrawPolygon() { // Abbiamo bisogno di almeno tre vertici per poter disegnare // un poligono if (vertexes.Count < 3) return; // Lista dei lati LinkedList<Side> sides = new LinkedList<Side>(); // Nodo della lista corrente su cui lavoreremo tra poco LinkedListNode<Side> currentNode = null; // Nodo della lista che delimita correntemente il poligono // a sinistra/destra LinkedListNode<Side> leftBoundNode = null; LinkedListNode<Side> rightBoundNode = null; // Coordinata Y del limite superiore del poligono. // Si tenga presente che l'asse Y procede dall'angolo // in alto a sinistra verso il basso. float topY = float.MaxValue; // Coordinata Y del limite inferiore del poligono float bottomY = 0; // [...] }
Per prima cosa verifica se ci sono almeno tre vertici, se così non fosse
non stiamo parlando di un poligono. Crea poi una LinkedList di strutture
Side (ovvero lati, come vedremo tra poco) che conterrà tutti i lati del
poligono collegati tra loro in senso orario. Ad esempio dato un elemento
della LinkedList<Side> (quindi di tipo
LinkedListNode<Side>) tramite le proprietà Next
e Previous sarà possibile ottenere rispettivamente il
lato successivo o precedente nell'ordinamento dato. Nota: si ricordi che il sistema di coordinate dello schermo ha l'asse delle Y che parte dall'angolo in alto a sinistra e scende verso il basso. Pertanto, per evitare confusione non ci riferiremo alle coordinate Y in termini di valore minore o maggiore ma di limite superiore e o inferiore. Vediamo dunque come viene popolata la linked-list, come vengono individuati i lati più in alto e i limiti inferiori e superiori del poligono: // Creiamo una linked-list di lati, individuiamo i due lati più in alto // che saranno i primi leftBoundNode e rightBoundNode e memorizziamo // le coordinate Y degli estremi del poligono (topY e bottomY) for (int C1 = 0; C1 < vertexes.Count; C1++) { // Indica il vertice successivo, ovvero quello con indice // successivo o il primo della lista se quello corrente è // l'ultimo, grazie all'operatore modulo. int nextIndex = (C1 + 1) % vertexes.Count; // Ignoriamo i lati orizzontali if (vertexes[C1].Y != vertexes[nextIndex].Y) { // Aggiungiamo in fondo alla linked-list il lato corrente // formato dal vertice corrente e dal successivo. currentNode = sides.AddLast(new Side(vertexes[C1], vertexes[nextIndex])); // Individuiamo il limite inferiore del poligono if (vertexes[C1].Y > bottomY) bottomY = vertexes[C1].Y; // Individuiamo il lato destro e sinistro più in alto // e al contempo troviamo la coordinata Y del limite // superiore del poligono // Individuiamo il lato che inizia più in alto // che sarà quindi il primo limite destro del poligono. if (currentNode.Value.Start <= topY) { topY = currentNode.Value.Start; rightBoundNode = currentNode; } // Individuiamo il lato che finisce più in alto // che sarà quindi il primo limite sinistro del poligono. if (currentNode.Value.End <= topY) { topY = currentNode.Value.End; leftBoundNode = currentNode; } } }
Sostanzialmente si effettua un ciclo lungo tutti i vertici del poligono
e per prima cosa si determina l'indice del vertice successivo, che è
sempre quello con indice di valore maggiore di uno eccetto quando siamo
giunti all'ultimo vertice: in tal caso prendiamo come vertice successivo
il primo vertice. Subito poi imponiamo la condizione di ignorare tutti i
lati orizzontali in quanto nella nostra scansione dall'alto al basso non
danno alcun contributo. // Struttura che contiene informazioni su un lato del poligono public struct Side { public float Start; public float End; public float XStep; public float StartX; // Costruiamo un lato a partire dai suoi vertici public Side(Vector2 pt1, Vector2 pt2) { // Memorizziamo dove inizia e dove finisce sull'asse Y Start = pt1.Y; End = pt2.Y; // Calcoliamo per ogni unità lungo le Y di cui ci muoviamo // quanto e in che direzione ci muoviamo sulle X (in sostanza // il coefficiente angolare della retta). XStep = (pt2.X - pt1.X) / (pt2.Y - pt1.Y); // Memorizziamo le coordinata X del punto più in alto, da cui // partiremo if (pt1.Y < pt2.Y) StartX = pt1.X; else StartX = pt2.X; } } La classe Side mantiene informazioni su un lato che ci saranno utili durante la scansione verticale:
Torniamo ora al nostro ciclo. Dopo aver aggiunto alla lista il nuovo lato, cerchiamo con un semplice if il limite inferiore del poligono. Per individuare i lati più in alto a sinistra e a destra invece basta cercare quelli che, rispettivamente, finiscono e iniziano più in alto. Usiamo questo controllo anche per determinare il punto più alto del poligono. Non ci resta ora che impostare quello che potremmo definire un "for poligonale": // Cursori che useremo per muoverci all'interno del poligono // sui due assi. float CY = topY; float CX = 0; // Limiti destro e sinistro entro il quale il cursore // lungo l'asse X dovrà muoversi. float xRightLimit = rightBoundNode.Value.StartX; float xLeftLimit = leftBoundNode.Value.StartX; // Effettuiamo una scansione dal limite superiore // a quello inferiore del poligono while (CY < bottomY) { // Posizioniamo il cursore X sul limite sinistro CX = xLeftLimit; // Ci muoviamo lungo l'asse X fino al limite destro while (CX <= xRightLimit) { // Disegniamo il pixel corrente spriteBatch.Draw(redSquare, new Rectangle((int)CX, (int)CY, 1, 1), Color.White); // Avanziamo lungo l'asse X CX += 1; } // Avanziamo lungo l'asse Y CY += 1; // Spostiamo i limiti sinistro e destro di un passo // a destra o a sinistra a seconda dell'inclinazione // del lato corrente. xLeftLimit += leftBoundNode.Value.XStep; xRightLimit += rightBoundNode.Value.XStep; // Se la coordinata Y a cui siamo giunti è al di fuori // del range di valori che copre il segmento destro // corrente passiamo al successivo if (CY > rightBoundNode.Value.End) { rightBoundNode = rightBoundNode.Next; // Se siamo giunti all'ultimo lato, passiamo al primo della lista if (rightBoundNode == null) rightBoundNode = sides.First; xRightLimit = rightBoundNode.Value.StartX; } // Come sopra, ma tutto è invertito per il lato sinistro if (CY > leftBoundNode.Value.Start) { leftBoundNode = leftBoundNode.Previous; if (leftBoundNode == null) leftBoundNode = sides.Last; xLeftLimit = leftBoundNode.Value.StartX; } }
Una volta definiti due cursori, un per le coordinate X (variabile
CX) e uno per le
Y (variabile CY), dichiariamo due variabili che indicheranno i valori limite
destro e sinistro entro cui si potrà muovere la coordinata X. Il cursore
Y parte dal valore del limite superiore del poligono.
ALGORITMO PER CONTROLLARE SE UN PUNTO È
ALL'INTERNO DI UN POLIGONO Vediamo ora la seconda funzionalità che presentiamo in questo tutorial: un semplice algoritmo per determinare se un punto selezionato dall'utente si trova all'interno o all'esterno del poligono, senza dover effettuare una scansione pixel per pixel. public class ConvexPolygonGame : Microsoft.Xna.Framework.Game { // [...] // Memorizziamo lo stato del mouse di volta in volta per // poter rilevare l'evento di rilascio di un tasto private MouseState lastMouseState; // Punto che conterrà le coordinate del punto che si // intende verificare se sia all'interno del poligono. // Per cambiarlo fare click destro nella finestra del gioco. private Vector2 checkPoint = new Vector2(); // Memorizziamo se il checkPoint è all'interno del poligono private bool isCheckPointInside = false; protected override void Update(GameTime gameTime) { // [...] MouseState ms = Mouse.GetState(); // [...] // Se nel precedente Update il tasto destro del mouse era premuto e ora non lo è più if (lastMouseState.RightButton == ButtonState.Pressed && ms.RightButton == ButtonState.Released) { // Aggiorniamo il checkPoint checkPoint = new Vector2(ms.X, ms.Y); // Verifichiamo se è all'interno del poligono o no isCheckPointInside = ConvexPolygonGame.IsInsideConvexPolygon(vertexes, checkPoint); } // Memorizziamo lo stato del mouse corrente lastMouseState = ms; base.Update(gameTime); } static bool IsInsideConvexPolygon(IList<Vector2> vertexes, Vector2 checkPoint) { // [...] } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); // [...] // Disegniamo il checkPoint spriteBatch.Draw(redSquare, new Rectangle((int)checkPoint.X, (int)checkPoint.Y, 5, 5), Color.White); spriteBatch.DrawString(arialFont, isCheckPointInside ? "in" : "out", new Vector2(checkPoint.X, checkPoint.Y) + new Vector2(3, 3), Color.Green); spriteBatch.End(); base.Draw(gameTime); } }
Sostanzialmente abbiamo una variabile pointCheck che viene impostata nel
metodo Update quando si clicca con il tasto destro del mouse; sempre in
Update viene richiamata la funzione IsInsideConvexPolygon, il cui
risultato viene salvato nella variabile isCheckPointInside che influirà
sul metodo Draw per disegnare sotto il punto selezionato la scritta "in"
o "out". static bool IsInsideConvexPolygon(IList<Vector2> vertexes, Vector2 checkPoint) { // Variabile in cui, dopo il primo ciclo, memorizzeremo // se il punto si trova a destra o a sinsitra del lato // così da poter verificare che sia così per ogni lato. sbyte sign = 0; // Cicliamo lungo tutti i vertici for (int C1 = 0; C1 < vertexes.Count; C1++) { // Prendiamo il punto corrente e il successivo Vector2 pt1 = vertexes[C1]; Vector2 pt2 = vertexes[(C1 + 1) % vertexes.Count]; // Sottraiamo (traslazione) al checkPoint e // e ad un vertice del lato corrente l'altro vertice // così da poter calcolare il segno dell'angolo // compreso tra il lato e il checkPoint tramite il // prodotto vettore (cross-product) Vector2 translatedCheckPoint = checkPoint - pt1; pt2 -= pt1; // Prendiamo il segno dell'angolo ottenuto tramite il // prodotto vettore. sbyte angleSign = (sbyte)Math.Sign(CrossProduct(pt2, translatedCheckPoint)); // Se è il primo ciclo if (sign == 0) { sign = angleSign; } else if (angleSign != sign) { // Se non è il primo ciclo e il segno dell'angolo // è differente dagli altri, siamo fuori dal poligono return false; } } // Abbiamo controllato tutti i lati, siamo all'interno del poligono return true; }
L'algoritmo funziona come segue: dato un punto per controllare se è
all'interno di un poligono convesso basta percorrere tutti i lati del
poligono in senso orario o anti-orario e verificare che stia,
rispettivamente, sempre a destra o a sinistra di ciascuno di essi. Se così
non fosse, il punto è esterno.
|
|||
<< INDIETRO | by VeNoM00 |