Procedural Generation för enkla pussel

Vad du ska skapa

Pussel är en integrerad del av gameplayen för många genrer. Oavsett om det är enkelt eller komplicerat, kan man snabbt utveckla pussel manuellt snabbt. Denna handledning syftar till att lätta den bördan och bana väg för andra, roligare, aspekter av design.

Tillsammans kommer vi att skapa en generator för att komponera enkla proceduriska "kapslade" pussel. Den typ av pussel som vi kommer att fokusera på är den traditionella "lås och nyckeln" som oftast itereras som: få x objekt att låsa upp y-området. Dessa typer av pussel kan bli tråkiga för lag som arbetar med vissa typer av spel, särskilt dungeon crawlers, sandlådor och rollspel där pussel ofta är beroende av innehåll och prospektering.

Genom att använda processgenerering är vårt mål att skapa en funktion som tar några parametrar och returnerar en mer komplex tillgång till vårt spel. Att tillämpa denna metod ger en exponentiell avkastning på utvecklar tiden utan att åsidosätta spelkvaliteten. Utvecklaren bedövning kan också minska som en lycklig bieffekt.

Vad behöver jag veta?

För att följa med måste du känna till ett programmeringsspråk som du väljer. Eftersom det mesta av det vi diskuterar endast är data och generaliseras i pseudokod, är det något objektorienterat programmeringsspråk som räcker till. 

Faktum är att vissa drag-och-släppredigerare också kommer att fungera. Om du vill skapa en spelbar demo av generatorn som nämns här, behöver du också en del förtrogenhet med ditt föredragna spelbibliotek.

Skapa generatorn

Låt oss börja med en titt på någon pseudokod. De mest grundläggande byggstenarna i vårt system kommer att vara nycklar och rum. I det här systemet är en spelare spärrad från att komma in i ett rums dörr om de inte har sin nyckel. Här är vad dessa två objekt skulle se ut som klasser:

klass nyckel Var playerHas; Var plats; Funktion init (setLocation) Plats = setLocation; PlayerHas = false;  Function pickUp () this.playerHas = true;  klassrum Var isLocked; Var assocKey; Funktion init () isLocked = true; assocKey = ny nyckel (detta);  Funktionslåsning () this.isLocked = false;  Funktion canUnlock If (this.key.PlayerHas) Return true;  Annars Return false; 

Vår nyckelklass innehåller bara två delar av informationen just nu: nyckelens läge, och om spelaren har den nyckeln i hans eller hennes inventering. De två funktionerna är initialisering och upptagning. Initialisering bestämmer grunderna för en ny nyckel, medan pickup är för när en spelare interagerar med nyckeln.

Vår rumsklass innehåller i sin tur två variabler: är låst, som håller rummets nuvarande tillstånd, och assocKey, som håller nyckelobjektet som låser upp det här specifika rummet. Den innehåller också en funktion för initialisering, en att ringa för att låsa upp dörren och en annan för att kontrollera om dörren för närvarande kan öppnas.

En enda dörr och nyckel är roligt, men vi kan alltid krydda det med att bo. Genom att implementera den här funktionen kan vi skapa dörrar inom dörrarna medan de fungerar som vår primära generator. För att bibehålla nestning måste vi lägga till några ytterligare variabler till vår dörr också:

Klassrummet Var isLocked; Var assocKey; Var parentRoom; Var djup; Funktion init (setParentRoom, setDepth) Om (setParentRoom) parentRoom = setParentRoom;  Annars parentRoom = none;  Djup = setDepth; isLocked = true; assocKey = ny nyckel (detta);  Funktionslåsning () this.isLocked = false;  Funktion canUnlock If (this.key.playerHas) Return true;  Annars Return false;  FunktionsrumGenerator (depthMax) Array roomsToCheck; Array finishedRooms; Rum initialRoom.init (ingen, 0); roomsToCheck.add (initialRoom); Medan (roomsToCheck! = Tom) Om (currentRoom.depth == deepMax) finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  Annars Room newRoom.init (currentRoom, currentRoom.depth + 1); roomsToCheck.add (newRoom); finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom); 

Denna generatorkod gör följande:

  1. Tar in parametern för vårt genererade pussel (specifikt hur många lager djupt ett kapslat rum ska gå).

  2. Skapa två arrays: en för rum som kontrolleras för potentiell nestning, och en annan för att registrera rum som redan är kapslade.

  3. Skapa ett första rum för att innehålla hela scenen och lägg till den i matrisen så att vi kan kolla senare.

  4. Tar rummet på framsidan av matrisen för att sätta igång slingan.

  5. Kontrollera djupet på det aktuella rummet mot det maximala djupet som ges (det bestämmer om vi skapar ett ytterligare barnrum eller om vi slutför processen).

  6. Etablera ett nytt rum och fylla det med nödvändig information från föräldrarnas rum.

  7. Lägga till det nya rummet till roomsToCheck array och flytta det föregående rummet till den färdiga matrisen.

  8. Upprepa denna process tills varje rum i matrisen är komplett.

Nu kan vi få så många rum som vår maskin kan hantera, men vi behöver fortfarande nycklar. Nyckelplacering har en stor utmaning: lönsamhet. Varhelst vi placerar nyckeln måste vi se till att en spelare får tillgång till det! Oavsett hur utmärkt den dolda nyckelcachen verkar, om spelaren inte kan nå den, är han eller hon effektivt infångad. För att spelaren ska kunna fortsätta genom pusset måste nycklarna vara tillgängliga.

Den enklaste metoden för att säkerställa lönsamhet i vårt pussel är att använda det hierarkiska systemet för relationer mellan moder och barn. Eftersom varje rum ligger inom en annan, förväntar vi oss att en spelare måste ha tillgång till föräldrarna till varje rum för att nå det. Så länge som nyckeln ligger ovanför rummet på den hierarkiska kedjan, garanterar vi att vår spelare kan få tillgång.

För att lägga till nyckelgenerering till vår processgenerering lägger vi följande kod i vår huvudfunktion:

 Funktion roomGenerator (depthMax) Array roomsToCheck; Array finishedRooms; Rum initialRoom.init (ingen, 0); roomsToCheck.add (initialRoom); Medan (roomsToCheck! = Tom) Om (currentRoom.depth == deepMax) finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  Annars Room newRoom.init (currentRoom, currentRoom.depth + 1); roomsToCheck.add (newRoom); finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom); Array allParentRooms; roomCheck = newRoom; Medan (roomCheck.parent) allParentRooms.add (roomCheck.parent); roomCheck = roomCheck.parent;  Key newKey.init (Slumpmässig (allParentRooms)); newRoom.Key = newKey; Returnera färdiga rum  Annars finishedRooms.add (currentRoom); roomsToCheck.remove (currentRoom);  

Denna extra kod kommer nu att skapa en lista över alla rum som ligger över ditt nuvarande rum i karthierarkin. Då väljer vi en av dem slumpmässigt och ställer in nyckelens plats till det rummet. Därefter tilldelar vi nyckeln till rummet det låser upp.

När den heter kommer vår generatorfunktion nu skapa och returnera ett visst antal rum med nycklar, vilket potentiellt sparar timmar av utvecklingstid!

Det sveper upp pseudokoddelen av vår enkla pusselgenerator, så nu låt oss sätta den i aktion.

Procedurell pusselgenerationsdemo

Vi byggde vår demo med hjälp av JavaScript och Crafty.js-biblioteket för att hålla det så lätt som möjligt, så att vi kan hålla vårt program under 150 rader kod. Det finns tre huvudkomponenter i vår demo som anges nedan:

  1. Spelaren kan röra sig över varje nivå, hämtningsnycklar och låsa upp dörrar.

  2. Generatorn som vi kommer att använda för att automatiskt skapa en ny karta varje gång demoen körs.

  3. En förlängning för vår generator att integrera med Crafty.js, som tillåter oss att lagra objekt, kollision och enhetinformation.

Pseudokoden ovan fungerar som ett verktyg för förklaring, så att implementera systemet på ditt eget programmeringsspråk kommer att kräva viss modifiering.

För vår demo förenklas en del av klasserna för effektivare användning i JavaScript. Detta inkluderar att släppa vissa funktioner relaterade till klasserna, eftersom JavaScript tillåter enklare åtkomst till variabler inom klasserna.

För att skapa speldelen av vår demo, initierar vi Crafty.js och sedan en spelarenhet. Därefter ger vi vår spelare entitet de grundläggande fyra riktningskontrollerna och lite mindre kollisionsdetektering för att förhindra att de kommer in i låsta rum.

Rummen ges nu en Crafty enhet, lagra information om deras storlek, plats och färg för visuell representation. Vi lägger också till en teckningsfunktion så att vi kan skapa ett rum och dra det till skärmen.

Vi kommer att tillhandahålla nycklar med liknande tillägg, inklusive lagring av sin Crafty-enhet, storlek, plats och färg. Nycklarna kommer också att färgkodas för att matcha de rum de låser upp. Slutligen kan vi nu placera nycklarna och skapa sina enheter med hjälp av en ny teckningsfunktion.

Sist men inte minst utvecklar vi en liten hjälparfunktion som skapar och returnerar ett slumpmässigt hexadecimalt färgvärde för att ta bort bördan för att välja färger. Om du inte gillar färgstarka färger, förstås.

Vad gör jag nästa?

Nu när du har din egen enkla generator, här är några idéer för att utöka våra exempel:

  1. Sätt i generatorn för att möjliggöra användning i ditt programmerade språk.

  2. Förläng generatorn så att den omfattar att skapa förgreningsrum för ytterligare anpassning.

  3. Lägg till förmågan att hantera flera rums ingångar till vår generator för att möjliggöra mer komplexa pussel.

  4. Utvid generatorn för att möjliggöra nyckelplacering på mer komplicerade platser för att förbättra spelarens problemlösning. Detta är speciellt intressant när det är parat med flera banor för spelare.

Avslutar

Nu när vi har skapat denna pusselgenerator tillsammans, använd de begrepp som visas för att förenkla din egen utvecklingscykel. Vilka repetitiva uppgifter hittar du själv? Vad stör dig mest om att skapa ditt spel? 

Chansen är att med en liten planering och procedurgenerering kan du göra processen betydligt enklare. Förhoppningsvis kan vår generator låta dig fokusera på de mer tilltalande delarna av spelframställning medan du skär ut det vardagliga.

Lycka till, och jag ser dig i kommentarerna!