Lag et prosedyrisk generert Dungeon Cave System

For mange er prosessegenerering et magisk konsept som bare er utenfor rekkevidde. Kun veteran spillutviklere vet hvordan man bygger et spill som kan skape sine egne nivåer ... ikke sant? Det kan synes som magi, men PCG (prosessuell innholdsgenerering) kan læres av nybegynnere spillutviklere. I denne opplæringen vil jeg vise deg hvordan du prosessivt genererer et fangehullssystem.


Hva vi skal dekke

Her er en SWF-demo som viser hva slags nivåoppsett som denne teknikken kan generere:


Klikk på SWF for å generere et nytt nivå.

Å lære det grunnleggende betyr vanligvis mye Google-søk og eksperimentering. Problemet er at det er svært få enkel guider om hvordan du kommer i gang. Til referanse er her noen gode kilder til informasjon om emnet, som jeg har studert:

  • Fullfør Roguelike Tutorial (Python og libtcod)
  • Grid-Based Dungeon Generation
  • PCG Wiki

Før du kommer inn i detaljene, er det en god idé å vurdere hvordan vi skal løse problemet. Her er noen enkle å fordøye biter som vi vil bruke for å holde denne tingen enkel:

  1. Tilfeldig plasser det opprettede innholdet i spillverdenen.
  2. Kontroller at innholdet er plassert på et sted som gir mening.
  3. Kontroller at innholdet ditt er tilgjengelig av spilleren.
  4. Gjenta disse trinnene til nivået ditt kommer sammen pent.

Når vi har gjennomgått følgende eksempler, bør du ha de nødvendige ferdighetene til å eksperimentere med PCG i dine egne spill. Spennende, eh?


Hvor plasserer vi vårt spillinnhold?

Det første vi skal gjøre er tilfeldig plasser romene til et prosessoralt generert fangehullsnivå.

For å følge med, er det en god ide å ha en grunnleggende forståelse av hvordan flisekartene fungerer. I tilfelle du trenger en rask oversikt eller en oppfriskning, sjekk ut denne tegningen kart tutorial. (Det er rettet mot Flash, men selv om du ikke er kjent med Flash, er det fortsatt bra for å få tak i flisekartene.)

Opprette et rom som skal plasseres i ditt Dungeon-nivå

Før vi kommer i gang, må vi fylle vårt flisekart med veggfliser. Alt du trenger å gjøre er å gjenta gjennom hvert sted i kartet ditt (et 2D-array, ideelt) og plasser flisen.

Vi må også konvertere pikselkoordinatene til hvert rektangel til våre gridkoordinater. Hvis du vil gå fra piksler til gridplassering, del pikselkoordinatet av flisbredden. For å gå fra rutenett til piksler, må du multiplisere rutekoordinasjonen ved flisbredden.

For eksempel, hvis vi vil plassere våre roms øverste venstre hjørne på (5, 8) på vårt rutenett og vi har en flisbredde på 8 piksler, vi trenger å plassere det hjørnet på (5 * 8, 8 * 8) eller (40, 64) i pikselkoordinater.

La oss lage en Rom klasse; det kan se slik ut i Haxe-koden:

klasserommet utvider Sprite // disse verdiene holder rutenettkoordinater for hvert hjørne av rommet offentlige var x1: Int; offentlig var x2: Int; offentlig var y1: Int; offentlig var y2: Int; // bredde og høyde på rom i form av rutenett, offentlig var w: Int; offentlig var h: Int; // midtpunktet av rommet offentlige var sentrum: Punkt; // konstruktør for å skape nye rom offentlig funksjon ny (x: Int, y: Int, w: Int, h: Int) super (); x1 = x; x2 = x + w; y1 = y; y2 = y + h; this.x = x * Main.TILE_WIDTH; this.y = y * Main.TILE_HEIGHT; this.w = w; this.h = h; senter = nytt punkt (Math.floor ((x1 + x2) / 2), Math.floor ((y1 + y2) / 2));  // return true hvis dette rommet krysser forutsatt at rommet offentlig funksjon krysser (rom: Rom): Bool return (x1 <= room.x2 && x2 >= room.x1 && y1 <= room.y2 && room.y2 >= room.y1); 

Vi har verdier for hvert roms bredde-, høyde-, midtpunktsposisjon og fire hjørneposisjoner, og en funksjon som forteller oss om dette rommet krysser en annen. Vær også oppmerksom på at alt unntatt x- og y-verdiene er i vårt nettkoordinatsystem. Dette skyldes at det gjør livet mye lettere å bruke små tall hver gang vi får tilgang til romverdiene.

Ok, vi har rammen for et rom på plass. Nå hvordan genererer vi prosedyrisk og plasserer et rom? Vel, takket være innebygde tilfeldige nummergeneratorer, er denne delen ikke for vanskelig.

Alt vi trenger å gjøre er å gi tilfeldige x- og y-verdier for rommet vårt innenfor grensen til kartet, og gi tilfeldige bredde- og høydeverdier innenfor et forutbestemt område.


Gjør vår tilfeldige plassering mening?

Siden vi bruker tilfeldige steder og dimensjoner for våre rom, er vi bundet til å overlappe med tidligere opprettede rom mens vi fyller vår fangehull. Vel, vi har allerede kodet opp en enkel skjærer () metode for å hjelpe oss med å ta vare på problemet.

Hver gang vi prøver å plassere et nytt rom, ringer vi bare skjærer () på hvert par rom i hele listen. Denne funksjonen returnerer en boolsk verdi: ekte hvis rommene er overlappende, og falsk ellers. Vi kan bruke den verdien til å bestemme hva vi skal gjøre med rommet vi forsøkte å plassere.


Sjekk tilbake på skjærer () funksjon. Du kan se hvordan x- og y-verdiene overlapper og returnerer ekte.
 privat funksjon sted Rom () // lage oppsett for romlagring for enkle tilgangsrom = ny Array (); // randomiser verdier for hvert rom for (r i 0 ... maxRooms) var w = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var h = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var x = Std.random (MAP_WIDTH - w - 1) + 1; var y = Std.random (MAP_HEIGHT - h - 1) + 1; // lage rom med randomiserte verdier var newRoom = nytt rom (x, y, w, h); var mislyktes = false; for (andreRoom i rom) hvis (newRoom.intersects (otherRoom)) failed = true; gå i stykker;  hvis (! mislyktes) // lokal funksjon for å utarbeide nytt rom createRoom (newRoom); // skyv nytt rom inn i rom array rooms.push (newRoom)

Nøkkelen her er mislyktes boolean; den er satt til returverdi av skjærer (), og det er også ekte hvis (og bare hvis) rommene er overlappende. Når vi bryter ut av løkken, kontrollerer vi dette mislyktes variabel og, hvis det er feil, kan vi skjære ut det nye rommet. Ellers slipper vi bare rommet og prøver igjen til vi har rammet vårt maksimale antall rom.


Hvordan skal vi håndtere uoppnåelig innhold?

Det store flertallet av spill som bruker prosessert generert innhold, streber etter at alt innholdet nås av spilleren, men det er noen få folk der ute som tror at dette ikke nødvendigvis er den beste designbeslutningen. Hva om du hadde noen rom i ditt fangehull som spilleren bare sjelden kunne komme til, men kunne alltid se? Dette kan legge til en interessant dynamikk til fangehullet ditt.

Selvfølgelig, uansett hvilken side av argumentet du er på, er det sannsynligvis fortsatt en god ide å forsikre deg om at spilleren alltid kan komme seg gjennom spillet. Det ville være ganske frustrerende hvis du kom til et nivå av spillets fangehull og utgangen ble helt blokkert av.

Med tanke på at de fleste spill skyter for 100% tilgjengelig innhold, holder vi oss til det.

La oss håndtere den gjennomførbarheten

Nå skal du ha et flisekart oppe og det skal være kode på plass for å opprette et variabelt antall rom av varierende størrelse. Se på det; Du har allerede noen klare prosessormessige dungeon-rom!

Nå er målet å koble hvert rom slik at vi kan gå gjennom fangehullet vårt og til slutt nå en utgang som fører til neste nivå. Vi kan oppnå dette ved å hugge ut korridorer mellom rommene.

Vi må legge til en punkt variabel til koden for å holde styr på midten av hvert rom som er opprettet. Når vi lager og plasserer et rom, bestemmer vi sentrum og kobler det til forrige roms senter.

Først skal vi implementere korridorene:

privat funksjon hCorridor (x1: Int, x2: Int, y) for (x i Std.int (Math.min (x1, x2)) ... Std.int (Math.max (x1, x2)) + 1)  // destory fliser til å "skjære ut" korridorkart [x] [y] .parent.removeChild (kart [x] [y]); // plasser et nytt unblocked tile map [x] [y] = new Tile (Tile.DARK_GROUND, false, false); // legg til fliser som et nytt spillobjekt addChild (kart [x] [y]); // angi plasseringen av flisen riktig kart [x] [y] .setLoc (x, y);  // lage vertikal korridor for å koble rom privat funksjon vCorridor (y1: Int, y2: Int, x) for (y i Std.int (Math.min (y1, y2)) ... Std.int (Math.max (y1, y2)) + 1) // ødelegge fliser til å "skjære ut" korridorkart [x] [y] .parent.removeChild (kart [x] [y]); // plasser et nytt unblocked tile map [x] [y] = new Tile (Tile.DARK_GROUND, false, false); // legg til fliser som et nytt spillobjekt addChild (kart [x] [y]); // angi plasseringen av flisen riktig kart [x] [y] .setLoc (x, y); 

Disse funksjonene virker på nesten samme måte, men en skiller seg ut horisontalt og den andre vertikalt.

Koble det første rommet til det andre rommet krever en vCorridor og en hCorridor.

Vi trenger tre verdier for å gjøre dette. For horisontale korridorer trenger vi start-x-verdien, slutt-x-verdien og gjeldende y-verdi. For vertikale korridorer trenger vi start- og slutt-y-verdiene sammen med gjeldende x-verdi.

Siden vi beveger oss fra venstre til høyre, trenger vi de to tilsvarende x-verdiene, men bare en y-verdi siden vi ikke beveger oss opp eller ned. Når vi beveger seg vertikalt, trenger vi y-verdiene. I til loop i begynnelsen av hver funksjon, vier det fra startverdien (x eller y) til sluttverdien inntil vi har skåret ut hele korridoren.

Nå som vi har korridorkoden på plass, kan vi endre vår placeRooms () funksjon og ring våre nye korridorfunksjoner:

 privat funksjon sted Rom () // butikk rom i en matrise for enkel tilgang rom = ny Array (); // variabel for sporing av hvert rom var newCenter = null; // randomiser verdier for hvert rom for (r i 0 ... maxRooms) var w = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var h = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var x = Std.random (MAP_WIDTH - w - 1) + 1; var y = Std.random (MAP_HEIGHT - h - 1) + 1; // lage rom med randomiserte verdier var newRoom = nytt rom (x, y, w, h); var mislyktes = false; for (andreRoom i rom) hvis (newRoom.intersects (otherRoom)) failed = true; gå i stykker;  hvis (! mislyktes) // lokal funksjon for å utarbeide nytt rom createRoom (newRoom); // butikk senter for nytt rom newCenter = newRoom.center; hvis (rooms.length! = 0) // butikk senter for forrige rom var prevCenter = rom [rooms.length - 1] .center; // utlegge korridorer mellom rom basert på sentre // tilfeldig start med horisontale eller vertikale korridorer hvis (Std.random (2) == 1) hCorridor (Std.int (prevCenter.x), Std.int (newCenter.x) ), Std.int (prevCenter.y)); vCorridor (Std.int (prevCenter.y), Std.int (newCenter.y), Std.int (newCenter.x));  ellers vCorridor (Std.int (prevCenter.y), Std.int (newCenter.y), Std.int (prevCenter.x)); hCorridor (Std.int (prevCenter.x), Std.int (newCenter.x), Std.int (newCenter.y));  hvis (! mislyktes) rooms.push (newRoom); 

I bildet ovenfor kan du følge korridoropprettelsen fra første til fjerde rom: rød, grønn og blå. Du kan få noen interessante resultater avhengig av plasseringen av rommene - for eksempel, to korridorer ved siden av hverandre, gjør en dobbel bred korridor.

Vi la til noen variabler for å spore midt i hvert rom, og vi festet rommene med korridorer mellom sine sentre. Nå er det flere ikke-overlappende rom og korridorer som holder hele fangehullet tilkoblet. Ikke verst.


Vi er ferdig med vår Dungeon, Høyre?

Du har kommet langt fra å bygge ditt første prosessorgenerert fangehullsnivå, og jeg håper du har innsett at PCG ikke er noe magisk dyr som du aldri vil ha sjansen til å drepe.

Vi gikk over hvordan du tilfeldigvis plasserer innhold rundt fangehullet ditt med enkle tilfeldige tallgeneratorer, og noen forhåndsdefinerte områder for å holde innholdet riktig størrelse og omtrent på riktig sted. Deretter oppdaget vi en veldig enkel måte å avgjøre om din tilfeldige plassering var fornuftig ved å sjekke overlappende rom. Til slutt snakket vi litt om fordelene ved å holde innholdet tilgjengelig og vi fant en måte å sikre at spilleren din kan nå hvert rom i fangehullet ditt.

De tre første trinnene i vår fire trinns prosess er ferdig, noe som betyr at du har byggeklossene til et flott fangehull for ditt neste spill. Det endelige trinnet er nede for deg: du må gjenta det du lærte å skape mer prosesselt generert innhold for uendelig replayability.

Det er alltid mer å lære

Metoden for carving ut enkle fangehullsnivåer i denne opplæringen riper bare på overflaten av PCG, og det finnes noen andre enkle algoritmer som du enkelt kan plukke opp.

Min utfordring for deg er å begynne å eksperimentere med begynnelsen av spillet ditt som du opprettet her, og for å gjøre noen undersøkelser av flere metoder for å forandre fangehullene dine.

En flott metode for å skape grotte nivåer, er å bruke cellular automata, som har uendelige muligheter for å tilpasse fangehull nivåer. En annen god metode for å lære er binær plasspartisjonering (BSP), noe som skaper noe ugudelig ser gridlike fangehullsnivåer.

Jeg håper dette ga deg et godt hopp i begynnelsen av prosessorisk innholdsgenerering. Sørg for å kommentere med eventuelle spørsmål du har, og jeg vil gjerne se noen eksempler på hva du lager med PCG.

Relaterte innlegg
  • Generer tilfeldige grotte nivåer ved hjelp av Cellular Automata