Creați un sistem de peșteră Dungeon Generated Procedurally

Pentru mulți, generarea procedurală este un concept magic care nu poate fi atins. Numai dezvoltatorii de jocuri veteran știu cum să construiască un joc care să-și poată crea propriile niveluri ... nu? Acesta poate părea cum ar fi magia, dar PCG (generarea conținutului procedural) poate fi învățat de dezvoltatorii de jocuri pentru începători. În acest tutorial, vă voi arăta cum să generați procedural un sistem de peșteră temnită.


Ce vom face să acopere

Iată un demo SWF care arată tipul de planuri care pot genera această tehnică:


Faceți clic pe SWF pentru a genera un nou nivel.

Învățarea elementelor de bază înseamnă, de obicei, o mulțime de căutări și experimente Google. Problema este că există foarte puține simplu ghiduri despre cum să începeți. Pentru referință, aici sunt câteva surse excelente de informare pe această temă, pe care le-am studiat:

  • Complet Tutorial Roguelike (Python și libtcod)
  • Grid-Based Dungeon Generation
  • PCG Wiki

Înainte de a intra în detalii, este o idee bună să luați în considerare modul în care vom rezolva problema. Iată câteva bucăți ușor de digerat pe care le vom folosi pentru a păstra acest lucru simplu:

  1. Alegeți în mod aleatoriu conținutul creat în lumea jocurilor.
  2. Verificați dacă conținutul este plasat într-un loc care are sens.
  3. Verificați dacă conținutul dvs. este accesibil de către player.
  4. Repetați acești pași până când nivelul dvs. se reuneste frumos.

Odată ce am lucrat prin următoarele exemple, ar trebui să aveți abilitățile necesare pentru a experimenta cu PCG în propriile jocuri. Interesant, eh?


Unde ne plasăm conținutul jocului?

Primul lucru pe care îl vom face este plasarea aleatorie a camerelor unui nivel de dungeon generat procedural.

Pentru a urmări, este o idee bună să aveți o înțelegere de bază a modului în care funcționează hărțile de țigle. În cazul în care aveți nevoie de o privire de ansamblu rapidă sau o actualizare completă, consultați acest tutorial pentru harta țiglelor. (Este orientat spre Flash, dar, chiar dacă nu sunteți familiarizat cu Flash, este încă bine pentru a obține o listă de hărți de țiglă.)

Crearea unei camere care să fie plasată în nivelul dvs. de temnita

Înainte de a începe, trebuie să umplem harta țiglelor cu plăci de perete. Tot ce trebuie să faceți este să repetați fiecare loc din hartă (o matrice 2D, în mod ideal) și să plasați placa.

De asemenea, trebuie să convertim coordonatele pixelilor din fiecare dreptunghi la coordonatele rețelei noastre. Dacă doriți să treceți de la pixeli la locația rețelei, împărțiți coordonatele pixelilor în funcție de lățimea plăcii. Pentru a trece de la rețea la pixeli, multiplicați coordonatele rețelei cu lățimea plăcii.

De exemplu, dacă vrem să plasăm colțul din stânga sus al camerei noastre (5, 8) pe grilă și avem o lățime de țiglă 8 pixeli, ar trebui să punem colțul ăsta la (5 * 8, 8 * 8) sau (40, 64) în coordonate pixeli.

Să creăm a Cameră clasă; ar putea arăta astfel în codul Haxe:

class Room extinde Sprite // aceste valori dețin coordonatele rețelei pentru fiecare colț al camerei public var x1: Int; public var x2: Int; public var1: Int; public var2: Int; // lățimea și înălțimea camerei din punct de vedere al rețelei publice var w: Int; public var h: Int; // punctul central al camerei publice var var: Point; // constructor pentru crearea de noi funcții publice noi (x: int, y: int, w: int, h: int) super (); x1 = x; x2 = x + w; y1 = y; y2 = y + h; this.x = x * Main.TILE_WIDTH; this.y = y * Main.TILE_HEIGHT; aceasta.w = w; this.h = h; center = punct nou (Math.floor ((x1 + x2) / 2), Math.floor ((y1 + y2) / 2));  // return true dacă această cameră se intersectează cu condiția intersectării camerei publice (cameră: cameră): Bool return (x1 <= room.x2 && x2 >= room.x1 && y1 <= room.y2 && room.y2 >= room.y1); 

Avem valori pentru lățimea, înălțimea, poziția punctului central și pozițiile celor patru colțuri ale fiecărei camere și o funcție care ne spune dacă această încăpere intersectează o altă încăpere. De asemenea, rețineți că totul, cu excepția valorilor x și y, se află în sistemul nostru de coordonate de rețea. Acest lucru se datorează faptului că face ca viața să fie mult mai ușoară de a utiliza numere mici de fiecare dată când accesăm valorile camerei.

Bine, avem cadrul pentru o cameră în loc. Acum, cum procedăm generând și plasăm o cameră? Ei bine, datorită generatoarelor de numere aleatoare încorporate, această parte nu este prea dificilă.

Tot ce trebuie să facem este să furnizăm valori aleatorii x și y pentru camera noastră în limitele hărții și să oferim valori ale lățimii și înălțimii aleatorii într-un interval predeterminat.


Plasarea noastră în mod aleatoriu are sens?

Deoarece folosim locațiile și dimensiunile aleatoare pentru camerele noastre, suntem obligați să se suprapună cu camerele create anterior, pe măsură ce umplem temnita noastră. Ei bine, am codificat deja un simplu intersecteaza () pentru a ne ajuta să rezolvăm problema.

De fiecare dată când încercăm să plasăm o cameră nouă, sunăm pur și simplu intersecteaza () pe fiecare pereche de camere din întreaga listă. Această funcție returnează o valoare booleană: Adevărat dacă încăperile se suprapun și fals in caz contrar. Putem folosi acea valoare pentru a decide ce să facem cu camera pe care tocmai am încercat să o plasăm.


Verificați din nou la intersecteaza () funcţie. Puteți vedea cum valorile x și y se suprapun și se întorc Adevărat.
 funcția private placeRooms () // a crea matrice pentru spațiul de stocare pentru camere cu acces ușor = Array nou (); // valorile randomizate pentru fiecare cameră pentru (r în 0 ... maxRooms) var w = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var h = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var x = Std.random (MAP_WIDTH -w-1) + 1; var y = Std.random (MAP_HEIGHT - h-1) + 1; // crea camera cu valori randomizate var newRoom = cameră nouă (x, y, w, h); var failed = false; pentru (altRoom în camere) if (newRoom.intersects (otherRoom)) failed = true; pauză;  dacă (! a eșuat) // funcția locală pentru a extrage noua cameră createRoom (newRoom); // împingeți camera nouă în camerele array rooms.push (newRoom)

Cheia este aici a eșuat boolean; este setat la valoarea de retur din intersecteaza (), și așa este Adevărat dacă (și numai dacă) camerele dvs. se suprapun. Odată ce ieșim din bucla, verificăm asta a eșuat variabilă și, dacă este falsă, putem scoate camera nouă. În caz contrar, vom renunța la cameră și vom încerca din nou până când vom ajunge la numărul maxim de camere.


Cum ar trebui să tratăm conținut inaccesibil?

Marea majoritate a jocurilor care utilizează conținut generat de procedură se străduiesc să facă tot ce poate fi atins de către jucător, dar există câțiva oameni care cred că aceasta nu este neapărat cea mai bună decizie de proiectare. Dacă ai avea niște camere în temnița pe care le-ar putea juca rareori, dar ar putea vedea întotdeauna? Acest lucru ar putea adăuga o dinamică interesantă în temnița ta.

Bineînțeles, indiferent de ce parte a argumentului sunteți, probabil că este încă o idee bună să vă asigurați că jucătorul poate progresa mereu prin joc. Ar fi destul de frustrant dacă ai ajuns la un nivel din temnița jocului și ieșirea a fost complet blocată.

Având în vedere că cele mai multe jocuri trag pentru conținut 100% accesibil, vom rămâne cu asta.

Să ne ocupăm de această disponibilitate

Până acum, ar trebui să aveți o hartă de țigle și să fie difuzate și ar trebui să existe cod în loc pentru a crea un număr variabil de camere de dimensiuni diferite. Uită-te la asta; aveți deja niște camere dungeon generate de proceduri inteligente!

Acum, obiectivul este de a conecta fiecare cameră, astfel încât să putem merge prin temnita noastră și ajunge în cele din urmă o ieșire care duce la nivelul următor. Putem realiza acest lucru prin realizarea de coridoare între camere.

Va trebui să adăugăm o punct variabilă la cod pentru a urmări centrul fiecărei camere create. Ori de câte ori creăm și plasăm o cameră, determinăm centrul ei și îl conectăm la centrul camerei anterioare.

În primul rând, punem în aplicare coridoarele:

(x1, x2)) + 1) x (x1, x2) // distrugeți plăcile pentru a "sculpta" pe harta coridorului [x] [y] .parent.removeChild (hartă [x] [y]); // plasați o nouă hartă neablocată a plăcilor [x] [y] = țiglă nouă (Tile.DARK_GROUND, false, false); // adăugați țiglă ca un obiect de joc nou addChild (hartă [x] [y]); // setați locația plăcii în mod corespunzător [x] [y] .setLoc (x, y);  // a crea coridor vertical pentru a conecta funcțiile private vCorridor (y1: int, y2: int, x) pentru (y în Std.int (Math.min (y1, y2)) ... Std.int (Math.max (y1, y2)) + 1) // distrugeți plăcile pentru a "sculpta" pe harta coridorului [x] [y] .parent.removeChild (harta [x] [y]); // plasați o nouă hartă neablocată a plăcilor [x] [y] = țiglă nouă (Tile.DARK_GROUND, false, false); // adăugați țiglă ca un obiect de joc nou addChild (hartă [x] [y]); // setați locația plăcii în mod corespunzător [x] [y] .setLoc (x, y); 

Aceste funcții acționează aproape în același mod, dar unul se sculă pe orizontală, iar celălalt pe verticală.

Conectarea primei camere la a doua cameră necesită a vCorridor si un hCorridor.

Avem nevoie de trei valori pentru a face acest lucru. Pentru coridoare orizontale avem nevoie de valoarea de pornire x, valoarea de sfârșit de x și valoarea curentă y. Pentru coridoarele verticale avem nevoie de valorile y de pornire și de sfârșit, împreună cu valoarea curentă x.

Deoarece ne mișcăm de la stânga la dreapta, avem nevoie de cele două valori x corespunzătoare, dar numai o valoare y, deoarece nu ne mișcăm în sus sau în jos. Când ne mișcăm vertical vom avea nevoie de valorile y. În pentru la începutul fiecărei funcții, iterăm de la valoarea de pornire (x sau y) până la valoarea finală până când vom fi sculptat întregul coridor.

Acum, când avem codul coridorului în loc, ne putem schimba placeRooms () funcția și apelați noile noastre funcții de coridor:

 funcția privată placeRooms () // depozitează camere într-o matrice pentru camere cu acces ușor = Array nou (); // variabila pentru centrul de urmărire a fiecărei camere var newCenter = null; // valorile randomizate pentru fiecare cameră pentru (r în 0 ... maxRooms) var w = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var h = minRoomSize + Std.random (maxRoomSize - minRoomSize + 1); var x = Std.random (MAP_WIDTH -w-1) + 1; var y = Std.random (MAP_HEIGHT - h-1) + 1; // crea camera cu valori randomizate var newRoom = cameră nouă (x, y, w, h); var failed = false; pentru (altRoom în camere) if (newRoom.intersects (otherRoom)) failed = true; pauză;  dacă (! a eșuat) // funcția locală pentru a extrage noua cameră createRoom (newRoom); // magazin centru pentru cameră nouă newCenter = newRoom.center; dacă rooms.length! = 0) // centrul magazinului camerei precedente var prevCenter = camere [camere.length - 1] .center; // se creează coridoare între camere bazate pe centre // începeți aleatoriu cu coridoare orizontale sau verticale dacă (Std.random (2) == 1) hCorridor (Std.int (prevCenter.x), Std.int (newCenter.x ), Std.int (prevCenter.y)); vCorridor (Std.int (prevCenter.y), Std.int (newCenter.y), Std.int (newCenter.x));  altceva vCorridor (Std.int (prevCenter.y), Std.int (newCenter.y), Std.int (prevCenter.x)); hCorridor (Std.int (prevCenter.x), Std.int (newCenter.x), Std.int (newCenter.y));  dacă (! a eșuat) rooms.push (newRoom); 

În imaginea de mai sus, puteți urma creația coridorului din prima cameră până în a patra: roșu, verde, apoi albastru. Puteți obține rezultate interesante în funcție de amplasarea camerelor - de exemplu, două coridoare unul lângă celălalt fac un coridor dublu.

Am adăugat câteva variabile pentru a urmări centrul fiecărei camere și am atașat camerele cu coridoarele dintre centrele lor. Acum există mai multe camere și coridoare care nu se suprapun și care mențin întreaga nivel de dungeon conectat. Nu-i rău.


Am terminat cu Temnița noastră, bine?

Ați parcurs un drum lung pentru a vă construi primul nivel de dungeon generat de procedură și sper că v-ați dat seama că PCG nu este o fiară magică pe care nu o veți avea niciodată șansa de a ucide.

Am trecut peste cum să plasați în mod aleatoriu conținutul în jurul nivelului dvs. de temnita cu generatoare simple de numere aleatorii și câteva intervale predeterminate pentru a vă menține conținutul la dimensiunea potrivită și aproximativ în locul potrivit. Apoi, am descoperit o modalitate foarte simplă de a determina dacă plasarea aleatoare a făcut sens prin verificarea camerelor suprapuse. În cele din urmă, am vorbit puțin despre meritele de a vă menține conținutul accesibil și am găsit o modalitate de a vă asigura că jucătorul dvs. poate ajunge la fiecare cameră din temniță.

Primii trei pași ai procesului nostru de patru etape sunt finalizați, ceea ce înseamnă că aveți blocurile de construcție a unei mari temnite pentru următorul joc. Ultimul pas este pentru tine: trebuie să repetați ceea ce ați învățat pentru a crea mai mult conținut generat procedural pentru o repetabilitate nesfârșită.

Există întotdeauna mai multe de învățat

Metoda de sculptare a nivelelor de temnita simple din acest tutorial scarpina doar suprafata PCG si exista alti algoritmi simpli pe care le puteti culege cu usurinta.

Provocarea mea pentru tine este să începi să experimenti cu începutul jocului pe care l-ai creat aici și să faci niște cercetări în mai multe metode pentru a-ți schimba temnițele.

O metodă excelentă pentru crearea nivelurilor de peșteră este utilizarea automatelor celulare, care au posibilități infinite de a personaliza nivelurile de dungeon. O altă metodă excelentă de învățare este partiționarea spațială binară (BSP), care creează niște niveluri de temnita.

Sper că acest lucru ți-a dat un început bun în generarea conținutului procedural. Asigurați-vă că ați comentat mai jos cu orice întrebări aveți și mi-ar plăcea să văd câteva exemple despre ceea ce creați cu PCG.

postări asemănatoare
  • Generați nivele aleatoare de peșteri utilizând automatele celulare