Hur man gör din första Roguelike

Roguelikes har nyligen varit i strålkastaren, med spel som Dungeons of Dredmor, Spelunky, The Binding of Isaac och FTL når breda publik och mottar kritiskt tilltalande. Långt njut av hardcore-spelare i en liten nisch, roguelike-element i olika kombinationer hjälper nu att ge mer djup och återspelningsförmåga till många befintliga genrer.


Wayfarer, en 3D-roguelike som för närvarande är i utveckling.

I den här handledningen lär du dig hur du gör en traditionell roguelike med hjälp av JavaScript och HTML 5-spelmotorn Phaser. I slutet kommer du att ha ett helt funktionellt enkelt roguelike spel som kan spelas i din webbläsare! (För vårt ändamål definieras en traditionell roguelike som en singelspelare, slumpmässig, turbaserad dungeon-sökrobot med permadeath.)


Klicka för att spela spelet. relaterade inlägg
  • Så här läser du Phaser HTML5-spelmotorn

Obs! Även om koden i denna handledning använder JavaScript, HTML och Phaser, borde du kunna använda samma teknik och begrepp i nästan alla andra kodningsspråk och spelmotor.


Gör mig i ordning

För denna handledning behöver du en textredigerare och en webbläsare. Jag använder Notepad ++, och jag föredrar Google Chrome för dess omfattande utvecklingsverktyg, men arbetsflödet blir ungefär lika med någon textredigerare och webbläsare du väljer.

Du ska då hämta källfilerna och börja med i det mapp; Detta innehåller Phaser och de grundläggande HTML- och JS-filerna för vårt spel. Vi skriver vår spelkod i den nuvarande tomma rl.js fil.

De index.html filen laddar helt enkelt Phaser och vår tidigare nämnda spelkodsfil:

  roguelike handledning    

Initialisering och definitioner

För närvarande använder vi ASCII-grafik för vår roguelike-i framtiden kan vi ersätta dessa med bitmapgrafik, men för närvarande använder enkla ASCII våra liv lättare.

Låt oss definiera några konstanter för teckensnittstorleken, dimensionerna på vår karta (det vill säga nivån) och hur många aktörer som hyser det:

 // fontstorlek var FONT = 32; // kartdimensioner var ROWS = 10; var COLS = 15; // Antal aktörer per nivå, inklusive spelare var ACTORS = 10;

Låt oss också initiera Phaser och lyssna på händelser för tangentbordet, eftersom vi kommer att skapa ett turnbaserat spel och kommer att vilja agera en gång för varje nyckelslag:

// initiera phaser, ring skapa () en gång gjort var game = nya Phaser.Game (COLS * FONT * 0,6, ROWS * FONT, Phaser.AUTO, null, skapa: skapa); funktion skapa () // init tangentbord kommandon game.input.keyboard.addCallbacks (null, null, onKeyUp);  funktion påKeyUp (händelse) switch (event.keyCode) case Keyboard.LEFT: case Keyboard.RIGHT: case Keyboard.UP: case Keyboard.DOWN:

Eftersom standardmonospace-teckensnitt tenderar att vara ungefär 60% så brett som de är höga, har vi initialiserat den dukstorlek som ska vara 0,6 * teckensnittsstorleken * antalet kolumner. Vi berättar också för Phaser att det borde ringa till oss skapa() funktion direkt efter det att den har slutförts initialiseringen, vid vilken tidpunkt vi initierar tangentbordskontrollerna.

Du kan se spelet hittills här, inte att det finns mycket att se!


Kartan

Kakelplattan representerar vårt spelområde: en diskret (i motsats till kontinuerlig) 2D-serie av plattor eller celler, vilka varje representeras av ett ASCII-tecken som kan innebära en vägg (#: block rörelse) eller golv (.: blockerar inte rörelse):

 // strukturen på kartan var karta

Låt oss använda den enklaste formen av procedurgenerering för att skapa våra kartor: Slumpmässigt bestämmer vilken cell som ska innehålla en vägg och vilken golv:

funktion initMap () // skapa en ny slumpmässig kartakarta = []; för (var y = 0; y < ROWS; y++)  var newRow = []; for (var x = 0; x < COLS; x++)  if (Math.random() > 0,8) newRow.push ('#'); annars newRow.push ('.');  map.push (newRow); 
relaterade inlägg
  • Så här använder du BSP-träd för att skapa spelkartor
  • Generera slumpmässiga grottnivåer med hjälp av Cellular Automata

Detta borde ge oss en karta där 80% av cellerna är väggar och resten är golv.

Vi initierar den nya kartan för vårt spel i skapa() funktion, direkt efter att du ställt in tangentbordshändelserna:

funktion skapa () // init tangentbord kommandon game.input.keyboard.addCallbacks (null, null, onKeyUp); // initiera karta initMap (); 

Du kan se demoen här, men det är inget att se igen, eftersom vi inte har gjort kartan ännu.


Skärmen

Det är dags att rita vår karta! Vår skärm kommer att vara en 2D-rad textelement, som innehåller en enda karaktär:

 // ascii-displayen, som en 2d-serie av tecken var ascii-visning

Om du ritar kartan fyller du in skärmens innehåll med kartans värden, eftersom båda är enkla ASCII-tecken:

 funktion drawMap () for (var y = 0; y < ROWS; y++) for (var x = 0; x < COLS; x++) asciidisplay[y][x].content = map[y][x]; 

Slutligen, innan vi ritar kartan måste vi initiera skärmen. Vi går tillbaka till vårt skapa() fungera:

 funktion skapa () // init tangentbord kommandon game.input.keyboard.addCallbacks (null, null, onKeyUp); // initiera karta initMap (); // initiera skärmen asciidisplay = []; för (var y = 0; y < ROWS; y++)  var newRow = []; asciidisplay.push(newRow); for (var x = 0; x < COLS; x++) newRow.push( initCell(", x, y) );  drawMap();  function initCell(chr, x, y)  // add a single cell in a given position to the ascii display var style =  font: FONT + "px monospace", fill:"#fff"; return game.add.text(FONT*0.6*x, FONT*y, chr, style); 

Du ska nu se en slumpmässig karta som visas när du kör projektet.


Klicka för att se spelet hittills.

Skådespelare

Nästa i rad är skådespelarna: vår spelare karaktär och de fiender de måste besegra. Varje skådespelare kommer att vara ett objekt med tre fält: x och y för dess plats i kartan och hk för sina träffpunkter.

Vi håller alla skådespelare i actorList array (det första elementet är spelaren). Vi håller också en associativ grupp med aktörernas platser som nycklar för snabb sökning, så att vi inte behöver iterera över hela skådespelarlistan för att hitta vilken skådespelare som upptar en viss plats. Det hjälper oss när vi kodar rörelsen och striden.

// en lista över alla aktörer 0 är spelaren var spelare var skådespelare var livingEnemies; // pekar på varje skådespelare i sin position, för snabb sökning var actorMap;

Vi skapar alla våra aktörer och tilldelar en slumpmässig ledig position i kartan till var och en:

funktion randomInt (max) returnera Math.floor (Math.random () * max);  funktion initActors () // skapa aktörer på slumpmässiga platser actorList = []; actorMap = ; för (var e = 0; e 

Det är dags att visa skådespelarna! Vi kommer att rita alla fiender som e och spelarens karaktär som antal träffpunkter:

funktion drawActors () for (var en i actorList) if (actorList [a] .hp> 0) asciidisplay [actorList [a] .y] [actorList [a] .x] .content = a == 0? " + player.hp: 'e';

Vi använder de funktioner som vi just skrev för att initiera och rita alla aktörer i vår skapa() fungera:

funktion skapa () ... // initiera aktörer initActors (); ... drawActors (); 

Vi kan nu se vår spelares karaktär och fiender sprids ut i nivån!


Klicka för att se spelet hittills.

Blockering och gångbara plattor

Vi måste se till att våra skådespelare inte går av skärmen och genom väggar, så låt oss lägga till den här enkla kontrollen för att se i vilka riktningar en given skådespelare kan gå:

funktion canGo (skådespelare, dir) return actor.x + dir.x> = 0 && actor.x + dir.x <= COLS - 1 && actor.y+dir.y >= 0 && actor.y + dir.y <= ROWS - 1 && map[actor.y+dir.y][actor.x +dir.x] == '.'; 

Rörelse och bekämpning

Vi har äntligen kommit till en viss interaktion: rörelse och strid! Eftersom i klassiska roguelikes utlöses den grundläggande attacken genom att flytta in i en annan skådespelare, vi hanterar båda dessa på samma plats, vår flytta till() funktion, som tar en skådespelare och en riktning (riktningen är den önskade skillnaden i x och y till den position som skådespelaren går in i):

funktion moveTo (skådespelare, dir) // kolla om skådespelare kan flytta i den angivna riktningen om (! canGo (skådespelare, dir)) returnerar false; // flyttar skådespelare till den nya platsen var newKey = (actor.y + dir.y) + '_' + (actor.x + dir.x); // om destinationens kakel har en skådespelare i den om (actorMap [newKey]! = null) // decrement hitpoints av skådespelaren på destinationens kakel var offer = actorMap [newKey]; victim.hp--; // om det är död ta bort referensen om (victim.hp == 0) actorMap [newKey] = null; actorList [actorList.indexOf (offer)] = null; om (offer! = spelare) livingEnemies--; om (livingEnemies == 0) // seger meddelande var seger = game.add.text (game.world.centerX, game.world.centerY, "Victory! \ nCtrl + r för att starta om", fill: '# 2e2 ', Centrera i linje med"  ); victory.anchor.setTo (0.5,0.5);  annat // ta bort referens till skådespelarens gamla position actorMap [actor.y + '_' + actor.x] = null; // uppdateringsposition actor.y + = dir.y; actor.x + = dir.x; // lägg till hänvisning till skådespelarens nya position actorMap [actor.y + '_' + actor.x] = skådespelare;  returnera sant; 

I grund och botten:

  1. Vi ser till att skådespelaren försöker flytta till ett giltigt läge.
  2. Om det finns en annan skådespelare i den positionen attackerar vi den (och dödar den om dess HP-tal når 0).
  3. Om det inte finns någon annan skådespelare i den nya positionen flytta vi där.

Observera att vi också visar ett enkelt segermeddelande när den sista fienden har blivit dödad och återvänder falsk eller Sann beroende på om vi lyckades utföra ett giltigt flytt.

Nu, låt oss gå tillbaka till vårt onKeyUp () funktionen och ändra den så att vi varje gång användaren trycker på en tangent raderar vi den föregående skådespelarens positioner från skärmen (genom att dra kartan överst), flytta spelarens tecken till den nya platsen och redraw sedan skådespelarna:

funktion onKeyUp (händelse) // draw map för att skriva över tidigare aktörer positioner drawMap (); // act på spelarens inmatning var agerad = false; switch (event.keyCode) fallet Phaser.Keyboard.LEFT: acted = moveTo (spelare, x: -1, y: 0); ha sönder; fallet Phaser.Keyboard.RIGHT: acted = moveTo (spelare, x: 1, y: 0); ha sönder; fallet Phaser.Keyboard.UP: acted = moveTo (spelare, x: 0, y: -1); ha sönder; fallet Phaser.Keyboard.DOWN: acted = moveTo (spelare, x: 0, y: 1); ha sönder;  // teckna aktörer i nya positioner drawActors (); 

Vi kommer snart att använda agerat variabel för att veta om fienderna ska agera efter varje spelarens inmatning.


Klicka för att se spelet hittills.

Grundläggande artificiell intelligens

Nu när vår spelare karaktär rör sig och attackerar, låt oss även oddsen genom att fienderna agerar enligt en mycket enkel väg att hitta så länge spelaren är sex steg eller färre från dem. (Om spelaren är längre bort, går fienden slumpmässigt.)

Observera att vår attackkod inte bryr oss vem skådespelaren attackerar; detta innebär att om du justerar dem rätt, kommer fienderna att attackera varandra medan du försöker att driva spelarens karaktär, Doom-stil!

funktion aiAct (skådespelare) var riktningar = [x: -1, y: 0, x: 1, y: 0, x: 0, y: -1, x: 0, y: 1 ]; var dx = player.x - actor.x; var dy = player.y - actor.y; // om spelaren är långt bort, gå slumpmässigt om (Math.abs (dx) + Math.abs (dy)> 6) // försök att gå i slumpmässiga riktningar tills du lyckas en gång medan (! moveTo (skådespelare, riktningar [randomInt (riktningslängd)])) ; // annars går mot spelare om (Math.abs (dx)> Math.abs (dy)) if (dx < 0)  // left moveTo(actor, directions[0]);  else  // right moveTo(actor, directions[1]);   else  if (dy < 0)  // up moveTo(actor, directions[2]);  else  // down moveTo(actor, directions[3]);   if (player.hp < 1)  // game over message var gameOver = game.add.text(game.world.centerX, game.world.centerY, 'Game Over\nCtrl+r to restart',  fill : '#e22', align: "center"  ); gameOver.anchor.setTo(0.5,0.5);  

Vi har också lagt till ett spel över meddelande, vilket visas om en av fienderna dödar spelaren.

Nu är allt som finns kvar att göra fienderna att agera varje gång spelaren flyttar, vilket kräver att vi lägger till följande till slutet av vårt onKeyUp () funktioner, strax före teckning av aktörerna i sin nya position:

funktion påKeyUp (händelse) ... // fiender agerar varje gång spelaren gör om (agerat) för (var fiende i skådespelare) // hoppa över spelaren om (fiende == 0) fortsätt; var e = aktörslista [fiende]; om (e! = null) aiAct (e);  // teckna aktörer i nya positioner drawActors (); 

Klicka för att se spelet hittills.

Bonus: Haxe Version

Jag skrev ursprungligen denna handledning i ett Haxe, ett bra multiplattformsspråk som kompilerar till JavaScript (bland andra språk). Även om jag översatte versionen ovan för hand för att se till att vi får idiosynkratisk JavaScript, om du, som jag, föredrar Haxe till JavaScript, kan du hitta Haxe-versionen i haxe mapp på källans nedladdning.

Du måste först installera haxe-kompilatorn och kan använda vilken textredigerare du önskar och kompilera haxekoden genom att ringa haxe build.hxml eller dubbelklicka på build.hxml fil. Jag inkluderade också ett FlashDevelop-projekt om du föredrar en trevlig IDE till en textredigerare och kommandorad; bara öppen rl.hxproj och tryck på F5 att springa.


Sammanfattning

Det är allt! Vi har nu en komplett enkel roguelike, med slumpmässig kartgenerering, rörelse, strid, AI och både vinna och förlora förhållanden.

Här är några idéer för nya funktioner du kan lägga till i ditt spel:

  • flera nivåer
  • power-ups
  • lager
  • förbrukningsvaror
  • Utrustning

Njut av!