Coaceți-vă temnițele 3D cu rețete procedurale

În acest tutorial, veți învăța cum să construiți dungeoni complexe de la piese prefabricate, necondiționate la rețelele 2D sau 3D. Jucătorii dvs. nu vor ieși niciodată din temnițe pentru a explora, artiștii dvs. vor aprecia libertatea creativă și jocul dvs. va avea o mai bună replayability.

Pentru a beneficia de acest tutorial, trebuie să înțelegeți transformările 3D de bază și să vă simțiți confortabil cu graficele de scenă și sistemele componente-entitate.

Un pic de istorie

Unul dintre cele mai vechi jocuri de a folosi generația globală procedurală a fost Rogue. Realizat în 1980, a fost prezentat dinamic din punct de vedere al grilei bazate pe rețea. Datorită faptului că nici două play-uri nu erau identice, iar jocul a dat naștere unui gen întreg de jocuri numit "roguelikes". Acest tip de temnita este inca destul de obisnuita peste 30 de ani mai tarziu.

În 1996, Daggerfall a fost eliberat. Acesta a inclus dungeonuri 3D și orașe proceduale, care le-a permis dezvoltatorilor să creeze mii de locații unice, fără a trebui să le construiască manual toate. Chiar dacă abordarea sa 3D oferă multe avantaje față de dungeon-urile clasice 2D, nu este foarte comună.


Această imagine prezintă o parte dintr-o temniță mai mare, extrasă pentru a ilustra ce module au fost folosite pentru a-l construi. Imaginea a fost generată cu "Daggerfall Modeling", descărcată de la dfworkshop.net.

Ne vom concentra pe generarea unor dungeoni asemănătoare cu cele ale lui Daggerfall.

Cum se construieste o temnita?

Pentru a construi o temnita, trebuie sa definim ce este o temnita. În acest tutorial, vom defini o temniță ca un set de module (modele 3D) conectate între ele în conformitate cu un set de reguli. Noi vom folosi camere conectat prin coridoare și intersecții:

  • A cameră este o zonă mare care are una sau mai multe ieșiri
  • A coridor este o zonă îngustă și lungă care poate fi înclinată și are exact două ieșiri
  • A joncţiune este o zonă mică care are trei sau mai multe ieșiri

În acest tutorial, vom folosi modele simple pentru module - ochiurile lor vor conține doar podea. Vom folosi trei dintre ele: camere, coridoare și intersecții. Vom vizualiza marcatorii de ieșire ca obiecte axe, axa -X / + X fiind roșie, + axa Y verde și + axa Z albastră.

Modulele folosite pentru a construi o temnita

Observați că orientarea ieșirilor nu este limitată la creșterile de 90 de grade.

Când vine vorba de conectarea modulelor, vom defini următoarele reguli:

  • Camerele se pot conecta la coridoare
  • Coridoarele se pot conecta la camere sau la intersecții
  • Juncțiile se pot conecta la coridoare

Fiecare modul conține un set de ieșiri-obiecte marker cu o poziție și rotație cunoscute. Fiecare modul este etichetat să spună ce fel este și fiecare ieșire are o listă de etichete la care se poate conecta.

La cel mai înalt nivel, procesul de construire a temnita este după cum urmează:

  1. Instanțiați un modul de pornire (de preferință unul cu un număr mai mare de ieșiri).
  2. Instanțiați și conectați module valide la fiecare dintre ieșirile neconectate ale modulului.
  3. Refaceți o listă de ieșiri neconectate în întreaga temniță până acum.
  4. Repetați procesul până când se construiește o temniță suficientă.
O privire la iterațiile algoritmului la locul de muncă.

Procesul detaliat de conectare a două module împreună este:

  1. Alegeți o ieșire neconectată de la modulul vechi.
  2. Alegeți un prefabricat al unui nou modul cu etichete de potrivire a etichetelor permise de ieșirea vechiului modul.
  3. Instanțiați noul modul.
  4. Alegeți o ieșire din noul modul.
  5. Conectați modulele: potriviți ieșirea modulului nou cu cea veche.
  6. Marcați ambele ieșiri ca fiind conectate sau pur și simplu le ștergeți din graficul scenelor.
  7. Repetați pentru restul ieșirilor neconectate ale modulului vechi.

Pentru a conecta împreună două module, trebuie să le aliniem (rotiți-le și să le traduceți în spațiu 3D), astfel încât o ieșire din primul modul să corespundă unei ieșiri din al doilea modul. Ieșirile sunt potrivire atunci când poziția lor este aceeași și axele lor + Z sunt opuse, în timp ce axele lor + Y se potrivesc.

Algoritmul de a face acest lucru este simplu:

  1. Rotiți noul modul de pe axa + Y cu originea de rotație la poziția noului ieșire, astfel încât axa Z + a ieșirii vechi să fie opusă axei Z de ieșire nouă și axele lor + Y să fie identice.
  2. Traduceți noul modul astfel încât noua poziție a ieșirii să fie aceeași cu cea a ieșirii vechi.

Conectarea a două module.

Punerea în aplicare

Pseudo-codul este Python-ish, dar ar trebui să poată fi citit de oricine. Codul sursă eșantion este un proiect Unity.

Să presupunem că lucrăm cu un sistem component-entitate care deține entități într-un grafic de scenă, definind relația părinte-copil. Un bun exemplu al unui motor de joc cu un astfel de sistem este Unitatea, cu obiectele și componentele sale de joc. Modulele și ieșirile sunt entități; ieșirile sunt copii ai modulelor. Modulele au o componentă care definește eticheta lor, iar ieșirile au o componentă care definește etichetele la care este permis să se conecteze.

Ne vom ocupa mai întâi de algoritmul de generare a dungeonului. Constrângerea finală pe care o vom folosi este o serie de iterații ale pașilor de generare a dungeonului.

 Definiție: generate_dungeon (start_module_prefab, module_prefabs, iterații): starting_module = instanțiate (starting_module_prefab) pending_exits = list (starting_module.get_exits ()) în timp ce iterațiile> 0: new_exits = [pending_exit in pending_exits: tag = random.choice (pending_exit.tags) new_module_prefab = get_random_with_tag (module_prefabs, tag) new_module_instance = instanțiate (new_module_prefab) exit_to_match = random.choice (new_module_instance.exits) match_exits (pending_exit, exit_to_match) pentru new_exit în new_module_instance.get_exits (): if_exit! = exit_to_match: new_exits.append ) pending_exits = iterații noi_existe - = 1

instanțiați () funcția creează o instanță a modulelor prefabricate: creează o copie a modulului împreună cu ieșirile sale și le plasează în scenă. get_random_with_tag () funcția se repetă prin toate modulele prefabricate și o alege pe aleatoriu, etichetat cu eticheta furnizată. random.choice () funcția primește un element aleator dintr-o listă sau dintr-o matrice trecută ca parametru.

match_exits funcția în care se desfășoară toată magia și este prezentată în detaliu mai jos:

 def_exit (new_exit, new_exit): new_module = new_exit.parent forward_vector_to_match = old_exit.backward_vector corrective_rotation = azimut (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) azimut de def (vector): # Returnează unghiul semnalizat al vectorului care se rotește relativ la axa globală + axa Z forward = [0, 0, 1] return vector_angle (înainte, vector) * math.copysign (vector. X)

backward_vector proprietatea unei ieșiri este vectorul -Z. rotate_around_y () funcția rotește obiectul în jurul axei + Y cu pivotul său la un punct prevăzut, cu un unghi specificat. translate_global () funcția traduce obiectul cu copiii săi în spațiul global (scenă), indiferent de relația dintre copii la care poate face parte. vector_angle () funcția returnează un unghi între două vectori arbitrari și, în final, math.copysign () funcția copiază semnul unui număr furnizat: -1 pentru un număr negativ, 0 pentru zero și +1 pentru un număr pozitiv.

Extinderea generatorului

Algoritmul poate fi aplicat și altor tipuri de generații mondiale, nu doar temnițelor. Putem extinde definiția unui modul care să acopere nu numai piese de temnita, cum ar fi camere, coridoare și intersecții, dar și mobilier, dulapuri de comori, decorațiuni de cameră etc. Prin plasarea marcatorilor de ieșire în mijlocul unei încăperi sau pe o cameră perete și marcând-o ca pe pradă, decor, sau chiar monstru, putem aduce temnita la viață, cu obiecte pe care le puteți fura, admira sau ucide.

Există o singură modificare care trebuie făcută, astfel încât algoritmul să funcționeze corespunzător: unul dintre markerii prezenți într-un element care poate fi plasat trebuie să fie marcat ca Mod implicit, astfel încât acesta să fie întotdeauna selectat ca cel care va fi aliniat scenei existente.


În imaginea de mai sus, o cameră, două cufere, trei stâlpi, un altar, două lumini și două elemente au fost create și etichetate. O cameră deține un set de markere care fac referire la etichetele altor modele, cum ar fi cufăr, stâlp, altar, sau lumina de perete. Un altar are trei articol markeri pe ea. Aplicând tehnica generării de dungeoni într-o singură cameră, putem crea numeroase variații ale acesteia.

Același algoritm poate fi folosit pentru a crea elemente procedurale. Dacă doriți să creați o sabie, puteți defini modul de prindere ca modul de pornire. Mânerul se va conecta la pommel și la cruce. Cross-guard-ul se va conecta la lama. Având doar trei versiuni ale fiecărei părți ale sabii, ați putea genera 81 de săbii unice.

caveats

Probabil ați observat unele probleme cu modul în care funcționează acest algoritm.

Prima problemă este că cea mai simplă versiune a acesteia construiește temnițele ca un arbore al modulelor, cu rădăcina fiind modulul de pornire. Dacă urmăriți o ramură a structurii temnita, vi se garantează că vă va lovi un capăt mort. Ramurile copacilor nu sunt interconectate, iar temnita nu va avea bușteni de încăperi sau coridoare. O modalitate de a aborda acest lucru ar fi scoaterea deoparte a unor ieșiri ale modulului pentru prelucrarea ulterioară și nu conectarea unor module noi la aceste ieșiri. Odată ce generatorul a trecut prin destule iterații, ar alege o pereche de ieșiri la întâmplare și ar încerca să le conecteze cu un set de coridoare. Există un pic de muncă algoritmică care ar trebui făcută, pentru a găsi un set de module și o modalitate de a le interconecta într-un mod care să creeze o cale accesibilă între aceste ieșiri. Această problemă în sine este suficient de complexă pentru a merita un articol separat.

O altă problemă este că algoritmul nu cunoaște caracteristicile spațiale ale modulelor pe care le plasează; știe doar ieșirile etichetate și orientările și locațiile acestora. Acest lucru face ca modulele să se suprapună. O adăugare a unei simple verificări a coliziunii între un nou modul care trebuie plasat în jurul modulelor existente ar permite algoritmului să construiască dungeon-uri care nu suferă de această problemă. Când modulele se ciocnesc, ar putea să elimine modulul pe care încerca să-l introducă și ar încerca altfel.


Cea mai simplă implementare a algoritmului, fără verificări de coliziune, face ca modulele să se suprapună.

Gestionarea ieșirilor și a etichetelor acestora reprezintă o altă problemă. Algoritmul sugerează definirea etichetelor pentru fiecare instanță de ieșire și etichetarea tuturor camerelor - dar acest lucru este o mulțime de lucrări de întreținere, dacă există o modalitate diferită de a conecta modulele pe care doriți să le încercați. De exemplu, dacă doriți să permiteți conectarea camerelor la coridoare și intersecții în loc de coridoare doar, va trebui să treceți prin toate ieșirile din toate modulele camerei și să le actualizați etichetele. Un mod în jurul acestui lucru este definirea regulilor de conectivitate pe trei nivele separate: dungeon, modul și ieșire. Nivelul Dungeon ar defini reguli pentru întreaga dungeon - ar defini ce etichete pot interconecta. Unele dintre camere ar putea suprascrie regulile de conectivitate atunci când sunt procesate. Ai putea avea o cameră "șef" care să garanteze că în spatele ei există întotdeauna o cameră de "comori". Anumite ieșiri ar depăși cele două niveluri anterioare. Definirea etichetelor pe ieșire permite cea mai mare flexibilitate, dar uneori prea multă flexibilitate nu este atât de bună.

Valorile matematice nu sunt perfecte, iar acest algoritm se bazează foarte mult pe ele. Toate transformările de rotație, orientările arbitrare de ieșire și pozițiile se vor adăuga și pot provoca artefacte ca cusături sau suprapuneri unde se conectează ieșirile, mai ales dincolo de centrul lumii. Dacă acest lucru ar fi prea vizibil, puteți extinde algoritmul pentru a plasa un suport suplimentar în cazul în care modulele se încadrează, cum ar fi un cadru de ușă sau un prag. Artistul tău prietenos va găsi cu siguranță o modalitate de a ascunde imperfecțiunile. Pentru dungeonuri de o dimensiune rezonabilă (mai mică de 10.000 de unități), această problemă nu este nici măcar observabilă, presupunând că a fost făcută suficientă atenție la introducerea și rotirea marcatorilor de ieșire ai modulelor.

Concluzie

Algoritmul, în ciuda unor neajunsuri, oferă un mod diferit de a privi generația de temnițe. Nu veți mai fi constrânși de turnuri de 90 de grade și de camere dreptunghiulare. Artiștii dvs. vor aprecia libertatea creativă pe care această abordare o va oferi, iar jucătorii dvs. se vor bucura de o simțire mai naturală a temnițelor.