Coding Destructible Pixel Terrain come far esplodere tutto

In questo tutorial, implementeremo terreni pixel completamente distruttibili, nello stile di giochi come Cortex Command e Worms. Imparerai come far esplodere il mondo ovunque lo spari e come far depositare la "polvere" sul terreno per creare nuova terra.

Nota: Sebbene questo tutorial sia scritto in Processing e compilato con Java, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Anteprima del risultato finale

Puoi anche fare la demo da solo. WASD per spostare, clic sinistro per sparare proiettili esplosivi, fare clic destro per spruzzare pixel.


Step 1: The Terrain

Nella nostra sandbox a scorrimento laterale, il terreno sarà la meccanica di base del nostro gioco. Gli algoritmi simili hanno spesso un'immagine per la texture del terreno, e un'altra come una maschera in bianco e nero per definire quali pixel sono solidi. In questa demo, il terreno e la sua trama sono tutti un'immagine, ei pixel sono solidi in base al fatto che siano trasparenti o meno. L'approccio maschera sarebbe più appropriato se si desidera definire le proprietà di ciascun pixel, ad esempio quanto probabile si sloggerà o quanto rimbalzerà il pixel sarà.

Per rendere il terreno, la sandbox disegna prima i pixel statici, quindi i pixel dinamici con tutto il resto in cima.

Il terreno ha anche metodi per scoprire se un pixel statico in una posizione è solido o meno, e metodi per rimuovere e aggiungere pixel. Probabilmente il modo più efficace di archiviare l'immagine è come un array 1-dimensionale. Ottenere un indice 1D da una coordinata 2D è piuttosto semplice:

indice = x + y * larghezza

Affinché i pixel dinamici rimbalzino, dovremo essere in grado di scoprire la superficie normale in qualsiasi momento. Passa attraverso un'area quadrata attorno al punto desiderato, trova tutti i pixel solidi vicini e media la loro posizione. Prendi un vettore da quella posizione al punto desiderato, inverti e normalizzalo. C'è il tuo normale!

Le linee nere rappresentano le normali al terreno in vari punti.

Ecco come appare nel codice:

 normal (x, y) Vector avg per x = da -3 a 3 // 3 è un numero arbitrario per y = da -3 a 3 // numeri maggiori dell'utente per superfici più lisce se il pixel è solido su (x + w, y + h) avg - = (x, y) lunghezza = sqrt (avgX * avgX + avgY * avgY) // distanza da avg al centro ritorno avg / length // normalizza il vettore dividendo per quella distanza

Passaggio 2: Dynamic Pixel and Physics

Il "Terreno" stesso memorizza tutti i pixel statici non in movimento. I pixel dinamici sono pixel attualmente in movimento e sono memorizzati separatamente dai pixel statici. Mentre il terreno esplode e si assesta, i pixel vengono commutati tra stati statici e dinamici mentre si sloggiano e si scontrano. Ogni pixel è definito da un numero di proprietà:

  • Posizione e velocità (richiesto per il lavoro fisico).
  • Non solo la posizione, ma anche la posizione precedente del pixel. (Possiamo scansionare tra i due punti per rilevare le collisioni.)
  • Altre proprietà includono il colore, la viscosità e la morbidezza del pixel.

Affinché il pixel si muova, la sua posizione deve essere inoltrata con la sua velocità. L'integrazione di Eulero, sebbene imprecisa per simulazioni complesse, è abbastanza semplice per noi per spostare in modo efficiente le nostre particelle:

posizione = posizione + velocità * tempo trascorso

Il tempo trascorso è la quantità di tempo trascorso dall'ultimo aggiornamento. La precisione di qualsiasi simulazione può essere completamente interrotta se il tempo trascorso è troppo variabile o troppo grande Questo non è tanto un problema per i pixel dinamici, ma lo sarà per altri schemi di rilevamento delle collisioni.

Useremo timestep di dimensioni fisse, prendendo il tempo trascorso e dividendolo in pezzi di dimensioni costanti. Ogni chunk è un "aggiornamento" completo alla fisica, con qualsiasi rimanente inviato nel frame successivo.

 elapsedTime = lastTime - currentTime lastTime = currentTime // reset lastTime // aggiungi tempo che non può essere utilizzato last frame elapsedTime + = leftOverTime // dividerlo in blocchi di 16 ms timesteps = floor (elapsedTime / 16) // store time non potremmo usare per il fotogramma successivo. leftOverTime = elapsedTime - timesteps per (i = 0; i < timesteps; i++)  update(16/1000) // update physics 

Passaggio 3: rilevamento collisione

Rilevare le collisioni per i nostri pixel volanti è semplice come disegnare alcune linee.

L'algoritmo di Bresenham è stato sviluppato nel 1962 da un gentiluomo di nome Jack E. Bresenham. Fino ad oggi, è stato utilizzato per disegnare linee alias semplici in modo efficiente. L'algoritmo si attacca rigorosamente agli interi e utilizza principalmente addizioni e sottrazioni per ottenere linee di trama efficaci. Oggi lo useremo per uno scopo diverso: il rilevamento delle collisioni.

Sto usando il codice preso in prestito da un articolo su gamedev.net. Mentre la maggior parte delle implementazioni dell'algoritmo di riga di Bresenham riordina l'ordine di disegno, questo particolare ci consente di scansionare sempre dall'inizio alla fine. L'ordine è importante per il rilevamento delle collisioni, altrimenti rileveremo collisioni nella parte sbagliata del percorso del pixel.

La pendenza è una parte essenziale dell'algoritmo di linea di Bresenham. L'algoritmo funziona dividendo la pendenza nelle sue componenti "ascesa" e "corsa". Se, per esempio, la pendenza della linea fosse 1/2, possiamo tracciare la linea posizionando due punti orizzontalmente, andando su (e destra) uno, e poi altri due.

L'algoritmo che mostro qui rappresenta tutti gli scenari, indipendentemente dal fatto che le linee abbiano una pendenza positiva o negativa o verticale. L'autore spiega come lo ottiene su gamedev.net.

rayCast (int startX, int startY, int lastX, int lastY) int deltax = (int) abs (lastX - startX) int deltay = (int) abs (lastY - startY) int x = (int) startX int y = ( int) startY int xinc1, xinc2, yinc1, yinc2 // Determina se xey aumenta o diminuisce se (lastX> = startX) // I valori x sono in aumento xinc1 = 1 xinc2 = 1 else // i valori x stanno diminuendo xinc1 = -1 xinc2 = -1 if (lastY> = startY) // I valori y stanno aumentando yinc1 = 1 yinc2 = 1 else // I valori y sono decrescenti yinc1 = - 1 yinc2 = -1 int den, num, numadd, numpixels if (deltax> = deltay) // C'è almeno un valore x per ogni valore y xinc1 = 0 // Non modificare x quando il numeratore > = denominator yinc2 = 0 // Non modificare y per ogni iterazione den = deltax num = deltax / 2 numadd = deltay numpixels = deltax // Ci sono più valori x che valori y else // C'è almeno un valore y per ogni valore x xinc2 = 0 // Non modificare la x per ogni iterazione yinc1 = 0 // Do not ch ange the when numerator> = denominator den = deltay num = deltay / 2 numadd = deltax numpixels = deltay // Ci sono più valori y che valori x int prevX = (int) startX int prevY = (int) startY per (int curpixel = 0; curpixel <= numpixels; curpixel++)  if (terrain.isPixelSolid(x, y)) return (prevX, prevY) and (x, y) prevX = x prevY = y num += numadd // Increase the numerator by the top of the fraction if (num >= den) // Controlla se numeratore> = denominatore num - = den // Calcola il nuovo valore numeratore x + = xinc1 // Cambia la x come appropriato y + = yinc1 // Cambia la y come appropriato x + = xinc2 // Cambia la x come appropriato y + = yinc2 // Cambia la y come appropriato return null // non è stato trovato nulla

Passaggio 4: Gestione delle collisioni

Il pixel dinamico può fare una delle due cose durante una collisione.

  • Se si muove abbastanza lentamente, il pixel dinamico viene rimosso e uno statico viene aggiunto al terreno in cui è colliso. Attaccare sarebbe la nostra soluzione più semplice. Nell'algoritmo di riga di Bresenham, è meglio tenere traccia di un punto precedente e un punto corrente. Quando viene rilevata una collisione, il "punto corrente" sarà il primo pixel solido colpito dal raggio, mentre il "punto precedente" è lo spazio vuoto appena prima di esso. Il punto precedente è esattamente la posizione in cui dobbiamo incollare il pixel.
  • Se si muove troppo velocemente, lo rimbalziamo dal terreno. È qui che entra in gioco il nostro normale algoritmo di superficie! Rifletti la velocità iniziale della palla attraverso il normale per farla rimbalzare.
  • L'angolo su entrambi i lati del normale è lo stesso.

 // Proiettate la velocità sulla normale, moltiplicatela per 2 e sottraetela alla velocità normale = getNormal (collision.x, collision.y) // velocità del progetto sul normale usando dot product projection = velocity.x * normal.x + velocity .y * normal.y // velocity - = normal * proiezione * 2

Step 5: Bullets ed Explosions!

I punti elenco funzionano esattamente come i pixel dinamici. Il movimento è integrato allo stesso modo e il rilevamento delle collisioni utilizza lo stesso algoritmo. La nostra unica differenza è la gestione delle collisioni

Dopo che una collisione è stata rilevata, i proiettili esplodono rimuovendo tutti i pixel statici all'interno di un raggio, quindi posizionando i pixel dinamici nella loro posizione con le loro velocità puntate verso l'esterno. Uso una funzione per scansionare un'area quadrata attorno al raggio di un'esplosione per scoprire quali pixel rimuovere. Successivamente, la distanza del pixel dal centro viene utilizzata per stabilire una velocità.

 esplodere (x, y, raggio) per (xPos = x - raggio; xPos <= x + radius; xPos++)  for (yPos = y - radius; yPos <= y + radius; yPos++)  if (sq(xPos - x) + sq(yPos - y) < radius * radius)  if (pixel is solid)  remove static pixel add dynamic pixel     

Step 6: The Player

Il giocatore non è una parte fondamentale della meccanica del terreno distruttabile, ma implica un rilevamento di collisione che sarà sicuramente rilevante per i problemi che arriveranno in futuro. Spiegherò come viene rilevata la collisione e gestita nella demo per il giocatore.

  1. Per ogni bordo, loop da un angolo al successivo, controllando ogni pixel.
  2. Se il pixel è solido, iniziare dal centro del lettore e scansionare verso quel pixel in cui si colpisce un pixel solido.
  3. Allontana il giocatore dal primo pixel solido colpito.

Passaggio 7: ottimizzazione

Migliaia di pixel vengono gestiti contemporaneamente, causando un notevole sforzo sul motore fisico. Come qualsiasi altra cosa, per velocizzare questo consiglio, raccomanderei l'uso di un linguaggio ragionevolmente veloce. La demo è compilata in Java.

Puoi fare cose da ottimizzare anche a livello di algoritmo. Ad esempio, il numero di particelle da esplosioni può essere ridotto abbassando la risoluzione di distruzione. Normalmente troviamo ogni pixel e lo trasformiamo in un pixel dinamico 1x1. Invece, scansiona ogni 2x2 pixel, o 3x3, e lancia un pixel dinamico di quelle dimensioni. Nella demo usiamo 2x2 pixel.

Se stai usando Java, la garbage collection sarà un problema. La JVM troverà periodicamente oggetti in memoria che non vengono più utilizzati, come i pixel dinamici che vengono scartati in cambio di pixel statici e cercano di sbarazzarsi di quelli per fare spazio a più oggetti. Cancellare oggetti, tonnellate di oggetti, richiede tempo, e ogni volta che la JVM esegue una pulizia, il gioco si bloccherà brevemente.

Una delle possibili soluzioni è usare una cache di qualche tipo. Invece di creare / distruggere oggetti per tutto il tempo, puoi semplicemente tenere gli oggetti morti (come i pixel dinamici) da riutilizzare in seguito.

Usa i primitivi ovunque sia possibile. Ad esempio, l'uso di oggetti per posizioni e velocità renderà le cose un po 'più difficili per la garbage collection. Sarebbe ancora meglio se potessi memorizzare tutto come primitive in matrici unidimensionali.


Passaggio 8: rendilo tuo

Ci sono molte direzioni diverse che puoi prendere con questa meccanica di gioco. Le funzionalità possono essere aggiunte e personalizzate per adattarsi a qualsiasi stile di gioco che desideri.

Ad esempio, le collisioni tra pixel dinamici e statici possono essere gestite in modo diverso. Una maschera di collisione sotto il terreno può essere utilizzata per definire la viscosità, la forza e la forza di ciascun pixel statico, o la probabilità di essere spostati da un'esplosione.

Ci sono una varietà di cose diverse che puoi fare alle pistole pure. I proiettili possono avere una "profondità di penetrazione", per consentirgli di muoversi attraverso tanti pixel prima di esplodere. È possibile applicare anche la meccanica della pistola tradizionale, come una varia velocità di fuoco o, come un fucile, è possibile sparare più proiettili contemporaneamente. Puoi persino, come per le particelle rimbalzanti, far rimbalzare i proiettili dai pixel metallici.


Conclusione

La distruzione del terreno 2D non è completamente unica. Ad esempio, i classici Worms and Tanks rimuovono parti del terreno in caso di esplosioni. Il Comando Cortex utilizza particelle rimbalzanti simili che usiamo qui. Altri giochi potrebbero pure, ma non ho ancora sentito parlare di loro. Non vedo l'ora di vedere ciò che altri sviluppatori faranno con questa meccanica.

La maggior parte di ciò che ho spiegato qui è completamente implementato nella demo. Si prega di dare un'occhiata alla sua fonte se tutto sembra ambiguo o confuso. Ho aggiunto commenti alla fonte per renderla il più chiara possibile. Grazie per aver letto!