Bak je eigen 3D Dungeons met procedurele recepten

In deze tutorial leer je hoe je complexe kerkers kunt bouwen van geprefabriceerde onderdelen, onbeperkt tot 2D- of 3D-rasters. Je spelers zullen nooit uit de kerkers komen om te ontdekken, je artiesten zullen de creatieve vrijheid waarderen, en je spel zal beter herspeelbaar zijn.

Om van deze tutorial te kunnen profiteren, moet u de basis-3D-transformaties begrijpen en vertrouwd zijn met scènegrafieken en entiteit-componentsystemen.

Een beetje geschiedenis

Een van de vroegste games om procedurele wereldgeneratie te gebruiken was Rogue. Gemaakt in 1980, bevatte het dynamisch gegenereerde, 2D, grid-gebaseerde kerkers. Dankzij die waren geen twee playthroughs identiek, en de game heeft een geheel nieuw genre van games voortgebracht, genaamd "roguelikes". Dit type kerker is nog steeds vrij algemeen meer dan 30 jaar later.

In 1996 werd Daggerfall uitgebracht. Het bevatte procedurele 3D kerkers en steden, waardoor de ontwikkelaars duizenden unieke locaties konden creëren, zonder ze allemaal handmatig te hoeven bouwen. Hoewel de 3D-benadering veel voordelen biedt ten opzichte van de klassieke 2D-rasterkerkers, is deze niet erg gebruikelijk.


Deze afbeelding toont een deel van een grotere kerker, geëxtraheerd om te illustreren welke modules zijn gebruikt om het te bouwen. De afbeelding is gegenereerd met "Daggerfall Modeling", gedownload van dfworkshop.net.

We zullen ons concentreren op het genereren van kerkers vergelijkbaar met die van Daggerfall.

Hoe een kerker te bouwen?

Om een ​​kerker te bouwen, moeten we definiëren wat een kerker is. In deze zelfstudie definiëren we een kerker als een set modules (3D-modellen) die volgens een reeks regels met elkaar zijn verbonden. We zullen gebruiken kamers verbonden door corridors en kruispunten:

  • EEN kamer is een groot gebied met een of meer uitgangen
  • EEN gang is een smal en lang gebied dat schuin kan staan ​​en heeft precies twee uitgangen
  • EEN knooppunt is een klein gebied met drie of meer uitgangen

In deze zelfstudie gebruiken we eenvoudige modellen voor de modules: hun netten bevatten alleen de vloer. We zullen drie van elk gebruiken: kamers, gangen en knooppunten. We zullen uitgangsmarkeringen visualiseren als asobjecten, met de -X / + X-as rood, + Y-as groen en + Z-as blauw.

Modules gebruikt om een ​​kerker te bouwen

Merk op dat de oriëntatie van uitgangen niet beperkt is tot stappen van 90 graden.

Als het gaat om het aansluiten van de modules, zullen we de volgende regels definiëren:

  • Kamers kunnen verbinding maken met gangen
  • Corridors kunnen verbinding maken met kamers of knooppunten
  • Knooppunten kunnen verbinding maken met gangen

Elke module bevat een set van exits-markeer objecten met een bekende positie en rotatie. Elke module is voorzien van een tag om aan te geven wat voor soort tag het is, en elke exit heeft een lijst met tags waarmee het kan verbinden.

Op het hoogste niveau is het proces van het bouwen van de kerker als volgt:

  1. Start een startmodule (bij voorkeur een met een groter aantal uitgangen).
  2. Instantiëren en verbinden geldige modules met elk van de niet-verbonden uitgangen van de module.
  3. Herbouw een lijst met niet-verbonden uitgangen in de hele kerker tot nu toe.
  4. Herhaal het proces tot een groot genoeg kerker is gebouwd.
Een blik op de iteraties van het algoritme op het werk.

Het gedetailleerde proces om twee modules met elkaar te verbinden is:

  1. Kies een niet-verbonden uitgang van de oude module.
  2. Kies een prefab van een nieuwe module met tags voor tag-matching die is toegestaan ​​door de exit van de oude module.
  3. Start de nieuwe module.
  4. Kies een exit uit de nieuwe module.
  5. Verbind de modules: koppel de uitgang van de nieuwe module aan de oude.
  6. Markeer beide uitgangen als verbonden of verwijder ze gewoon uit de scènegrafiek.
  7. Herhaal dit voor de rest van de niet-verbonden uitgangen van de oude module.

Om twee modules met elkaar te verbinden, moeten we ze uitlijnen (roteren en vertalen in 3D-ruimte), zodat een uitgang van de eerste module overeenkomt met een uitgang van de tweede module. Uitgangen zijn passen bij wanneer hun positie hetzelfde is en hun + Z-assen tegenovergesteld zijn, terwijl hun + Y-assen overeenkomen.

Het algoritme om dit te doen is eenvoudig:

  1. Draai de nieuwe module op de + Y-as met de rotatieoorsprong op de positie van de nieuwe uitgang, zodat de + -as van de oude uitgang tegenover de uitgangsas + Z-as staat en hun + Y-assen hetzelfde zijn.
  2. Vertaal de nieuwe module zodat de positie van de nieuwe uitgang gelijk is aan de positie van de oude uitgang.

Twee modules verbinden.

Implementatie

De pseudo-code is Python-achtig, maar deze moet voor iedereen leesbaar zijn. De voorbeeldbroncode is een Unity-project.

Laten we aannemen dat we werken met een entiteitscomponentensysteem dat entiteiten in een scènegrafiek bevat, en hun ouder-kindrelatie definieert. Een goed voorbeeld van een game-engine met een dergelijk systeem is Unity, met zijn game-objecten en componenten. Modules en exits zijn entiteiten; uitgangen zijn kinderen van modules. Modules hebben een component die hun tag definieert, en exits hebben een component die de tags definieert waarmee ze verbinding kunnen maken.

We zullen eerst het algoritme voor de generatie van de kerker behandelen. De eindconstraint die we zullen gebruiken is een aantal iteraties van stappen voor het genereren van kerkers.

 def generate_dungeon (starting_module_prefab, module_prefabs, iterations): starting_module = instantiate (starting_module_prefab) pending_exits = list (starting_module.get_exits ()) while iterations> 0: new_exits = [] voor pending_exit in pending_exits: 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) voor new_exit in new_module_instance.get_exits (): if new_exit! = exit_to_match: new_exits.append (new_exit ) pending_exits = iteraties voor new_exits - = 1

De instantiëren () functie maakt een exemplaar van een module prefab: het maakt een kopie van de module, samen met zijn uitgangen, en plaatst deze in de scène. De get_random_with_tag () function itereert door alle module prefabs, en pakt er één willekeurig uit, getagd met de voorziene tag. De random.choice () functie krijgt een willekeurig element uit een lijst of een reeks die als parameter is doorgegeven.

De match_exits functie is waar alle magie gebeurt, en wordt hieronder in detail getoond:

 def match_exits (old_exit, new_exit): new_module = new_exit.parent forward_vector_to_match = old_exit.backward_vector corrective_rotation = azimuth (forward_vector_to_match) - azimuth (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 azimuth (vector): # Retourneert de ondertekende hoek waarin deze vector is geroteerd ten opzichte van globaal + Z-as forward = [0, 0, 1] retourvector_hoek (vooruit, vector) * math.copysign (vector. X)

De backward_vector eigenschap van een uitgang is de -Z vector. De rotate_around_y () functie roteert het object rond een + Y-as met zijn draaipunt op een opgegeven punt, met een bepaalde hoek. De translate_global () function vertaalt het object met zijn kinderen in de globale (scène) ruimte, ongeacht de onderliggende relatie waar het deel van uitmaakt. De vector_angle () functie retourneert een hoek tussen twee willekeurige vectoren en tenslotte de math.copysign () functie kopieert het teken van een verstrekt nummer: -1 voor een negatief getal, 0 voor nul, en +1 voor een positief getal.

De generator uitbreiden

Het algoritme kan worden toegepast op andere typen wereldgeneratie, niet alleen kerkers. We kunnen de definitie van een module uitbreiden om niet alleen kerkeronderdelen, zoals kamers, gangen en kruispunten, maar ook meubels, schatkisten, kamerdecoraties, enz. Te dekken. Door de uitgangsmarkeringen in het midden van een kamer of in een kamer te plaatsen muur en taggen als een buit, decoratie, of zelfs monster, we kunnen de kerker tot leven brengen, met voorwerpen die je kunt stelen, bewonderen of doden.

Er is slechts één wijziging die moet worden uitgevoerd, zodat het algoritme correct werkt: een van de markeringen in een plaatsbaar item moet worden gemarkeerd als standaard, zodat het altijd wordt uitgekozen als degene die wordt uitgelijnd met de bestaande scène.


In de bovenstaande afbeelding zijn één kamer, twee kisten, drie pilaren, één altaar, twee lichten en twee items gemaakt en getagd. Een ruimte bevat een aantal markeringen die verwijzen naar de tags van andere modellen, zoals borst, pijler, altaar, of muur licht. Een altaar heeft er drie item markeringen erop. Door de techniek van de kerkergeneratie toe te passen op een enkele kamer, kunnen we er verschillende variaties van maken.

Hetzelfde algoritme kan worden gebruikt om procedurele items te maken. Als je een zwaard wilt maken, kun je de grip ervan definiëren als een startmodule. De grip zou verbinden met de pommel en met de cross-guard. De zijbescherming zou verbinding maken met het blad. Door slechts drie versies van elk van de zwaarddelen te hebben, kun je 81 unieke zwaarden genereren.

Voorbehoud

U hebt waarschijnlijk enkele problemen opgemerkt met de manier waarop dit algoritme werkt.

Het eerste probleem is dat de eenvoudigste versie ervan kerkers bouwt als een boom met modules, waarvan de root de startmodule is. Als je een tak van de structuur van de kerker volgt, ben je gegarandeerd doodlopend. De takken van de boom zijn niet met elkaar verbonden, en de kerker zal geen lussen van kamers of gangen hebben. Een manier om dit aan te pakken, is door enkele van de uitgangen van de module opzij te zetten voor latere verwerking en geen nieuwe modules aan te sluiten op deze uitgangen. Zodra de generator voldoende iteraties doormaakte, zou hij willekeurig een paar uitgangen kiezen en ze proberen te verbinden met een reeks gangen. Er moet een beetje algoritmisch werk worden gedaan om een ​​reeks modules te vinden en een manier om ze onderling te verbinden op een manier die een redelijk pad tussen deze uitgangen zou creëren. Dit probleem op zichzelf is complex genoeg om een ​​apart artikel te verdienen.

Een ander probleem is dat het algoritme zich niet bewust is van ruimtelijke kenmerken van de modules die het plaatst; het kent alleen de gelabelde exits en hun oriëntaties en locaties. Hierdoor overlappen de modules elkaar. Een toevoeging van een eenvoudige botsingcontrole tussen een nieuwe module die om bestaande modules wordt geplaatst, zou het algoritme in staat stellen kerkers te bouwen die niet aan dit probleem lijden. Wanneer de modules botsen, kan deze de module die hij probeerde te plaatsen, verwijderen en in plaats daarvan een andere proberen.


De eenvoudigste implementatie van het algoritme zonder botsingcontroles zorgt ervoor dat modules elkaar overlappen.

Het beheer van de exits en hun tags is een ander probleem. Het algoritme stelt voor om tags te definiëren voor elke exit-instantie en alle kamers te taggen, maar dit is nogal wat onderhoudswerk, als er een andere manier is om de modules aan te sluiten die je zou willen proberen. Als u bijvoorbeeld wilt toestaan ​​dat kamers verbinding maken met gangen en knooppunten in plaats van alleen gangen, zou u alle uitgangen in alle ruimtemodules moeten doorlopen en hun tags moeten bijwerken. Een manier om dit te omzeilen is om de verbindingsregels op drie verschillende niveaus te definiëren: kerker, module en exit. Het niveau van de kerker zou regels voor de hele kerker definiëren. Het zou definiëren welke tags mogen worden verbonden. Sommige kamers kunnen de verbindingsregels overschrijven wanneer ze worden verwerkt. Je zou een 'baas'-kamer kunnen hebben die zou garanderen dat er altijd een' schatkamer 'achter zit. Bepaalde uitgangen zouden de vorige twee niveaus overschrijven. Het definiëren van tags per exit biedt de grootste flexibiliteit, maar soms is te veel flexibiliteit niet zo goed.

Drijvende-kommawiskunde is niet perfect, en dit algoritme vertrouwt er zwaar op. Alle rotatietransformaties, willekeurige exitoriëntaties en posities worden opgeteld en kunnen artefacten als naden of overlappingen veroorzaken waar exits verbinding maken, vooral verder van het centrum van de wereld. Als dit te opvallend zou zijn, zou u het algoritme kunnen uitbreiden om een ​​extra prop te plaatsen waar de modules elkaar ontmoeten, zoals een deurpost of een drempel. Je vriendelijke kunstenaar zal zeker een manier vinden om de onvolkomenheden te verbergen. Voor kerkers met een redelijke omvang (kleiner dan 10.000 eenheden) is dit probleem niet eens merkbaar, ervan uitgaande dat er voldoende zorg is besteed aan het plaatsen en roteren van de uitgangsmarkeringen van de modules.

Conclusie

Het algoritme biedt, ondanks enkele tekortkomingen, een andere manier om te kijken naar de generatie van de kerker. Je bent niet langer gebonden aan bochten van 90 graden en rechthoekige kamers. Uw artiesten zullen de creatieve vrijheid waarderen die deze aanpak biedt, en uw spelers zullen genieten van het meer natuurlijke gevoel van de kerkers.