Generați nivele aleatoare de peșteri utilizând automatele celulare

Generatoare de conținut de procedură sunt biți de cod scrise în joc, care pot crea oricând noi piese de conținut de joc - chiar și atunci când jocul se execută! Dezvoltatorii de jocuri au încercat să genereze proceduri de la lumi 3D la coloane sonore muzicale. Adăugarea unei generații în jocul dvs. este o modalitate excelentă de a conecta o valoare suplimentară: jucătorii îl iubesc deoarece primesc conținut nou, imprevizibil și interesant de fiecare dată când joacă.

În acest tutorial, vom analiza o metodă excelentă de generare a nivelelor aleatoare și vom încerca să întindem limitele a ceea ce credeți că poate fi generat.

postări asemănatoare

Dacă sunteți interesat să citiți mai multe despre subiectele de generare a conținutului procedural, design de nivel, AI sau automate celulare, asigurați-vă că verificați aceste alte postări:

  • Crearea vieții: Jocul vieții lui Conway
  • Portal 2 Level Design: Crearea de puzzle-uri pentru a-ți provoca jucătorii
  • StarCraft II Design de nivel: Designul estetic și sfaturile editorului
  • Codarea unui generator de secvențe personalizate pentru a realiza un Starscape
  • Gamedev Gamedev: Generatoare de secvențe și generatoare de numere pseudorandomice

Bine ați venit în Peșteri!

În acest tutorial, vom construi un generator de peșteri. Peșterile sunt minunate pentru tot felul de genuri și setări de jocuri, dar îmi amintesc în special de vechile temnițe în jocuri de rol.

Uitați-vă la demonstrația de mai jos pentru a vedea tipurile de ieșiri pe care le veți putea obține. Faceți clic pe "Lumea nouă" pentru a crea o peșteră nouă pentru a vă uita. Vom vorbi despre ceea ce fac diferitele setări în timp util.


Acest generator ne întoarce de fapt o gamă largă de blocuri bidimensionale, fiecare dintre ele fiind solidă sau goală. Deci, de fapt, ați putea folosi acest generator pentru tot felul de jocuri, în afară de crawlerele dungeon-ului: nivele aleatorii pentru jocuri de strategie, tilemaps pentru jocuri de platformă, poate chiar și ca arene pentru un shooter multiplayer! Dacă te uiți atent, flipping blocurile solide și goale face și un generator de insulă. Totul utilizează același cod și ieșire, ceea ce face ca acesta să fie un instrument cu adevărat flexibil.

Să începem prin a pune o întrebare simplă: oricum, pe pământ este un automatizator celular?


Noțiuni de bază cu celule

În anii 1970, un matematician numit John Conway a publicat o descriere a jocului de viață, uneori numit Life. Viața nu era într-adevăr un joc; a fost mai mult ca o simulare care a luat o grilă de celule (care ar putea fi fie viu, fie mort) și le-a aplicat niște reguli simple.

Au fost aplicate patru reguli pentru fiecare celulă în fiecare pas al simulării:

  1. Dacă o celulă vie are mai puțin de doi vecini, ea moare.
  2. Dacă o celulă vii are doi sau trei vecini, ea rămâne în viață.
  3. Dacă o celulă vii are mai mult de trei vecini, ea moare.
  4. Dacă o celulă moartă are exact trei vecini vii, devine viu.

Frumoasă și simplă! Cu toate acestea, dacă încercați diferite combinații de grile de pornire, puteți obține rezultate foarte ciudate. Bucle infinite, mașini care scuipă forme și multe altele. Jocul Vieții este un exemplu de a celular automat - o grilă de celule care sunt guvernate de anumite reguli.

Vom implementa un sistem foarte asemănător cu cel al Vieții, dar în loc de a produce modele și forme amuzante, va crea sisteme uimitoare de peșteri pentru jocurile noastre.


Implementarea unui automatizator celular

Vom reprezenta grila noastră celulară ca o matrice bidimensională de boolean (Adevărat sau fals) valori. Acest lucru ne convine pentru că suntem interesați doar dacă o faianță este solidă sau nu.

Iată-ne inițiază grilă de celule:

boolean [] [] celulă = nouă booleană [lățime] [înălțime];

Bacsis: Observați că primul index este coordonatul x pentru matrice, iar cel de-al doilea index este coordonatul y. Acest lucru face accesarea matricei mai naturală în cod.

În majoritatea limbajelor de programare, această matrice se va inițializa cu toate valorile setate la fals. E bine pentru noi! Dacă un indice matrice (X y) este fals, vom spune că celula este goală; daca este Adevărat, acea țiglă va fi roci solide.

Fiecare dintre aceste poziții de matrice reprezintă una dintre "celulele" rețelei noastre celulare. Acum trebuie să ne stabilim grila astfel încât să putem începe să construim peșterile noastre.

Vom începe prin a stabili la întâmplare fiecare celulă fie morală, fie în viață. Fiecare celulă va avea aceeași șansă aleatoare de a fi în viață și ar trebui să vă asigurați că această valoare șansă este stabilită într-o variabilă undeva, pentru că vom dori cu siguranță să o modificăm mai târziu și să o avem undeva ușor de accesat, ne va ajuta acea. O să folosesc 45% pentru a începe cu.

float chanceToStartAlive = 0.45f; public boolean [] [] initialiseMap (boolean [] [] harta) pentru (int x = 0; x 
Pestera noastră aleatoare înaintea oricărei etape de simulare a automatizării celulare.

Dacă executăm acest cod, ajungem cu o grilă mare de celule, cum ar fi cea de mai sus, care este aleatoriu viu sau mort. Este dezordonat și cu siguranță nu arată ca un sistem de peșteri pe care l-am văzut vreodată. Deci ce urmeaza?


Creșterea peșterilor noștri

Amintiți-vă de regulile care guvernează celulele în Jocul Vieții? De fiecare dată când simularea mergea cu un pas, fiecare celulă ar verifica regulile vieții și ar vedea dacă s-ar schimba în viață sau mort. Vom folosi exact aceeași idee pentru a construi peșterile noastre - vom scrie o funcție acum care bate peste fiecare celulă din rețea și aplică câteva reguli de bază pentru a decide dacă trăiește sau moare.

Așa cum veți vedea mai târziu, vom folosi acest cod de mai multe ori, așa că punerea în funcția sa înseamnă că o putem numi cât mai multe ori mai multe ori cât ne place. Îi vom da un nume informativ cum ar fi doSimulationStep (), de asemenea.

Ce trebuie să facă funcția? Ei bine, mai întâi, vom face o nouă rețea în care să putem pune valorile noastre de celule actualizate. Pentru a înțelege de ce trebuie să facem acest lucru, rețineți că pentru a calcula noua valoare a unei celule din rețea, trebuie să ne uităm la cei opt vecini ai săi:

Dar dacă am calculat deja noua valoare a unora dintre celule și le-am pus înapoi în rețea, atunci calculul nostru va fi un amestec de date noi și vechi, după cum urmează:

Hopa! Nu asta vrem deloc. Deci, de fiecare dată când calculam o nouă valoare a celulei, în loc să o punem înapoi pe vechea hartă, o vom scrie într-o nouă.

Să începem să scriem asta doSimulationStep () funcție, atunci:

publicul doSimulationStep (boolean [] [] oldMap) boolean [] [] newMap = boolean nou [lățime] [înălțime]; // ... 

Vrem să luăm în considerare fiecare celulă din rețea și să numărăm câți dintre vecinii săi sunt vii și morți. Numărarea vecinilor într-o matrice este unul din acele biți plictisitori de cod, va trebui să scrieți de un milion de ori. Iată o implementare rapidă a acesteia într-o funcție pe care am apelat-o countAliveNeighbours ():

// Returnează numărul de celule dintr-un inel în jurul valorii (x, y) care sunt în viață. public countAliveNeighbours (boolean [] [] hartă, int x, int y) int count = 0; pentru (int i = -1; i<2; i++) for(int j=-1; j<2; j++) int neighbour_x = x+i; int neighbour_y = y+j; //If we're looking at the middle point if(i == 0 && j == 0) //Do nothing, we don't want to add ourselves in!  //In case the index we're looking at it off the edge of the map else if(neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= map.length || neighbour_y> = hartă [0]. lungime) count = count + 1;  // În caz contrar, o verificare normală a vecinului altcuiva dacă map [neighour_x] [neighbour_y]) count = count + 1; 

Câteva lucruri despre această funcție:

În primul rând, pentru buclele sunt puțin ciudate dacă nu ați mai făcut așa ceva înainte. Ideea este că vrem să privim toate celulele din jurul punctului (X y). Dacă vă uitați la ilustrația de mai jos, puteți vedea cum indicii doriti sunt unul mai mic, egal și unul mai mult decât indicele original. Cei doi pentru buclele ne dau doar asta, începând de la -1, și de a trece prin +1. Apoi adăugăm asta la indicele original din interiorul lui pentru pentru a găsi fiecare vecin.

În al doilea rând, observați că dacă verificăm o referință la grilă care nu este reală (de exemplu, este pe marginea hărții), o considerăm drept vecinătate. Prefer acest lucru pentru generația de peșteri, deoarece acesta tinde să ajute la umplerea marginilor hărții, dar puteți experimenta dacă nu faceți acest lucru dacă vă place.

Deci, acum, să ne întoarcem la noi doSimulationStep () funcția și adăugați un cod mai mult:

public boolean [] [] doSimulationStep (boolean [] [] oldMap) boolean [] [] newMap = boolean nou [lățime] [înălțime]; // Culegere peste fiecare rând și coloană a hărții pentru (int x = 0; x birthLimit) newMap [x] [y] = adevărat;  altceva newMap [x] [y] = false;  retur noumap; 

Aceasta buclează întreaga hartă, aplicând regulile noastre fiecărei celule de rețea pentru a calcula noua valoare și pentru ao plasa newMap. Regulile sunt mai simple decât jocul vieții - avem două variabile speciale, una pentru celulele moarte (birthLimit) și una pentru uciderea celulelor vii (deathLimit). Dacă celulele vii sunt înconjurate de mai puțin de deathLimit celulele mor și dacă celulele moarte sunt aproape cel puțin birthLimit celulele devin vii. Frumos și simplu!

Tot ce a mai rămas la final este o atingere finală pentru a reveni la harta actualizată. Această funcție reprezintă un singur pas al regulilor automatelor noastre celulare - următorul pas este să înțelegem ce se întâmplă pe măsură ce o aplicăm o dată, de două ori sau de mai multe ori la harta noastră inițială de pornire.


Tweaking și Tuning

Să ne uităm la ceea ce arată acum generația de cod, folosind codul pe care l-am scris până acum.

public boolean [] [] generateMap () // Creați o nouă hartă booleană [] [] cellmap = nouă booleană [lățime] [înălțime]; // Configurați harta cu valorile aleatoare cellmap = initialiseMap (cellmap); // Și acum rulați simularea pentru un număr de pași setați pentru (int i = 0; i 

Singurul bit cu adevărat nou al codului este a pentru buclă care rulează metoda noastră de simulare de un număr de ori. Din nou, plasați-o într-o variabilă, astfel încât să o putem schimba, pentru că acum vom începe să jucăm cu aceste valori!

Până acum am stabilit aceste variabile:

  • chanceToStartAlive stabilește cât de densă este grila inițială cu celulele vii.
  • starvationLimit este limita inferioară a vecinului la care celulele încep să moară.
  • overpopLimit este limita superioară a vecinului la care celulele încep să moară.
  • birthNumber este numărul de vecini care provoacă o viață moartă.
  • numberOfSteps este numărul de timpuri care efectuează pasul de simulare.

Pestera noastră aleatorie după doi pași de simulare automată a automatelor.

Puteți rula cu aceste variabile în demo-ul din partea de sus a paginii. Fiecare valoare va schimba demo-ul dramatic, așa că trebuie să jucați în jur și să vedeți ce se potrivește.

Una dintre cele mai interesante schimbări pe care le puteți aduce este numberOfSteps variabil. Pe măsură ce executați simularea pentru mai mulți pași, rugozitatea hărții dispare, iar insulele se îndepărtează fără nimic. Am adaugat un buton pentru ca tu sa poti suna manual functia si sa vezi efectele. Experimentați un pic și veți găsi o combinație de setări care se potrivește stilului dvs. și felul de niveluri pe care jocul dvs. are nevoie.


Pestera noastră aleatorie după șase pași de simulare automată a automatelor.

Cu asta, ai terminat. Felicitări - tocmai ați făcut un generator de nivel procedural, bine făcut! Stați în spate, rulați și reluați codul și zâmbiți la sistemele ciudate și minunate de peșteri care ies. Bine ați venit în lumea generațiilor procedurale.


Luând-o mai departe

Dacă vă uitați la generatorul de peșteri minunat și vă întrebați ce altceva puteți face cu acesta, iată câteva idei de atribuire "extra credit":

Folosind Flood Fill pentru a face verificarea calității

Încărcarea de umplere este un algoritm foarte simplu pe care îl puteți utiliza pentru a găsi toate spațiile într-o matrice care se conectează la un anumit punct. La fel cum sugerează și numele, algoritmul funcționează puțin ca și cum ar fi turnat o găleată de apă în nivelul dvs. - se extinde de la punctul de plecare și umple toate colțurile.

Umplerea de umplere este excelentă pentru automatele celulare, deoarece le puteți folosi pentru a vedea cât de mare este o peșteră specială. Dacă executați demo-ul de câteva ori, veți observa că unele hărți sunt alcătuite dintr-o singură peșteră mare, în timp ce altele au câteva peșteri mai mici care sunt separate unul de celălalt. Plăcerea de umplere vă poate ajuta să detectați cât de mare este o peșteră și apoi să regenerați nivelul dacă este prea mic sau să decideți unde doriți să pornească jucătorul dacă credeți că este suficient de mare. Există o schiță excelentă a umplerii inundațiilor pe Wikipedia.

Plasarea rapidă și simplă a comorilor

Plasarea comorii în zonele răcoroase necesită uneori o mulțime de coduri, dar putem scrie destul de puțin un cod simplu pentru a scoate comoara din calea noastră în sistemele noastre de peșteri. Avem deja codul nostru care numără câți vecini are un pătrat, prin looping peste sistemul nostru de peșteri finalizat, putem vedea cât de înconjurat de ziduri un anumit pătrat este.

Dacă o celulă grilă goală este înconjurată de o mulțime de ziduri solide, este probabil chiar la capătul unui coridor sau ascuns în pereții sistemului de peșteri. Acesta este un loc minunat pentru a ascunde comoara - deci, făcând un simplu control al vecinilor noștri, putem aluneca comori în colțuri și pe alee.

public void placeTreasure (boolean [] [] lume) // Cât de ascuns trebuie să fie un loc pentru comori? // Am găsit 5 sau 6 este bună. 6 pentru o comoară foarte rară. int treasureHiddenLimit = 5; pentru (int x = 0; x < worldWidth; x++) for (int y=0; y < worldHeight; y++) if(!world[x][y]) int nbs = countAliveNeighbours(world, x, y); if(nbs >= treasureHiddenLimit); placeTreasure (x, y); 

Acest lucru nu este perfect. Uneori pune comori în găuri inaccesibile în sistemul de peșteri, iar uneori și petele vor fi destul de evidente. Dar, într-o manevră, este o modalitate foarte bună de a împrăștia colecțiile în jurul nivelului tău. Încercați-l în demo prin lovirea placeTreasure () buton!


Concluzii și lecturi suplimentare

Acest tutorial vă arată cum să construiți un generator de bază dar complet. Cu doar câțiva pași simpli am scris un cod care poate crea nivele noi în clipi de la ochi. Sperăm că acest lucru v-a oferit un degustator al potențialului de a crea generatoare de conținut procedural pentru jocurile dvs.!

Dacă doriți să citiți mai multe, Roguebasin este o sursă excelentă de informații despre sistemele procedurale de generare. Se concentrează în cea mai mare parte pe jocuri roguelike, dar multe dintre tehnicile sale pot fi folosite în alte tipuri de jocuri și există o mulțime de inspirație pentru generarea procedurală a altor părți ale jocului!

Dacă doriți mai multe despre generarea conținutului procedural sau a automatelor celulare, iată o versiune excelentă on-line a gamei de viață (deși am recomandat să tastați "Conway's Game of Life" în Google). S-ar putea să vă placă, de asemenea, Wolfram Tones, un experiment fermecător în utilizarea automatelor celulare pentru a genera muzică!