Bake din egen 3D Dungeons med Procedural Recipes

I denne opplæringen lærer du hvordan du bygger komplekse fangehuller fra prefabrikkerte deler, ubegrenset til 2D eller 3D-grid. Dine spillere vil aldri løpe ut av fangehullene for å utforske, artisterne vil sette pris på kreativ frihet, og spillet ditt vil ha bedre replayability.

For å dra nytte av denne opplæringen må du forstå grunnleggende 3D-transformasjoner og føle deg komfortabel med scenediagrammer og enhetskomponent-systemer.

Litt historie

En av de tidligste spillene for å bruke prosessorm verdens generasjon var Rogue. Laget i 1980, presenterte det dynamisk genererte, 2D, grid-baserte fangehuller. Takket være det var ingen to gjennomspill identiske, og spillet har skapt en helt ny sjanger av spill, kalt "roguelikes". Denne typen fangehull er fortsatt ganske vanlig over 30 år senere.

I 1996 ble Daggerfall utgitt. Den inneholdt prosedyre 3D-fangehuller og byer, som tillot utviklerne å skape tusenvis av unike steder, uten å måtte manuelt bygge dem alle. Selv om 3D-tilnærmingen gir mange fordeler i forhold til de klassiske 2D-fangehullene, er det ikke så vanlig.


Dette bildet viser en del av et større dungeon, utvunnet for å illustrere hvilke moduler som har blitt brukt til å bygge den. Bildet ble generert med "Daggerfall Modeling", lastet ned fra dfworkshop.net.

Vi vil fokusere på å generere fangehull som ligner på Daggerfalls.

Hvordan bygge en Dungeon?

For å bygge et fangehull må vi definere hva et fangehull er. I denne opplæringen vil vi definere et fangehull som et sett med moduler (3D-modeller) som er koblet til hverandre i henhold til et sett med regler. Vi vil bruke rom forbundet med korridorer og veikryss:

  • EN rom er et stort område som har en eller flere utganger
  • EN korridor er et smalt og langt område som kan være skråstilt, og har nøyaktig to utganger
  • EN kryss er et lite område som har tre eller flere utganger

I denne veiledningen vil vi bruke enkle modeller for modulene - deres masker inneholder bare gulv. Vi vil bruke tre av hver: rom, korridorer og veikryss. Vi vil visualisere utgangsmarkører som akseobjekter, med -X / + X-aksen er rød, + Y-aksen grønn og + Z-akse blå.

Moduler som brukes til å bygge et fangehull

Legg merke til at orienteringen av utganger ikke er begrenset til 90 graders trinn.

Når det gjelder å koble modulene, definerer vi følgende regler:

  • Rom kan koble til korridorer
  • Korridorer kan koble til rom eller veikryss
  • Junctions kan koble til korridorer

Hver modul inneholder et sett med utganger-markørobjekter med kjent posisjon og rotasjon. Hver modul er merket for å si hva slags det er, og hver utgang har en liste over tagger den kan koble til.

På høyeste nivå er prosessen med å bygge fangehullet som følger:

  1. Instantiate en startmodul (helst en med et større antall utganger).
  2. Instantiate og koble til gyldige moduler til hver av de uavhengige utgangene til modulen.
  3. Oppbygg en liste over uavhengige utganger i hele fangehullet så langt.
  4. Gjenta prosessen til en stor nok fangehull er bygget.
En titt på iterasjonene av algoritmen på jobb.

Den detaljerte prosessen med å koble to moduler sammen er:

  1. Velg en ikke-tilkoblet utgang fra den gamle modulen.
  2. Velg en prefab av en ny modul med tag-matchende tagger tillatt av den gamle modulens utgang.
  3. Instantiate den nye modulen.
  4. Velg en utgang fra den nye modulen.
  5. Koble modulene: Match den nye modulens utgang til den gamle.
  6. Merk begge utgangene som tilkoblet, eller bare slett dem fra scenediagrammet.
  7. Gjenta for resten av den gamle modulens ukoblede utganger.

For å koble to moduler sammen, må vi justere dem (rotere og oversette dem i 3D-plass), slik at en utgang fra den første modulen samsvarer med en utgang fra den andre modulen. Utgangene er matchende når deres posisjon er den samme og deres + Z-akser er motsatte, mens deres + Y-akser passer.

Algoritmen for å gjøre dette er enkel:

  1. Roter den nye modulen på + Y-aksen med rotasjonsopprinnelsen ved den nye avkjørs posisjonen, slik at den gamle utgangens + Z-aksen er motsatt den nye utgangens + Z-akse, og deres + Y-akser er de samme.
  2. Oversett den nye modulen slik at den nye utgangens posisjon er den samme som den gamle utgangens posisjon.

Koble til to moduler.

Gjennomføring

Pseudokoden er Python-ish, men den bør leses av alle. Eksempelkilden er et Unity-prosjekt.

La oss anta at vi jobber med et enhetskomponent-system som inneholder enheter i en scenediagram, som definerer deres foreldre-barn-forhold. Et godt eksempel på en spillmotor med et slikt system er Unity, med spillobjekter og -komponenter. Moduler og utganger er enheter; Utganger er barn av moduler. Moduler har en komponent som definerer deres tag, og utganger har en komponent som definerer kodene de har lov til å koble til.

Vi skal håndtere fangehullsgenereringsalgoritmen først. Slutten begrensningen vi vil bruke er en rekke iterasjoner av fangehull generasjon trinnene.

 def start_module (start_module_prefab, module_prefabs, iterations): start_module = instantiate (starting_module_prefab) pending_exits = liste (start_module.get_exits ()) mens iterasjoner> 0: new_exits = [] for ventende_eksempel i ventendeeksempel: tag = random.choice (pending_exit.tags) new_module_prefab = get_random_with_tag (module_prefabs, tag) new_module_instance = instantiate (new_module_prefab) exit_to_match = random.choice (new_module_instance.exits) match_exits (pending_exit, exit_to_match) for new_exit i new_module_instance.get_exits (): hvis new_exit! = exit_to_match: new_exits.append (new_exit ) pending_exits = new_exits iterations - = 1

De instansiere () funksjonen skaper en forekomst av en modul prefab: den lager en kopi av modulen, sammen med utgangene, og plasserer dem i scenen. De get_random_with_tag () funksjon iterates gjennom alle modul prefabs, og plukker en tilfeldig, merket med den medfølgende taggen. De random.choice () funksjonen får et tilfeldig element fra en liste eller en matrise som er bestått som en parameter.

De match_exits funksjonen er hvor alt magien skjer, og er vist i detalj nedenfor:

 def match_exits (old_exit, new_exit): new_module = new_exit.parent forward_vector_to_match = old_exit.backward_vector corrective_rotation = azimut (forward_vector_to_match) - azimut (new_exit.forward_vector) rotate_around_y (new_module, new_exit.position, corrective_rotation) corrective_translation = old_exit.position - new_exit.position translate_global (new_module, corrective_translation) def asimut (vektor): # Returnerer den signerte vinkelen denne vektoren roteres i forhold til global + Z-aksen fremover = [0, 0, 1] returnere vektor_angel (fremover, vektor) * math.copysign (vector. x)

De backward_vector egenskapen til en utgang er dens -Z vektor. De rotate_around_y () funksjonen roterer objektet rundt en + Y-akse med sving på et gitt punkt, med en angitt vinkel. De translate_global () funksjonen oversetter objektet med sine barn i den globale (scenen) plassen, uansett hvilket barns forhold det kan være en del av. De vector_angle () funksjonen returnerer en vinkel mellom to vilkårlig vektorer, og til slutt, math.copysign () funksjon kopierer tegnet på et angitt nummer: -1 for et negativt tall, 0 for null og +1 for et positivt tall.

Utvider generatoren

Algoritmen kan brukes til andre typer verdensgenerasjon, ikke bare fangehuller. Vi kan utvide definisjonen av en modul som ikke bare dekker fangehullsdeler som rom, korridorer og veikryss, men også møbler, skattekister, romdekorasjoner etc. Ved å plassere utgangsmarkørene midt i et rom eller på et rom vegg, og merker det som en tyvegods, dekorasjon, eller monster, Vi kan bringe fangehullet til liv, med gjenstander som du kan stjele, beundre eller drepe.

Det er bare en endring som må gjøres, slik at algoritmen fungerer som den skal: En av markørene som er tilstede i en plasserbar gjenstand må markeres som misligholde, slik at den alltid blir plukket som den som skal justeres til eksisterende scene.


I bildet ovenfor er ett rom, to kister, tre søyler, ett alter, to lys og to elementer skapt og merket. Et rom har et sett med markører som refererer til andre modellers koder, for eksempel bryst, søyle, alter, eller wallLight. Et alter har tre punkt markører på den. Ved å bruke dungeon generasjon teknikk til et enkeltrom, kan vi skape mange variasjoner av det.

Den samme algoritmen kan brukes til å lage prosessuelle elementer. Hvis du vil lage et sverd, kan du definere grepet som en startmodul. Gripet ville koble til pommel og til kryssbeskyttelsen. Kryssbeskytteren ville koble til bladet. Ved å ha bare tre versjoner av hver av sverddelene, kan du generere 81 unike sverd.

Advarsler

Du har sikkert lagt merke til noen problemer med måten denne algoritmen fungerer på.

Det første problemet er at den enkleste versjonen av den bygger fangehuller som et tre av moduler, med roten som startmodulen. Hvis du følger noen gren av strukturen i fangehullet, er du garantert å slå en blindgyde. Der treets grener ikke er sammenkoblet, og fangehullet vil mangle looper av rom eller korridorer. En måte å adressere dette på, ville sette inn noen av modulens utganger for senere behandling, og ikke koble nye moduler til disse utgangene. Når generatoren gikk gjennom nok iterasjoner, ville det plukke et par utganger tilfeldig, og ville prøve å koble dem til et sett med korridorer. Det er litt algoritmisk arbeid som må gjøres, for å finne et sett med moduler og en måte å knytte dem sammen på en måte som ville skape en god vei mellom disse utgangene. Dette problemet i seg selv er komplisert nok til å fortjene en egen artikkel.

Et annet problem er at algoritmen er uvitende om romlige egenskaper av moduler den plasserer; det vet bare de merkede utgangene, og deres retninger og steder. Dette fører til at modulene overlapper. Et tillegg til en enkel kollisjonskontroll mellom en ny modul som skal plasseres rundt eksisterende moduler, ville tillate algoritmen å bygge fangehull som ikke lider av dette problemet. Når modulene kolliderer, kan det kaste bort modulen den prøvde å plassere og ville prøve en annen i stedet.


Den enkleste implementeringen av algoritmen uten kollisjonskontroller fører til at modulene overlapper.

Å administrere utgangene og deres koder er et annet problem. Algoritmen foreslår at du definerer koder på hver avgangsinstans og merker alle rommene, men dette er ganske mye vedlikeholdsarbeid, hvis det er en annen måte å koble modulene du ønsker å prøve. Hvis du for eksempel vil tillate rom å koble til korridorer og veikryss i stedet for bare korridorer, må du gå gjennom alle utgangene i alle rommodulene, og oppdatere sine koder. En vei rundt dette er å definere tilkoblingsreglene på tre separate nivåer: fangehull, modul og utgang. Dungeon-nivået ville definere regler for hele fangehullet - det ville definere hvilke tagger som er tillatt å koble sammen. Noen av rommene vil kunne overstyre tilkoblingsreglene når de behandles. Du kan ha en "sjef" rom som vil garantere at det alltid er et "skatt" rom bak det. Enkelte utganger ville overstyre de to foregående nivåene. Definere koder per utgang gir størst fleksibilitet, men noen ganger er for mye fleksibilitet ikke så bra.

Flytende matematikk er ikke perfekt, og denne algoritmen er avhengig av det. Alle rotasjonstransformasjonene, vilkårlig utgangsretninger og stillinger vil legge opp, og kan forårsake gjenstander som sømmer eller overlapper hvor utganger forbinder, særlig lenger fra verdens sentrum. Hvis dette skulle være for merkbart, kan du utvide algoritmen for å plassere en ekstra stikkontakt der modulene møtes, for eksempel en dørramme eller en terskel. Din vennlige kunstner vil sikkert finne en måte å skjule ufullkommenhetene på. For fangehuller av en rimelig størrelse (mindre enn 10.000 enheter på tvers), er dette problemet ikke engang merkbar, forutsatt at det er tatt godt vare på når du plasserer og roterer exit markørene til modulene.

Konklusjon

Algoritmen, til tross for noen av sine mangler, gir en annen måte å se på fangehullsgenerasjonen. Du vil ikke lenger være tvunget til 90-graders sving og rektangulære rom. Dine artister vil sette pris på den kreative friheten denne tilnærmingen vil tilby, og spillerne dine vil nyte den mer naturlige følelsen av fangehullene.