Generer tilfeldige grotte nivåer ved hjelp av Cellular Automata

Produserende innholdsgeneratorer er biter av kode skrevet inn i spillet ditt som kan lage nye brikker av spillinnhold når som helst - selv når spillet kjører! Spillutviklere har forsøkt å prosedyre generere alt fra 3D-verdener til musikalske lydspor. Å legge til litt generasjon i spillet ditt er en fin måte å plugge inn ekstra verdi: spillere elsker det fordi de får nytt, uforutsigbart og spennende innhold hver gang de spiller.

I denne opplæringen ser vi på en god metode for å generere tilfeldige nivåer, og prøv å strekke grensene for hva du kanskje tror kan genereres.

Relaterte innlegg

Hvis du er interessert i å lese mer om temaene for prosessuell innholdsgenerering, nivådesign, AI eller mobilautomat, må du sjekke disse andre innleggene ut:

  • Skape liv: Conways livsspill
  • Portal 2 Level Design: Opprette puslespill for å utfordre dine spillere
  • StarCraft II Level Design: Estetisk Design og Editor Tips
  • Koding av en tilpasset sekvensgenerator for å gi en Starscape
  • Gamedev Ordliste: Sekvensgeneratorer og Pseudorandom Nummergeneratorer

Velkommen til hulene!

I denne opplæringen skal vi bygge en hule generator. Grotter er gode for alle slags spill sjangre og innstillinger, men de påminner meg spesielt om gamle fangehuller i rollespill.

Ta en titt på demoen under for å se hvilke utganger du vil kunne få. Klikk "New World" for å produsere en ny hule å se på. Vi snakker om hva de forskjellige innstillingene gjør med tiden.


Denne generatoren returnerer faktisk oss et stort todimensjonalt utvalg av blokker, som hver er enten solid eller tom. Så faktisk kan du bruke denne generatoren for alle slags spill i tillegg til dungeon-crawlere: tilfeldige nivåer for strategispill, tilemaps for plattformspill, kanskje til og med som arenaer for en multiplayer shooter! Hvis du ser forsiktig ut, gjør det også en økegenerator å bla de faste og tomme blokkene. Alt bruker samme kode og utgang, noe som gjør dette til et veldig fleksibelt verktøy.

La oss begynne med å stille et enkelt spørsmål: hva i all verden er en mobilmotor, uansett?


Komme i gang med celler

På 1970-tallet, en matematiker kalt John Conway publisert en beskrivelse av The Game of Life, noen ganger bare kalt Life. Livet var egentlig ikke et spill; det var mer som en simulering som tok et rutenett av celler (som kunne være levende eller døde) og brukte noen enkle regler på dem.

Fire regler ble brukt på hver celle i hvert trinn av simuleringen:

  1. Hvis en levende celle har mindre enn to levende naboer, dør den.
  2. Hvis en levende celle har to eller tre levende naboer, forblir den levende.
  3. Hvis en levende celle har mer enn tre levende naboer, dør den.
  4. Hvis en død celle har nøyaktig tre levende naboer, blir den levende.

Hyggelig og enkel! Likevel, hvis du prøver forskjellige kombinasjoner av startnett, kan du få svært merkelige utfall. Uendelige løkker, maskiner som spytter ut figurer og mer. Livets lek er et eksempel på a mobilautomat - et rutenett av celler som styres av visse regler.

Vi skal implementere et system som ligner på livet, men i stedet for å produsere morsomme mønstre og former, kommer det til å skape fantastiske hule systemer for våre spill.


Implementere en mobilautomat

Vi skal representere vårt mobilnett som et todimensjonalt utvalg av booleske (ekte eller falsk) verdier. Dette passer oss fordi vi bare er interessert i om en flis er solid eller ikke.

Her er vi initialiserer vårt grid av celler:

boolsk [] [] cellemap = ny boolsk [bredde] [høyde];

Tips: Legg merke til at den første indeksen er x-koordinatet for gruppen, og den andre indeksen er y-koordinaten. Dette gjør at arrayet er mer naturlig i kode.

I de fleste programmeringsspråk vil denne oppsettet initialiseres med alle sine verdier satt til falsk. Det er greit for oss! Hvis en arrayindeks (X, y) er falsk, vi vil si at cellen er tom; hvis det er ekte, den flisen vil være solid rock.

Hver av disse gruppeposisjonene representerer en av cellene i vårt mobilnett. Nå må vi sette opp vårt rutenett slik at vi kan begynne å bygge våre grotter.

Vi skal starte med å tilfeldigvis sette hver celle til enten død eller levende. Hver celle vil ha den samme tilfeldige sjansen for å bli gjort levende, og du bør sørge for at denne sjansen verdien er satt i en variabel et sted, fordi vi definitivt vil tilpasse det senere og ha det et sted som er lett tilgjengelig, vil hjelpe oss med at. Jeg skal bruke 45% å begynne med.

float chanceToStartAlive = 0.45f; offentlig boolean [] [] initialiseMap (boolean [] [] map) for (int x = 0; x 
Vår tilfeldige hule før noen simuleringstrinn for cellulær automat.

Hvis vi kjører denne koden, ender vi med et stort rutenett av celler som den ene over som er tilfeldig levende eller død. Det er rotete, og det ser definitivt ikke ut som et hulsystem jeg noensinne har sett. Så hva er neste??


Vokser våre huler

Husk reglene som styrte cellene i The Game of Life? Hver gang simuleringen gikk videre med ett steg, ville hver celle sjekke livets regler og se om det ville forandres til å være levende eller død. Vi skal bruke nøyaktig den samme ideen til å bygge hulene våre - vi skal skrive en funksjon nå som løkker over hver celle i rutenettet, og bruker noen grunnleggende regler for å avgjøre om den lever eller dør.

Som du vil se senere, skal vi bruke denne koden mer enn en gang, så å sette den i sin egen funksjon betyr at vi kan ringe det så mange eller så få ganger som vi liker. Vi gir det et hyggelig informativt navn som doSimulationStep (), også.

Hva må funksjonen gjøre? Vel, først, skal vi lage et nytt rutenett som vi kan sette våre oppdaterte celleverdier inn. For å forstå hvorfor vi trenger å gjøre dette, husk at for å beregne den nye verdien av en celle i rutenettet, må vi se på sine åtte naboer:

Men hvis vi allerede har beregnet den nye verdien av noen av cellene og legger dem tilbake i rutenettet, vil vår beregning være en blanding av nye og gamle data, slik som:

Oops! Det er ikke det vi vil ha i det hele tatt. Så hver gang vi beregner en ny celleverdi, i stedet for å sette den tilbake i det gamle kartet, skal vi skrive det til en ny.

La oss begynne å skrive det doSimulationStep () funksjon, så:

public doSimulationStep (boolean [] [] oldMap) boolean [] [] newMap = ny boolsk [bredde] [høyde]; // ... 

Vi vil vurdere hver celle i rutenettet i sin tur, og telle hvor mange av naboene som er i live og død. Å telle naboene dine i en matrise er en av de kjedelige biter av kode, du må skrive en million ganger. Her er en rask implementering av den i en funksjon jeg har ringt countAliveNeighbours ():

// Returnerer antall celler i en ring rundt (x, y) som er i live. offentlige countAliveNeighbours (boolean [] [] map, int x, int y) int count = 0; for (int i = -1; i<2; i++) for(int j=-1; j<2; j++) int neighbour_x = x+i; int neighbour_y = y+j; //If we're looking at the middle point if(i == 0 && j == 0) //Do nothing, we don't want to add ourselves in!  //In case the index we're looking at it off the edge of the map else if(neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= map.length || neighbour_y> = map [0] .length) count = count + 1;  // Ellers, en normal kontroll av naboen annet hvis (kart [neighour_x] [neighbor_y]) count = count + 1; 

Et par ting om denne funksjonen:

Først til løkker er litt rart hvis du ikke har gjort noe sånt før. Tanken er at vi vil se på alle cellene som er rundt punktet (X, y). Hvis du ser på illustrasjonen nedenfor, kan du se hvordan indeksene vi vil ha, er en mindre, lik og en mer enn den opprinnelige indeksen. Våre to til looper gir oss bare det, fra og med -1, og looping gjennom til +1. Vi legger da til den opprinnelige indeksen i til loop for å finne hver nabo.

For det andre, legg merke til hvordan hvis vi sjekker en gridreferanse som ikke er ekte (for eksempel, det er utenfor kanten av kartet), teller vi det som nabo. Jeg foretrekker dette for cavegenerering fordi det pleier å bidra til å fylle ut kantene på kartet, men du kan eksperimentere ved ikke å gjøre dette hvis du liker.

Så nå, la oss gå tilbake til vår doSimulationStep () funksjon og legg til i noe mer kode:

offentlig boolean [] [] doSimulationStep (boolean [] [] oldMap) boolean [] [] newMap = ny boolsk [bredde] [høyde]; // Loop over hver rad og kolonne på kartet for (int x = 0; x birthLimit) newMap [x] [y] = true;  ellers newMap [x] [y] = false;  returner newMap; 

Dette løkker over hele kartet, bruker våre regler til hver rutenett for å beregne den nye verdien og plassere den newMap. Reglene er enklere enn Game of Life - vi har to spesielle variabler, en for fødselsdøde celler (fbirthLimit), og en for å drepe levende celler (deathLimit). Hvis levende celler er omgitt av mindre enn deathLimit celler de dør, og hvis døde celler er nær i det minste birthLimit celler blir de levende. Hyggelig og enkel!

Alt som er igjen på slutten er en siste berøring for å returnere det oppdaterte kartet. Denne funksjonen representerer et enkelt trinn i vår cellular automatons regler - neste trinn er å forstå hva som skjer når vi bruker det en gang, to ganger eller flere ganger til vårt første startkart.


Tweaking og Tuning

La oss se på hva hovedgenereringskoden nå ser ut, ved hjelp av koden vi har skrevet så langt.

offentlig boolsk [] [] generateMap () // Opprett et nytt kart booleansk [] [] cellekart = ny boolsk [bredde] [høyde]; // Sett opp kartet med tilfeldige verdier cellmap = initialiseMap (cellemap); // Og kjør nå simuleringen for et angitt antall trinn for (int i = 0; i 

Den eneste virkelig nye koden er en til loop som kjører vår simuleringsmetode et bestemt antall ganger. Igjen, trykk den i en variabel slik at vi kan endre den, fordi vi skal begynne å spille med disse verdiene nå!

Så langt har vi satt disse variablene:

  • chanceToStartAlive angir hvor tett det opprinnelige rutenettet er med levende celler.
  • starvationLimit er den nedre grensen grensen hvor celler begynner å dø.
  • overpopLimit er den øvre grensen grensen hvor celler begynner å dø.
  • birthNumber er antall naboer som fører til at en død celle blir levende.
  • numberOfSteps er antall ganger vi utfører simuleringstrinnet.

Vår tilfeldige hule etter to simulatorstrinn for cellulær automat.

Du kan fla med disse variablene i demoen øverst på siden. Hver verdi vil endre demoen dramatisk, så spill et spill og se hva som passer.

En av de mest interessante endringene du kan gjøre er å numberOfSteps variabel. Når du kjører simuleringen for flere trinn, forsvinner rukkheten på kartet, og øyene glir bort i ingenting. Jeg har lagt inn en knapp slik at du kan ringe funksjonen manuelt selv, og se effektene. Prøv litt, og du vil finne en kombinasjon av innstillinger som passer til din stil og hvilke nivåer spillet ditt trenger.


Vår tilfeldige hule etter seks simuleringstrinn i simulatoren.

Med det er du ferdig. Gratulerer - du har nettopp laget en prosessnivå generator, godt utført! Len deg tilbake, kjør og kjør koden din igjen, og smil på de rare og fantastiske hulsystemene som kommer ut. Velkommen til verden av prosessegenerering.


Tar det videre

Hvis du stirrer på din nydelige grottegenerator og lurer på hva annet du kan gjøre med det, her er et par ekstra kredittoppdrag ideer:

Bruke Flood Fill å gjøre kvalitetskontroll

Flood fill er en veldig enkel algoritme som du kan bruke til å finne alle mellomrom i en matrise som kobler til et bestemt punkt. Akkurat som navnet antyder, fungerer algoritmen litt som å hente en bøtte med vann til ditt nivå - det sprer seg ut fra utgangspunktet og fyller i alle hjørnene.

Flood fill er flott for mobilautomat fordi du kan bruke den til å se hvor stor en bestemt hule er. Hvis du kjører demoen et par ganger, vil du legge merke til at noen kart består av en stor hule, mens andre har noen mindre grotter som er skilt fra hverandre. Flood fill kan hjelpe deg med å oppdage hvor stor en hule er, og deretter regenerere nivået hvis det er for lite, eller bestemme hvor du vil at spilleren skal starte hvis du tror det er stort nok. Det er en flott oversikt over flomfylling på Wikipedia.

Rask og enkel skatteplassering

Å sette skatt på kule områder krever ofte mye kode, men vi kan faktisk skrive ganske enkel kode for å sette skatt ut av veien i våre hulsystemer. Vi har allerede vår kode som teller hvor mange naboer en firkant har, så ved å løpe over det ferdige hulsystemet, kan vi se hvor omgitt av vegger et bestemt torg er.

Hvis en tom gridcelle er omgitt av mange faste vegger, er det sannsynligvis helt til enden av en korridor eller gjemt i veggene i hulsystemet. Dette er et flott sted å skjule skatten - så ved å gjøre en enkel kontroll av våre naboer kan vi skli skatten inn i hjørnene og ned bakgatene.

Offentlig tomromTreasure (boolsk [] [] verden) // Hvor skjult må et sted være for skatt? // Jeg finner 5 eller 6 er bra. 6 for veldig sjelden skatt. int treasureHiddenLimit = 5; for (int x = 0; x < worldWidth; x++) for (int y=0; y < worldHeight; y++) if(!world[x][y]) int nbs = countAliveNeighbours(world, x, y); if(nbs >= treasureHiddenLimit) placeTreasure (x, y); 

Dette er ikke perfekt. Det setter noen ganger skatt i utilgjengelige hull i hulsystemet, og noen ganger vil stedene bli ganske åpenbare også. Men i en klemme er det en fin måte å spre samleobjekter rundt ditt nivå. Prøv det i demoen ved å trykke på placeTreasure () knapp!


Konklusjoner og videre lesing

Denne opplæringen viste deg hvordan du bygger en grunnleggende, men komplett prosessorgenerator. Med bare noen få enkle trinn skrev vi kode som kan skape nye nivåer i blikket på øyet. Forhåpentligvis har dette gitt deg en smak av potensialet for å bygge prosessuelle innholdsgeneratorer for spillene dine!

Hvis du vil lese mer, er Roguebasin en god kilde til informasjon om prosessegenereringssystemer. Det fokuserer for det meste på roguelike-spill, men mange av sine teknikker kan brukes i andre typer spill, og det er mye inspirasjon for prosedyrisk generering av andre deler av et spill også!

Hvis du vil ha mer på Procedural Content Generation eller Cellular Automata, er det en flott online versjon av The Game of Life (selv om jeg anbefaler å skrive «Conway's Game of Life» til Google). Du vil kanskje også like Wolfram Tones, et sjarmerende forsøk på å bruke mobilautomat for å generere musikk!