Koding ødeleggende pixel terreng Hvordan gjøre alt eksplodere

I denne veiledningen vil vi implementere fullt ødeleggende piksel terreng, i stil med spill som Cortex Command og Worms. Du lærer hvordan du får verden til å eksplodere hvor du skyter den - og hvordan du får "støv" å slå seg ned på bakken for å skape nytt land.

Merk: Selv om denne opplæringen er skrevet i Behandling og kompilert med Java, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.


Endelig resultatforhåndsvisning

Du kan også spille demoen selv. WASD å flytte, venstre klikk for å skyte eksplosive kuler, høyreklikk for å spraye piksler.


Trinn 1: Terrenget

I vår sidescrolling sandkasse vil terrenget være kjernemekaniker i vårt spill. Lignende algoritmer har ofte ett bilde for terrengets tekstur, og en annen som en svart og hvit maske for å definere hvilke piksler som er faste. I denne demonstrasjonen er terrenget og dets tekstur alle ett bilde, og piksler er solide basert på om de er transparente eller ikke. Maske-tilnærmingen ville være mer hensiktsmessig hvis du vil definere hver pikselets egenskaper, som hvor sannsynlig det vil løsne eller hvor spenningen piksen blir.

For å gjøre terrenget, drar sandkassen de statiske pikslene først, deretter de dynamiske pikslene med alt annet på toppen.

Terrenget har også metoder for å finne ut om en statisk piksel på et sted er solid eller ikke, og metoder for å fjerne og legge til piksler. Sannsynligvis er den mest effektive måten å lagre bildet på som en 1-dimensjonal matrise. Å få en 1D-indeks fra en 2D-koordinat er ganske enkel:

indeks = x + y * bredde

For at dynamiske piksler skal sprette, må vi kunne finne ut overflaten normal når som helst. Gå gjennom et firkantet område rundt ønsket punkt, finn alle de faste pikslene i nærheten og gjennomsnitts deres posisjon. Ta en vektor fra den posisjonen til ønsket punkt, reverser og normaliser det. Det er normalt!

De svarte linjene representerer normaler til terrenget på ulike punkter.

Slik ser det ut i kode:

 Normal (x, y) Vector avg for x = -3 til 3 // 3 er et vilkårlig tall for y = -3 til 3 // bruker større tall for jevnere overflater hvis piksel er solid ved (x + w, y + h) avg - = (x, y) lengde = sqrt (avgX * avgX + avgY * avgY) // avstand fra avg til senterets retur avg / lengde // normaliser vektoren ved å dividere med den avstanden

Trinn 2: Dynamisk piksel og fysikk

Terrenget selv lagrer alle de ikke-bevegelige statiske pikslene. Dynamiske piksler er piksler i gang, og lagres separat fra de statiske pikslene. Når terrenget eksploderer og bosetter seg, blir piksler skiftet mellom statiske og dynamiske tilstander etter hvert som de løsner og kolliderer. Hver piksel er definert av et antall egenskaper:

  • Posisjon og hastighet (kreves for fysikken til å fungere).
  • Ikke bare plasseringen, men også pikselets forrige plassering. (Vi kan skanne mellom de to punktene for å oppdage kollisjoner.)
  • Andre egenskaper inkluderer pikselets farge, klebrighet og bounciness.

For at pikselet skal bevege seg, må posisjonen sin videresendes med sin hastighet. Euler-integrasjon, mens unøyaktig for komplekse simuleringer, er enkel nok til at vi effektivt kan flytte partiklene våre:

posisjon = posisjon + hastighet * gåttid

De elapsedTime er mengden tid som er gått siden siste oppdatering. Nøyaktigheten av enhver simulering kan være helt ødelagt hvis elapsedTime er for variabel eller for stor. Dette er ikke så mye av et problem for dynamiske piksler, men det vil være for andre kollisjonsdeteksjonsordninger.

Vi bruker tidsbegrensede tidsbegrensninger, ved å ta den forløpte tiden og dele den opp i kortere størrelse. Hver bit er en full "oppdatering" til fysikken, med noen igjen overført til neste ramme.

 elapsedTime = lastTime - currentTime lastTime = currentTime // tilbakestilt sistTime // legg til tid som ikke kunne brukes siste ramme forløptTime + = leftOverTime // dele det opp i biter på 16 ms timesteps = gulv (forløpt tid / 16) // lagringstid vi kunne ikke bruke til neste ramme. leftOverTime = forsinketTime - timesteps for (i = 0; i < timesteps; i++)  update(16/1000) // update physics 

Trinn 3: Kollisjonsdeteksjon

Registrering av kollisjoner for våre flygende piksler er like enkelt som å tegne noen linjer.

Bresenhams linjealgoritme ble utviklet i 1962 av en gentleman ved navn Jack E. Bresenham. Hittil har den blitt brukt til å tegne enkle aliased linjer effektivt. Algoritmen stikker strengt til heltall og bruker for det meste tillegg og subtraksjon for å efficeintly plot linjer. I dag bruker vi det til et annet formål: kollisjonsdeteksjon.

Jeg bruker kode lånt fra en artikkel på gamedev.net. Mens de fleste implementeringer av Bresenhams linjalgoritme reorders tegningsordren, tillater dette oss alltid å skanne fra start til slutt. Ordren er viktig for kollisjonsdeteksjon, ellers vil vi oppdage kollisjoner i feil ende av pikselbanen.

Hellingen er en viktig del av Bresenhams linjealgoritme. Algoritmen fungerer ved å dele opp skråningen i "stige" og "løpe" komponentene. Hvis for eksempel linjens helling var 1/2, kan vi plotte linjen ved å plassere to punkter horisontalt, gå opp (og til høyre) en, og deretter to flere.

Algoritmen jeg viser her står for alle scenarier, om linjene har en positiv eller negativ helling, eller om den er vertikal. Forfatteren forklarer hvordan han oppdager den på gamedev.net.

rayCast (int startX, int startY, int lastX, int sistY) 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 // Bestem om x og y øker eller avtar hvis (lastX> = startX) // x-verdiene øker xinc1 = 1 xinc2 = 1 else // The x-verdiene er avtagende xinc1 = -1 xinc2 = -1 hvis (lastY> = startY) // Y-verdiene øker yinc1 = 1 yinc2 = 1 else // Y-verdiene faller yinc1 = - 1 yinc2 = -1 int den, num, numadd, numpixels hvis (deltax> = deltay) // Det er minst en x-verdi for hver y-verdi xinc1 = 0 // Ikke endre x når teller > = nevner yinc2 = 0 // Ikke forandre y for hver iterasjon den = deltax num = deltax / 2 numadd = deltay numpixels = deltax // Det er flere x-verdier enn y-verdier ellers // Det er minst en y-verdi for hver x-verdi xinc2 = 0 // Ikke endre x for hver iterasjon yinc1 = 0 // Ikke ch angi y når teller> = nevner den = deltag num = deltay / 2 numadd = deltax numpixels = deltay // Det er flere y-verdier enn x-verdier int prevX = (int) startX int prevY = (int) startY for (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) // Sjekk om teller> = nevner num - = den // Beregn den nye tellerverdien x + = xinc1 // Endre x som passende y + = yinc1 // Endre y etter behov x + = xinc2 // Endre x som hensiktsmessig y + = yinc2 // Endre y etter behov returnere null // ingenting ble funnet

Trinn 4: Kollisjonshåndtering

Den dynamiske piksel kan gjøre en av to ting under en kollisjon.

  • Hvis den beveger seg sakte nok, blir den dynamiske pikselen fjernet og en statisk er lagt til terrenget der den kolliderte. Sticking ville være vår enkleste løsning. I Bresenhams linjealgoritme er det best å holde rede på et tidligere punkt og et nåværende punkt. Når en kollisjon blir oppdaget, blir det nåværende punktet det første faste pikselet strålen treffer, mens "forrige punkt" er det tomme rommet like før det. Det forrige punktet er akkurat det stedet vi trenger å holde pikselet.
  • Hvis den beveger seg for fort, spretter vi den av terrenget. Det er her vår overflate normale algoritme kommer inn! Reflekter ballens innledende hastighet over det normale for å sprette det.
  • Vinkelen på hver side av det normale er det samme.

 // Prosjekthastighet på normal, multipliser med 2, og trekk den fra hastighet normal = getNormal (collision.x, collision.y) // prosesshastighet på normal ved hjelp av punktproduktprojeksjon = hastighet.x * normal.x + hastighet .y * normal.y // hastighet - = normal * projeksjon * 2

Trinn 5: Kuler og eksplosjoner!

Kuler virker akkurat som dynamiske piksler. Bevegelsen er integrert på samme måte, og kollisjonsdeteksjon bruker samme algoritme. Vår eneste forskjell er kollisjonshåndtering

Når en kollisjon er oppdaget, eksploderer kuler ved å fjerne alle statiske piksler i en radius, og deretter plasserer dynamiske piksler på plass med hastigheter pekte utover. Jeg bruker en funksjon til å skanne et firkantet område rundt en eksplosjons radius for å finne ut hvilke piksler som skal løsnes. Etterpå brukes pikselens avstand fra sentrum for å etablere en hastighet.

 eksplodere (x, y, radius) for (xPos = x - radius; 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     

Trinn 6: Spilleren

Spilleren er ikke en kjerne del av den ødeleggende terrengmekanikeren, men det innebærer noen kollisjonsdeteksjon som vil være definitivt relevant for problemer som kommer fremover i fremtiden. Jeg vil forklare hvordan kollisjon blir oppdaget og håndtert i demoen for spilleren.

  1. For hver kant, loop fra ett hjørne til det neste, kontroller hver piksel.
  2. Hvis pikselet er solid, start i midten av spilleren og skann mot den piksel inn i du treffer en solid piksel.
  3. Flytt spilleren bort fra det første solide pikselet du treffer.

Trinn 7: Optimalisering

Tusenvis av piksler blir håndtert samtidig, noe som resulterer i litt belastning på fysikkmotoren. Som noe annet, for å gjøre det fort, anbefaler jeg at du bruker et språk som er rimelig raskt. Demoen er kompilert i Java.

Du kan også gjøre ting for å optimalisere på algoritmenivået også. For eksempel kan antall partikler fra eksplosjoner reduseres ved å redusere ødeleggelsesoppløsningen. Normalt finner vi hver piksel og gjør den til en 1x1 dynamisk piksel. I stedet skann hver 2x2 piksler, eller 3x3, og start en dynamisk piksel av den størrelsen. I demoen bruker vi 2x2 piksler.

Hvis du bruker Java, vil søppelinnsamling være et problem. JVM vil med jevne mellomrom finne objekter i minnet som ikke blir brukt lenger, som de dynamiske pikslene som kasseres i bytte for statiske piksler, og prøver å kvitte seg med dem som gir plass til flere objekter. Sletter objekter, tonnevis av objekter, tar tid skjønt, og hver gang JVM gjør en opprydding, fryser spillet vårt kort.

En mulig løsning det å bruke en cache av noe slag. I stedet for å lage / ødelegge objekter hele tiden, kan du bare holde døde gjenstander (som dynamiske piksler) som skal gjenbrukes senere.

Bruk primitive midler der det er mulig. For eksempel vil bruk av objekter for posisjoner og hastigheter gjøre ting litt vanskeligere for søppelinnsamlingen. Det ville være enda bedre hvis du kunne lagre alt som primitiver i endimensjonale arrays.


Trinn 8: Gjør det ditt eget

Det er mange forskjellige retninger du kan ta med denne spillmekanikeren. Funksjoner kan legges til og tilpasses for å matche hvilken som helst spillestil du ønsker.

For eksempel kan kollisjoner mellom dynamiske og statiske piksler håndteres annerledes. En kollisjonsmaske under terrenget kan brukes til å definere hver statisk pikselklebighet, bounciness og styrke, eller sannsynlighet for å bli løsnet av en eksplosjon.

Det er en rekke forskjellige ting du kan gjøre for våpen også. Kuler kan gis en "penetreringsdybde", slik at den kan bevege seg gjennom så mange piksler før eksploderingen. Tradisjonell pistolmekanikk kan også brukes, som en variert brannfrekvens, eller som et hagle, kan flere kuler avfyres samtidig. Du kan til og med, som for bouncypartiklene, ha kuler sprette av metallpiksler.


Konklusjon

2D terreng ødeleggelse er ikke helt unik. For eksempel fjerner klassikerne Worms and Tanks deler av terrenget på eksplosjoner. Cortex Command benytter lignende hoppende partikler som vi bruker her. Andre spill kan også, men jeg har ikke hørt om dem ennå. Jeg gleder meg til å se hvilke andre utviklere som vil gjøre med denne mekanikeren.

Mesteparten av det jeg har forklart her er fullt implementert i demonstrasjonen. Ta en titt på kilden hvis noe virker tvetydig eller forvirrende. Jeg har lagt til kommentarer til kilden for å gjøre det så klart som mulig. Takk for at du leste!