Voor velen is procesgeneratie een magisch concept dat net buiten bereik is. Alleen ervaren game-ontwikkelaars weten hoe ze een game moeten bouwen die zijn eigen levels kan maken ... toch? Het kan lijken zoals magie, maar PCG (procedurele content-generatie) kan worden geleerd door beginnende gameontwikkelaars. In deze zelfstudie laat ik je zien hoe je procedureel een kerkersgrotssysteem genereert.
Hier is een SWF-demo die de soort niveau-indelingen laat zien die deze techniek kan genereren:
Het leren van de basisprincipes betekent meestal veel zoeken en experimenteren met Google. Het probleem is, er zijn er maar heel weinig eenvoudig handleidingen over hoe te beginnen. Ter referentie, hier zijn enkele uitstekende informatiebronnen over het onderwerp, die ik heb bestudeerd:
Voordat we ingaan op de details, is het een goed idee om te overwegen hoe we het probleem gaan oplossen. Hier zijn enkele gemakkelijk te verteren brokken die we zullen gebruiken om dit ding eenvoudig te houden:
Zodra we de volgende voorbeelden hebben doorgenomen, moet u de nodige vaardigheden hebben om met PCG in uw eigen games te experimenteren. Spannend, eh?
Het eerste dat we gaan doen is willekeurig de kamers van een procedureel gegenereerd kerker-niveau plaatsen.
Om dit te kunnen volgen, is het een goed idee om een basiskennis te hebben van hoe tegelkaarten werken. Als je snel een overzicht of een opfrisser nodig hebt, bekijk dan deze tutorial met de tegelkaart. (Het is gericht op Flash, maar, zelfs als u niet bekend bent met Flash, is het nog steeds goed om de essen van tegelkaarten te krijgen.)
Voordat we beginnen, moeten we onze tegelkaart vullen met wandtegels. Het enige dat u hoeft te doen, is het herhalen van elke plek in uw kaart (een 2D-matrix, bij voorkeur) en de tegel plaatsen.
We moeten ook de pixelcoördinaten van elke rechthoek naar onze rastercoördinaten converteren. Als u van pixel naar rasterlocatie wilt gaan, deelt u de pixelcoördinaat op met de tegelbreedte. Als u van raster naar pixel wilt gaan, vermenigvuldigt u de rastercoördinaat met de tegelbreedte.
Als we bijvoorbeeld de linkerbovenhoek van onze kamer willen plaatsen (5, 8)
op ons raster en we hebben een tegelbreedte van 8
pixels, we zouden die hoek moeten plaatsen (5 * 8, 8 * 8)
of (40, 64)
in pixelcoördinaten.
Laten we een maken Kamer
klasse; het ziet er misschien zo uit in de Haxe-code:
class Room breidt Sprite uit // deze waarden bevatten rastercoördinaten voor elke hoek van de ruimte public var x1: Int; public var x2: Int; public var y1: Int; public var y2: Int; // breedte en hoogte van de kamer in termen van netwerk publieke var w: Int; public var h: Int; // middelpunt van de zaal public var centre: Point; // constructor voor het maken van nieuwe kamers public function new (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; this.w = w; this.h = h; center = nieuw punt (Math.floor ((x1 + x2) / 2), Math.floor ((y1 + y2) / 2)); // return true als deze kamer kruist op voorwaarde dat room public function snijdt (room: Room): Bool return (x1 <= room.x2 && x2 >= room.x1 && y1 <= room.y2 && room.y2 >= room.y1);
We hebben waarden voor de breedte, hoogte, middelpuntpositie en posities van vier hoeken, en een functie die ons vertelt of deze kamer een andere kruist. Merk ook op dat alles behalve de x- en y-waarden zich in ons rastercoördinatensysteem bevinden. Dit komt omdat het het leven veel gemakkelijker maakt om kleine aantallen te gebruiken elke keer dat we de kamerwaarden benaderen.
Oké, we hebben het raamwerk voor een kamer op zijn plaats. Hoe kunnen we nu procedureel een kamer genereren en plaatsen? Welnu, dankzij de ingebouwde generator voor willekeurige getallen is dit deel niet zo moeilijk.
Het enige wat we moeten doen, is het geven van willekeurige x- en y-waarden voor onze kamer binnen de grenzen van de kaart, en willekeurige breedte- en hoogtewaarden geven binnen een vooraf bepaald bereik.
Omdat we willekeurige locaties en dimensies gebruiken voor onze kamers, zijn we gebonden om te overlappen met eerder gemaakte kamers terwijl we onze kerker vullen. Wel, we hebben al een eenvoudig gecodeerd intersects ()
methode om ons te helpen het probleem op te lossen.
Elke keer dat we proberen een nieuwe kamer te plaatsen, bellen we gewoon intersects ()
op elk paar kamers binnen de volledige lijst. Deze functie retourneert een Booleaanse waarde: waar
als de ruimtes elkaar overlappen, en vals
anders. We kunnen die waarde gebruiken om te beslissen wat we moeten doen met de kamer die we zojuist hebben geprobeerd te plaatsen.
intersects ()
functie. U kunt zien hoe de x- en y-waarden elkaar overlappen en terugkeren waar
. private function placeRooms () // array maken voor ruimteopslag voor eenvoudig toegankelijke ruimten = new Array (); // randomiseer waarden voor elke kamer voor (r in 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; // maak een kamer met willekeurige waarden var newRoom = new Room (x, y, w, h); var failed = false; for (otherRoom in rooms) if (newRoom.intersects (otherRoom)) failed = true; breken; if (! failed) // lokale functie om nieuwe room te creëren createRoom (newRoom); // duw nieuwe kamer naar kamers array rooms.push (newRoom)
De sleutel hier is de mislukt
Boolean; het is ingesteld op de geretourneerde waarde van intersects ()
, en zo is het ook waar
als (en alleen als) je kamers elkaar overlappen. Zodra we uit de lus breken, controleren we dit mislukt
variabel en, als het niet waar is, kunnen we de nieuwe kamer uitsnijden. Anders negeren we de kamer en proberen we opnieuw totdat we ons maximum aantal kamers hebben bereikt.
De overgrote meerderheid van games die procedureel gegenereerde inhoud gebruiken, streeft ernaar al die inhoud bereikbaar te maken voor de speler, maar er zijn een paar mensen die geloven dat dit niet noodzakelijk de beste ontwerpbeslissing is. Wat als je een aantal kamers in je kerker had die de speler maar zelden kon bereiken maar altijd kon zien? Dit kan een interessante dynamiek toevoegen aan je kerker.
Natuurlijk, het maakt niet uit aan welke kant van het argument je staat, het is waarschijnlijk nog steeds een goed idee om ervoor te zorgen dat de speler altijd door het spel kan gaan. Het zou behoorlijk frustrerend zijn als je een level van de kerker van het spel zou bereiken en de uitgang volledig was afgesloten.
Aangezien de meeste games foto's maken voor 100% bereikbare inhoud, houden we ons daaraan.
Inmiddels moet u een tegelkaart in gebruik hebben en moet er een code aanwezig zijn om een variabel aantal kamers van verschillende grootte te maken. Moet je zien; je hebt al enkele slimme, procedureel gegenereerde kerkerruimten!
Het doel is nu om elke kamer met elkaar te verbinden zodat we door onze kerker kunnen lopen en uiteindelijk een uitgang kunnen bereiken die naar het volgende niveau leidt. We kunnen dit bereiken door de gangen tussen de kamers uit te graven.
We moeten een toevoegen punt
variabel voor de code om het centrum van elke gecreëerde ruimte bij te houden. Wanneer we een kamer maken en plaatsen, bepalen we het midden ervan en verbinden we het met het centrum van de vorige kamer.
Eerst zullen we de gangen implementeren:
persoonlijke functie hCorridor (x1: Int, x2: Int, y) for (x in Std.int (Math.min (x1, x2)) ... Std.int (Math.max (x1, x2)) + 1) // destory de tegels om de kaart "uit te knippen" [x] [y] .parent.removeChild (map [x] [y]); // plaats een nieuwe niet-geblokkeerde tegelkaart [x] [y] = nieuwe tegel (Tile.DARK_GROUND, false, false); // voeg tegel toe als een nieuw spelobject addChild (map [x] [y]); // plaats de locatie van de tegel op de juiste manier [x] [y] .setLoc (x, y); // maak een verticale gang om de privéfunctie van de kamers v te verbinden vCorridor (y1: Int, y2: Int, x) for (y in Std.int (Math.min (y1, y2)) ... Std.int (Math.max (y1, y2)) + 1) // vernietig de tegels om de corridorkaart [x] [y] "uit te snijden" (kaart [x] [y]); // plaats een nieuwe niet-geblokkeerde tegelkaart [x] [y] = nieuwe tegel (Tile.DARK_GROUND, false, false); // voeg tegel toe als een nieuw spelobject addChild (map [x] [y]); // plaats de locatie van de tegel op de juiste manier [x] [y] .setLoc (x, y);
Deze functies werken op vrijwel dezelfde manier, maar de ene wordt horizontaal uitgesneden en de andere verticaal.
Het verbinden van de eerste kamer met de tweede kamer vereist eenvCorridor
en een hCorridor
. We hebben hiervoor drie waarden nodig. Voor horizontale corridors hebben we de beginwaarde x, de x-waarde voor einde en de huidige y-waarde nodig. Voor verticale gangen hebben we de begin- en eind-y-waarden nodig, samen met de huidige x-waarde.
Omdat we van links naar rechts bewegen, hebben we de twee bijbehorende x-waarden nodig, maar slechts één y-waarde, omdat we niet omhoog of omlaag gaan. Als we verticaal bewegen, hebben we de y-waarden nodig. In de voor
lus aan het begin van elke functie, herhalen we van de beginwaarde (x of y) tot de eindwaarde totdat we de hele gang hebben uitgesneden.
Nu we de code voor de gang hebben, kunnen we onze code wijzigen placeRooms ()
functie en bel onze nieuwe gangfuncties:
private function placeRooms () // opslagruimten in een array voor eenvoudig toegankelijke ruimten = nieuwe Array (); // variabele voor volgcentrum van elke kamer var newCenter = null; // randomiseer waarden voor elke kamer voor (r in 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; // maak een kamer met willekeurige waarden var newRoom = new Room (x, y, w, h); var failed = false; for (otherRoom in rooms) if (newRoom.intersects (otherRoom)) failed = true; breken; if (! failed) // lokale functie om nieuwe room te creëren createRoom (newRoom); // winkelcentrum voor nieuwe kamer newCenter = newRoom.center; if (rooms.length! = 0) // store center van vorige room var prevCenter = rooms [rooms.length - 1] .center; // carve out corridors tussen kamers op basis van centers // start willekeurig met horizontale of verticale corridors if (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)); else 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)); als (! failed) rooms.push (newRoom);
In de bovenstaande afbeelding kunt u de gangcreatie volgen van de eerste naar de vierde kamer: rood, groen en vervolgens blauw. Afhankelijk van de plaatsing van de kamers kun je een aantal interessante resultaten krijgen. Zo maken twee gangen naast elkaar een dubbele brede gang.
We voegden een aantal variabelen toe om het centrum van elke kamer te volgen en we verbonden de kamers met gangen tussen hun centra. Nu zijn er meerdere niet-overlappende kamers en gangen die ervoor zorgen dat het hele kerkerniveau verbonden blijft. Niet slecht.
Je hebt een lange weg afgelegd met het bouwen van je eerste, procedureel gegenereerde kerker-niveau, en ik hoop dat je je hebt gerealiseerd dat PCG geen magisch beest is dat je nooit een kans zult hebben om te doden.
We hebben erover nagedacht hoe je willekeurig inhoud rond je kerker-niveau kunt plaatsen met eenvoudige willekeurige-nummergeneratoren en een paar vooraf bepaalde bereiken om je inhoud op de juiste maat te houden en ongeveer op de juiste plaats. Vervolgens ontdekten we een heel eenvoudige manier om te bepalen of uw willekeurige plaatsing zinvol was door te controleren op overlappende kamers. Ten slotte hebben we het een beetje gehad over de voordelen van het bereikbaar houden van uw inhoud en hebben we een manier gevonden om ervoor te zorgen dat uw speler elke kamer in uw kerker kan bereiken.
De eerste drie stappen van ons vierstapsproces zijn voltooid, wat betekent dat je de bouwstenen van een geweldige kerker hebt voor je volgende spel. De laatste stap ligt aan jou: je moet herhalen wat je hebt geleerd om meer procedureel gegenereerde inhoud te maken voor eindeloze herspeelbaarheid.
De methode voor het uithouwen van eenvoudige kerker niveaus in deze tutorial krast alleen het oppervlak van PCG en er zijn enkele andere eenvoudige algoritmen die je gemakkelijk kunt ophalen.
Mijn uitdaging voor jou is om te beginnen met het experimenteren met het begin van je spel dat je hier hebt gemaakt en om wat onderzoek te doen naar meer methoden om je kerkers te veranderen..
Een geweldige methode voor het maken van grotteniveaus is het gebruik van cellulaire automaten, die oneindige mogelijkheden heeft om kerkerlevels aan te passen. Een andere geweldige methode om te leren is Binary Space Partitioning (BSP), dat een aantal slecht lijkende rasterachtige kerkerlevels creëert.
Ik hoop dat dit je een goede start gaf met het genereren van procedurele content. Zorg ervoor dat je hieronder commentaar geeft bij vragen die je hebt, en ik zou graag enkele voorbeelden zien van wat je met PCG aan het maken bent.
gerelateerde berichten