Procedurele generatie voor eenvoudige puzzels

Wat je gaat creëren

Puzzels vormen een integraal onderdeel van het spel voor vele genres. Of het nu eenvoudig of ingewikkeld is, het handmatig ontwikkelen van puzzels kan snel omslachtig worden. Deze tutorial heeft als doel om die last te verlichten en de weg vrij te maken voor andere, leukere aspecten van design.

Samen gaan we een generator maken voor het samenstellen van eenvoudige procedurele "geneste" puzzels. Het type puzzel waar we ons op zullen richten is het traditionele "slot en sleutel" dat het vaakst wordt herhaald: ontvang x item om het y-gebied te ontgrendelen. Dit soort puzzels kan vervelend worden voor teams die aan bepaalde soorten games werken, met name kerkerscrawlers, zandbakken en rollenspellen waarbij vaker wordt vertrouwd op puzzels voor inhoud en verkenning.

Door proceduregeneratie te gebruiken, is ons doel om een ​​functie te creëren die een paar parameters in beslag neemt en een complexere asset voor ons spel retourneert. Het toepassen van deze methode levert een exponentieel rendement op de ontwikkelaarstijd op, zonder dat dit ten koste gaat van de gameplay-kwaliteit. Ontstelding van ontwikkelaars kan ook afnemen als een gelukkig neveneffect.

Wat moet ik weten?

Om mee te gaan, moet u vertrouwd zijn met een programmeertaal van uw keuze. Aangezien het merendeel van wat we bespreken alleen gegevens is en gegeneraliseerd in pseudocode, zal elke object-georiënteerde programmeertaal voldoende zijn. 

Sommige slepen-en-neerzetten-editors werken zelfs. Als u een afspeelbare demo van de hier genoemde generator wilt maken, moet u ook enige bekendheid met uw favoriete spelbibliotheek hebben.

De generator maken

Laten we beginnen met een blik op een pseudocode. De meest elementaire bouwstenen van ons systeem worden sleutels en kamers. In dit systeem kan een speler de deur van een kamer niet betreden tenzij hij zijn sleutel bezit. Dit is hoe deze twee objecten eruit zouden zien als klassen:

class Key Var playerHas; Var locatie; Functie init (setLocation) Location = setLocation; PlayerHas = false;  Functie pickUp () this.playerHas = true;  class Room Var isLocked; Var assocKey; Functie init () isLocked = true; assocKey = nieuwe sleutel (dit);  Functie ontgrendelen () this.isLocked = false;  Functie canUnlock If (this.key.PlayerHas) Return true;  Anders Return false; 

Onze sleutelklasse bevat momenteel slechts twee gegevens: de locatie van de sleutel en als de speler die sleutel in zijn of haar inventaris heeft. De twee functies zijn initialisatie en pick-up. Initialisatie bepaalt de basis van een nieuwe sleutel, terwijl pickup is voor wanneer een speler met de sleutel communiceert.

Onze ruimteklasse bevat op zijn beurt ook twee variabelen: is gesloten, welke de huidige staat van het slot van de kamer bevat, en assocKey, die het Key-object bevat dat deze specifieke ruimte ontgrendelt. Het bevat een functie voor initialisatie en een voor het ontgrendelen van de deur, en een andere om te controleren of de deur op dit moment kan worden geopend.

Een enkele deur en sleutel zijn leuk, maar we kunnen het altijd spannender maken met nesten. Door deze functie toe te passen, kunnen we deuren in deuren maken terwijl we onze primaire generator zijn. Om te blijven nesten, moeten we ook wat extra variabelen aan onze deur toevoegen:

class Room Var isLocked; Var assocKey; Var ouderRoom; Var diepte; Functie init (setParentRoom, setDepth) If (setParentRoom) parentRoom = setParentRoom;  Anders parentRoom = none;  Depth = setDepth; isLocked = true; assocKey = nieuwe sleutel (dit);  Functie ontgrendelen () this.isLocked = false;  Functie canUnlock If (this.key.playerHas) Return true;  Anders Return false;  FunctieruimteGenerator (depthMax) Array-kamersToCheck; Array finishedRooms; Room initialRoom.init (geen, 0); roomsToCheck.add (initialRoom); Terwijl (roomsToCheck! = Leeg) If (currentRoom.depth == depthMax) finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  Anders Room newRoom.init (currentRoom, currentRoom.depth + 1); roomsToCheck.add (newRoom); finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom); 

Deze generatorcode doet het volgende:

  1. De parameter nemen voor onze gegenereerde puzzel (specifiek hoeveel lagen diep een genestelde ruimte zou moeten gaan).

  2. Het maken van twee arrays: één voor kamers die worden gecontroleerd op potentieel nesten en een andere voor het registreren van kamers die al zijn genest.

  3. Een eerste ruimte maken om de volledige scène in te sluiten en vervolgens toevoegen aan de array zodat we deze later kunnen controleren.

  4. De ruimte aan de voorkant van de array nemen om door de lus te gaan.

  5. De diepte van de huidige kamer controleren op basis van de maximale diepte (dit bepaalt of we een nieuwe kinderkamer maken of dat we het proces voltooien).

  6. Een nieuwe kamer maken en deze vullen met de nodige informatie uit de ouderkamer.

  7. De nieuwe ruimte toevoegen aan de roomsToCheck array en verplaats de vorige kamer naar de voltooide array.

  8. Herhaal dit proces totdat elke ruimte in de array is voltooid.

Nu kunnen we zoveel kamers hebben als onze machine aankan, maar we hebben nog steeds sleutels nodig. Sleutelplaatsing heeft één grote uitdaging: solvabiliteit. Waar we de sleutel ook plaatsen, we moeten ervoor zorgen dat een speler er toegang toe heeft! Ongeacht hoe goed de cache met de verborgen sleutel lijkt, als de speler deze niet kan bereiken, zit hij of zij effectief vast. Om de speler door te laten met de puzzel, moeten de sleutels verkrijgbaar zijn.

De eenvoudigste methode om de solvabiliteit in onze puzzel te verzekeren, is het gebruik van het hiërarchische systeem van relaties tussen bovenliggende en onderliggende objecten. Omdat elke kamer zich in een andere bevindt, verwachten we dat een speler toegang moet hebben tot de ouder van elke kamer om deze te bereiken. Dus zolang de sleutel zich boven de ruimte in de hiërarchische keten bevindt, garanderen we dat onze speler toegang kan krijgen.

Om het genereren van sleutels toe te voegen aan onze procedurele generatie, zullen we de volgende code in onze hoofdfunctie plaatsen:

 FunctiekamerGenerator (depthMax) Array-kamersToCheck; Array finishedRooms; Room initialRoom.init (geen, 0); roomsToCheck.add (initialRoom); Terwijl (roomsToCheck! = Leeg) If (currentRoom.depth == depthMax) finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  Anders Room newRoom.init (currentRoom, currentRoom.depth + 1); roomsToCheck.add (newRoom); finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom); Array allParentRooms; roomCheck = newRoom; Terwijl (roomCheck.parent) allParentRooms.add (roomCheck.parent); roomCheck = roomCheck.parent;  Key newKey.init (Random (allParentRooms)); newRoom.Key = newKey; Return finishedRooms;  Anders finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  

Deze extra code produceert nu een lijst met alle kamers boven uw huidige kamer in de kaartenhiërarchie. Vervolgens kiezen we willekeurig een van die punten en stellen we de locatie van de sleutel in die ruimte in. Daarna kennen we de sleutel toe aan de ruimte die wordt ontgrendeld.

Wanneer deze wordt opgeroepen, zal onze generatorfunctie nu een bepaald aantal kamers met sleutels creëren en retourneren, waardoor mogelijk uren ontwikkeltijd worden bespaard!

Dat pakt het pseudocodegedeelte van onze eenvoudige puzzelgenerator, dus laten we het nu in actie brengen.

Procedurele puzzelgeneratiedemo

We hebben onze demo gebouwd met behulp van JavaScript en de Crafty.js-bibliotheek om het zo licht mogelijk te houden, waardoor we ons programma onder de 150 regels code kunnen houden. Er zijn drie hoofdcomponenten van onze demo, zoals hieronder uiteengezet:

  1. De speler kan door elk niveau bewegen, sleutels oppakken en deuren ontgrendelen.

  2. De generator die we gebruiken om automatisch een nieuwe kaart te maken telkens wanneer de demo wordt uitgevoerd.

  3. Een uitbreiding voor onze generator om te integreren met Crafty.js, waarmee we informatie over objecten, botsingen en entiteiten kunnen opslaan.

De pseudocode hierboven fungeert als een hulpmiddel voor uitleg, dus het implementeren van het systeem in uw eigen programmeertaal vereist enige aanpassing.

Voor onze demo is een deel van de klassen vereenvoudigd voor efficiënter gebruik in JavaScript. Dit omvat het laten vallen van bepaalde functies gerelateerd aan de klassen, omdat JavaScript gemakkelijker toegang geeft tot variabelen binnen klassen.

Om het gamedeel van onze demo te creëren, initialiseren we Crafty.js en vervolgens een spelersentiteit. Vervolgens geven we onze spelerentiteit de basisrichtlijnen voor vier richtingen en een kleine botsingsdetectie om te voorkomen dat er afgesloten kamers worden betreden.

Ruimtes krijgen nu een Crafty-entiteit met informatie over hun grootte, locatie en kleur voor visuele weergave. We voegen ook een tekenfunctie toe om een ​​ruimte te maken en naar het scherm te tekenen.

We leveren sleutels met vergelijkbare toevoegingen, waaronder opslag van de Crafty-entiteit, grootte, locatie en kleur. De toetsen hebben ook een kleurcode die overeenkomt met de kamers die ze ontgrendelen. Ten slotte kunnen we nu de sleutels plaatsen en hun entiteiten creëren met behulp van een nieuwe tekenfunctie.

En last but not least ontwikkelen we een kleine helperfunctie die een willekeurige hexadecimale kleurwaarde maakt en retourneert om de last van het kiezen van kleuren te verwijderen. Tenzij je graag swatching kleuren, natuurlijk.

Wat doe ik daarna?

Nu u uw eigen eenvoudige generator hebt, volgen hier enkele ideeën om onze voorbeelden uit te breiden:

  1. Poort de generator om gebruik in uw programmeertaal naar keuze mogelijk te maken.

  2. Breid de generator uit met het creëren van vertakkingsruimten voor verdere aanpassingen.

  3. Voeg de mogelijkheid toe om ingangen van meerdere kamers naar onze generator te verwerken om meer complexe puzzels toe te staan.

  4. Breid de generator uit voor sleutelplaatsing op meer gecompliceerde locaties om het oplossen van spelersproblemen te verbeteren. Dit is vooral interessant wanneer het wordt gecombineerd met meerdere paden voor spelers.

Afsluiten

Nu we deze puzzelgenerator samen hebben gemaakt, gebruikt u de getoonde concepten om uw eigen ontwikkelingscyclus te vereenvoudigen. Welke repetitieve taken merk je dat je doet? Wat je het meest stoort aan het maken van je spel? 

De kans is groot dat u, met een beetje planning en procedurele ontwikkeling, het proces aanzienlijk eenvoudiger kunt maken. Hopelijk staat onze generator je toe je te concentreren op de aantrekkelijkere delen van het maken van games terwijl je het alledaagse uitsnijdt.

Veel geluk, en ik zie je in de comments!