Coding Destructible Pixel Terrain alles laten ontploffen

In deze tutorial zullen we volledig vernietigbare pixelterreinen implementeren, in de stijl van games zoals Cortex Command en Worms. Je leert hoe je de wereld laat ontploffen waar je hem ook fotografeert - en hoe je het "stof" op de grond laat zakken om nieuw land te creëren.

Notitie: Hoewel deze tutorial geschreven is in Processing en gecompileerd met Java, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Eindresultaat voorbeeld

Je kunt de demo ook zelf spelen. WASD om te bewegen, klik met de linkermuisknop om explosieve kogels te schieten, klik met de rechtermuisknop om pixels te spuiten.


Stap 1: Het terrein

In onze sidescrolling sandbox is het terrein de kern van onze game. Vergelijkbare algoritmen hebben vaak één afbeelding voor de textuur van het terrein en een ander als een zwart-wit masker om te definiëren welke pixels solide zijn. In deze demo zijn het terrein en de textuur allemaal één beeld en de pixels zijn solide op basis van het feit of ze al dan niet transparant zijn. De maskeraanpak zou meer geschikt zijn als u de eigenschappen van elke pixel wilt definiëren, zoals hoe waarschijnlijk het zal losraken of hoe veerkrachtig de pixel zal zijn.

Om het terrein weer te geven, tekent de sandbox eerst de statische pixels en vervolgens de dynamische pixels met al het andere bovenop.

Het terrein heeft ook methoden om uit te zoeken of een statische pixel op een locatie solide is of niet, en methoden om pixels te verwijderen en toe te voegen. Waarschijnlijk de meest effectieve manier om het beeld op te slaan is als een 1-dimensionale array. Een 1D-index verkrijgen van een 2D-coördinaat is vrij eenvoudig:

index = x + y * breedte

Als dynamische pixels moeten stuiteren, moeten we op elk gewenst moment het normaal oppervlak kunnen bepalen. Loop door een vierkant gebied rond het gewenste punt, vind alle vaste pixels in de buurt en gemiddelde hun positie. Neem een ​​vector van die positie naar het gewenste punt, keer achteruit en normaliseer het. Daar is je normaal!

De zwarte lijnen vertegenwoordigen de normaalwaarden van het terrein op verschillende punten.

Dit is hoe dat eruit ziet in de code:

 normaal (x, y) Vector avg voor x = -3 tot 3 // 3 is een willekeurig getal voor y = -3 tot 3 // grotere getallen voor vloeiendere oppervlakken als de pixel vast is op (x + w, y + h) avg - = (x, y) length = sqrt (avgX * avgX + avgY * avgY) // afstand van avg tot het middelste rendement avg / lengte // de vector normaliseren door te delen door die afstand

Stap 2: De dynamische pixel en fysica

Het "Terrain" zelf slaat alle niet-bewegende statische pixels op. Dynamische pixels zijn pixels die momenteel in beweging zijn en worden afzonderlijk van de statische pixels opgeslagen. Terwijl het terrein explodeert en bezinkt, worden pixels geschakeld tussen statische en dynamische statussen wanneer ze losraken en botsen. Elke pixel wordt bepaald door een aantal eigenschappen:

  • Positie en snelheid (vereist voor de fysica om te werken).
  • Niet alleen de locatie, maar ook de vorige locatie van de pixel. (We kunnen tussen de twee punten scannen om botsingen te detecteren.)
  • Andere eigenschappen zijn de kleur, kleverigheid en veerkracht van de pixel.

Om de pixel te laten bewegen, moet zijn positie worden doorgestuurd met zijn snelheid. Euler-integratie, hoewel onnauwkeurig voor complexe simulaties, is eenvoudig genoeg om onze deeltjes efficiënt te verplaatsen:

positie = positie + snelheid * elapsedTime

De verstreken tijd is de tijd die is verstreken sinds de laatste update. De nauwkeurigheid van elke simulatie kan volledig worden verbroken als de verstreken tijd is te variabel of te groot. Dit is niet zozeer een probleem voor dynamische pixels, maar voor andere botsingsdetectieschema's.

We gebruiken timesteps van een vast formaat door de verstreken tijd te nemen en deze op te splitsen in brokken van constante grootte. Elk deel is een volledige "update" van de fysica, waarbij alle overgebleven bestanden naar het volgende frame worden verzonden.

 elapsedTime = lastTime - currentTime lastTime = currentTime // reset lastTime // voeg tijd toe die niet gebruikt kon worden laatste frame elapsedTime + = leftOverTime // deel het op in chunks van 16 ms timesteps = floor (elapsedTime / 16) // winkel tijd we konden het niet gebruiken voor het volgende frame. leftOverTime = elapsedTime - timesteps voor (i = 0; i < timesteps; i++)  update(16/1000) // update physics 

Stap 3: Collision Detection

Het detecteren van botsingen voor onze vliegende pixels is net zo eenvoudig als het tekenen van enkele lijnen.

Het lijnalgoritme van Bresenham werd in 1962 ontwikkeld door een heer genaamd Jack E. Bresenham. Tot op de dag van vandaag wordt het gebruikt voor het efficiënt tekenen van eenvoudige aliased lijnen. Het algoritme houdt zich strikt aan de gehele getallen en gebruikt meestal optellen en aftrekken om de lijnen uit te lijnen. Vandaag zullen we het voor een ander doel gebruiken: botsingsdetectie.

Ik gebruik code die is geleend van een artikel op gamedev.net. Hoewel de meeste implementaties van het lijnalgoritme van Bresenham de tekenvolgorde opnieuw ordenen, kunnen we met deze specifieke scan altijd van begin tot eind scannen. De volgorde is belangrijk voor botsingsdetectie, anders zullen we botsingen detecteren aan het verkeerde einde van het pad van de pixel.

De helling is een essentieel onderdeel van het lijnalgoritme van Bresenham. Het algoritme werkt door de helling op te splitsen in de componenten "stijgen" en "rennen". Als de hellingshoek van de lijn bijvoorbeeld 1/2 was, kunnen we de lijn plotten door twee punten horizontaal te plaatsen, een stijging (en rechts) een en vervolgens nog twee.

Het algoritme dat ik hier weergeef, geeft alle scenario's weer, of de lijnen een positieve of negatieve helling hebben of dat het verticaal is. De auteur legt uit hoe hij het afleidt op 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 // Bepaal of x en y toeneemt of afneemt als (lastX> = startX) // De x-waarden worden groter xinc1 = 1 xinc2 = 1 else // The x-waarden nemen af ​​xinc1 = -1 xinc2 = -1 if (lastY> = startY) // De y-waarden nemen toe yinc1 = 1 yinc2 = 1 else // de y-waarden nemen af ​​yinc1 = - 1 yinc2 = -1 int den, num, numadd, numpixels if (deltax> = deltay) // Er is minstens één x-waarde voor elke y-waarde xinc1 = 0 // Wijzig de x niet wanneer de teller > = noemer yinc2 = 0 // Wijzig de y niet voor elke iteratie den = deltax num = deltax / 2 numadd = deltay numpixels = deltax // Er zijn meer x-waarden dan y-waarden else // Er is minstens één y-waarde voor elke x-waarde xinc2 = 0 // verander de x niet voor elke iteratie yinc1 = 0 // niet ch ange the y wanneer teller = = denominator den = deltay num = deltay / 2 numadd = deltax numpixels = deltay // Er zijn meer y-waarden dan x-waarden int prevX = (int) startX int vorige = (int) startY voor (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) // Controleer of de teller> = noemer num - = den // Bereken de nieuwe tellerwaarde x + = xinc1 // Verander de x zoals gepast y + = yinc1 // Verander de y in voorkomend geval x + = xinc2 // Verander x in de juiste volgorde y + = yinc2 // Verander de y in voorkomend geval return null // niets gevonden

Stap 4: Collision Handling

De dynamische pixel kan een van de twee dingen doen tijdens een botsing.

  • Als het langzaam genoeg beweegt, wordt de dynamische pixel verwijderd en wordt een statische pixel toegevoegd aan het terrein waarop deze botste. Vasthouden zou onze eenvoudigste oplossing zijn. In het regelalgoritme van Bresenham is het het beste om een ​​vorig punt en een huidig ​​punt bij te houden. Wanneer een botsing wordt gedetecteerd, is het "huidige punt" de eerste vaste pixel die de straal raakt, terwijl het "vorige punt" de lege ruimte is net er voor. Het vorige punt is precies de locatie die we nodig hebben om de pixel te plakken.
  • Als het te snel beweegt, stuiteren we het van het terrein. Dit is waar ons oppervlaknormaal algoritme binnenkomt! Reflecteer de beginsnelheid van de bal over de normaal om deze te laten stuiteren.
  • De hoek aan weerszijden van de normaal is hetzelfde.

 // Project velocity op normaal, vermenigvuldig met 2, en trek deze af van normale snelheid = getNormal (collision.x, collision.y) // projectsnelheid op normaal met puntproductprojectie = velocity.x * normal.x + velocity .y * normal.y // velocity - = normal * projection * 2

Stap 5: Opsommingstekens en explosies!

Kogels werken precies zoals dynamische pixels. Motion is op dezelfde manier geïntegreerd en collision detection gebruikt hetzelfde algoritme. Ons enige verschil is de collision handling

Nadat een botsing is gedetecteerd, exploderen kogels door alle statische pixels binnen een straal te verwijderen en vervolgens dynamische pixels op hun plaats te plaatsen met hun snelheden naar buiten gericht. Ik gebruik een functie om een ​​vierkant gebied rond de straal van een explosie te scannen om erachter te komen welke pixels moeten losraken. Daarna wordt de afstand van de pixel ten opzichte van het midden gebruikt om een ​​snelheid vast te stellen.

 exploderen (x, y, radius) voor (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     

Stap 6: De speler

De speler is geen kernonderdeel van de verwoestbare terreinmonteur, maar er is wel een botsingsdetectie nodig die zeker relevant zal zijn voor problemen die in de toekomst zullen aankomen. Ik zal uitleggen hoe botsing wordt gedetecteerd en verwerkt in de demo voor de speler.

  1. Voor elke rand, loop van de ene hoek naar de volgende, controleer elke pixel.
  2. Als de pixel stevig is, begint u in het midden van de speler en scant u naar die pixel om een ​​vaste pixel te raken.
  3. Verplaats de speler weg van de eerste vaste pixel die u raakt.

Stap 7: Optimaliseren

Duizenden pixels worden tegelijk verwerkt, met als gevolg een behoorlijke belasting van de physics-engine. Om dit snel te maken, raad ik aan om een ​​taal te gebruiken die redelijk snel is. De demo is gecompileerd in Java.

U kunt ook dingen op het algoritmeniveau optimaliseren. Het aantal deeltjes uit explosies kan bijvoorbeeld worden verminderd door de vernietigingsresolutie te verlagen. Normaal vinden we elke pixel en veranderen deze in een 1x1 dynamische pixel. Scan in plaats daarvan elke 2x2 pixels of 3x3 en start een dynamische pixel van die grootte. In de demo gebruiken we 2x2 pixels.

Als u Java gebruikt, is garbagecollection een probleem. De JVM vindt periodiek objecten in het geheugen die niet meer worden gebruikt, zoals de dynamische pixels die worden weggegooid in ruil voor statische pixels, en probeer die weg te ruimen om ruimte te maken voor meer objecten. Het verwijderen van objecten, tonnen objecten kost echter tijd, en elke keer dat de JVM een opruimactie uitvoert, bevriest ons spel kort.

Een mogelijke oplossing om een ​​cache van een soort te gebruiken. In plaats van altijd objecten te maken / vernietigen, kunt u eenvoudig dode objecten (zoals dynamische pixels) vasthouden om later opnieuw te gebruiken.

Gebruik waar mogelijk primitieven. Het gebruik van objecten voor posities en snelheden maakt dingen bijvoorbeeld een beetje moeilijker voor de garbagecollection. Het zou nog beter zijn als u alles als primitieven in eendimensionale arrays zou kunnen opslaan.


Stap 8: Maak het uw eigen

Er zijn veel verschillende richtingen die je kunt inslaan met deze game-monteur. Functies kunnen worden toegevoegd en aangepast aan elke gewenste spelstijl.

Botsingen tussen dynamische en statische pixels kunnen bijvoorbeeld anders worden behandeld. Een botsmasker onder het terrein kan worden gebruikt om de plakkerigheid, veerkracht en kracht van elke statische pixel te bepalen, of de kans op losraken door een ontploffing.

Er zijn ook een aantal verschillende dingen die je kunt doen met geweren. Opsommingstekens kunnen een "penetratiediepte" krijgen, zodat het door zoveel pixels kan bewegen voordat het explodeert. Traditionele pistoolmechanica kan ook worden toegepast, zoals een gevarieerde vuursnelheid of, net als een jachtgeweer, kunnen meerdere kogels tegelijkertijd worden afgevuurd. Je kunt zelfs, net als voor de bouncy deeltjes, kogels terugveren van metalen pixels.


Conclusie

2D-terreinvernietiging is niet helemaal uniek. De klassiekers Worms en Tanks verwijderen bijvoorbeeld delen van het terrein bij explosies. Cortex Command gebruikt soortgelijke bouncy-deeltjes die we hier gebruiken. Andere spellen kunnen net zo goed, maar ik heb nog niet van ze gehoord. Ik zie ernaar uit om te zien wat andere ontwikkelaars met deze monteur zullen doen.

Het meeste van wat ik hier heb uitgelegd, is volledig geïmplementeerd in de demo. Bekijk de bron als iets dubbelzinnig of verwarrend lijkt. Ik heb opmerkingen aan de bron toegevoegd om deze zo duidelijk mogelijk te maken. Bedankt voor het lezen!