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 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.
We zullen ons concentreren op het genereren van kerkers vergelijkbaar met die van Daggerfall.
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:
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 bouwenMerk 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:
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:
Het gedetailleerde proces om twee modules met elkaar te verbinden is:
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:
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.
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.
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.
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.
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.