Hvordan lage din første Roguelike

Roguelikes har vært i fokus nylig, med spill som Dungeons of Dredmor, Spelunky, The Binding of Isaac, og FTL når bred publikum og mottar kritisk anerkjennelse. Langt glede av hardcore-spillere i en liten nisje, bidrar roguelike-elementer i ulike kombinasjoner nå til å gi mer dybde og replayability til mange eksisterende sjangere.


Wayfarer, en 3D-roguelike som for tiden er i utvikling.

I denne opplæringen lærer du hvordan du lager en tradisjonell roguelike ved hjelp av JavaScript og HTML 5-spillmotoren Phaser. På slutten vil du ha et fullt funksjonelt, enkelt roguelike-spill som kan spilles i nettleseren din! (For vår hensikt er en tradisjonell roguelike definert som en single-player, randomisert, turbasert dungeon-crawler med permadeath.)


Klikk for å spille spillet. Relaterte innlegg
  • Slik lærer du Phaser HTML5-spillmotoren

Merk: Selv om koden i denne opplæringen bruker JavaScript, HTML og Phaser, bør du kunne bruke samme teknikk og konsepter i nesten alle andre kodende språk og spillmotor.


Gjør seg klar

For denne opplæringen trenger du en tekstredigerer og en nettleser. Jeg bruker Notepad ++, og jeg foretrekker Google Chrome for sine omfattende utviklerverktøy, men arbeidsflyten vil være stort sett den samme med hvilken som helst tekstredigerer og nettleser du velger.

Du bør da laste ned kildefilene og starte med i det mappe; Dette inneholder Phaser og de grunnleggende HTML- og JS-filene for spillet vårt. Vi vil skrive vår spillkoden i det øyeblikket som er tom rl.js fil.

De index.html filen laster bare Phaser og vår tidligere nevnte spillkodefil:

  roguelike opplæring    

Initialisering og definisjoner

For tiden bruker vi ASCII-grafikk for våre roguelike-i fremtiden, kan vi erstatte disse med bitmapgrafikk, men for øyeblikket gjør bruk av enkle ASCII våre liv lettere.

La oss definere noen konstanter for skriftstørrelsen, dimensjonene til kartet vårt (det vil si nivået), og hvor mange skuespillere henter i det:

 // fontstørrelse var FONT = 32; // kart dimensjoner var ROWS = 10; var COLS = 15; // Antall aktører per nivå, inkludert spiller var ACTORS = 10;

La oss også initialisere Phaser og lytte etter tastaturoppstartingshendelser, da vi skal skape et turbasert spill og vil ønske å handle en gang for hvert tasteslag:

// initialiser phaser, ring opprett () en gang gjort var game = ny Phaser.Game (COLS * FONT * 0,6, ROWS * FONT, Phaser.AUTO, null, create: create); funksjon lage () // init tastaturkommandoer game.input.keyboard.addCallbacks (null, null, onKeyUp);  funksjon onKeyUp (event) switch (event.keyCode) case Keyboard.LEFT: case Keyboard.RIGHT: case Keyboard.UP: case Keyboard.DOWN:

Siden standard monospace skrifttyper har en tendens til å være omtrent 60% så bred som de er høye, har vi initialisert lerretstørrelsen til å være 0,6 * skriftstørrelsen * antall kolonner. Vi forteller også Phaser at den skal ringe vår skape() Fungerer umiddelbart etter at den er ferdig initialisert, da vi initialiserer tastaturkontrollene.

Du kan se spillet så langt her - ikke at det er mye å se!


Kartet

Flisekartet representerer vårt spillområde: En diskret (i motsetning til kontinuerlig) 2D-flis av fliser eller celler, hver representert av et ASCII-tegn som kan betegne enten en vegg (#: blokkerer bevegelse) eller gulv (.: blokkerer ikke bevegelse):

 // strukturen på kartet var kart;

La oss bruke den enkleste formen for prosessorgenerasjon til å lage våre kart: Slumpvis bestemme hvilken celle som skal inneholde en vegg og hvilken etasje:

funksjon initMap () // opprett et nytt tilfeldig kartkart = []; for (var y = 0; y < ROWS; y++)  var newRow = []; for (var x = 0; x < COLS; x++)  if (Math.random() > 0,8) newRow.push ('#'); ellers newRow.push ('.');  map.push (newRow); 
Relaterte innlegg
  • Slik bruker du BSP-trær for å generere spillkort
  • Generer tilfeldige grotte nivåer ved hjelp av Cellular Automata

Dette skal gi oss et kart hvor 80% av cellene er vegger og resten er gulv.

Vi initialiserer det nye kartet for spillet vårt i skape() Funksjonen, umiddelbart etter at du har satt opp tastaturhendelse lyttere:

funksjon lage () // init tastaturkommandoer game.input.keyboard.addCallbacks (null, null, onKeyUp); // initialiser kart initMap (); 

Du kan se demoen her - selv om det igjen ikke er noe å se, ettersom vi ikke har gitt kartet ennå.


Skjermen

Det er på tide å tegne kartet vårt! Skjermen vår vil være en 2D rekke tekstelementer, som hver inneholder en enkelt karakter:

 // ascii-skjermen, som et 2d utvalg av tegn var asciidisplay;

Tegning av kartet vil fylle ut skjermens innhold med kartverdiene, siden begge er enkle ASCII-tegn:

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

Til slutt, før vi tegner kartet må vi initialisere skjermen. Vi går tilbake til vår skape() funksjon:

 funksjon lage () // init tastaturkommandoer game.input.keyboard.addCallbacks (null, null, onKeyUp); // initialiser kart initMap (); // initialiser skjerm asciidisplay = []; for (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 bør nå se et tilfeldig kart som vises når du kjører prosjektet.


Klikk for å se spillet så langt.

Skuespillere

Neste på linje er skuespillerne: vår spiller karakter, og fiender de må beseire. Hver skuespiller vil være et objekt med tre felt: x og y for sin plassering i kartet, og hp for sine treffpunkter.

Vi beholder alle skuespillere i actorList array (det første elementet er spilleren). Vi holder også en assosiativ rekkevidde med skuespillernes steder som nøkler for rask søking, slik at vi ikke trenger å iterere over hele skuespillarlisten for å finne hvilken skuespiller som har en bestemt plassering; Dette vil hjelpe oss når vi koden bevegelsen og bekjempe.

// en liste over alle aktører 0 er spilleren var spiller; var skuespillerliste; var livingEnemies; // poeng til hver skuespiller i sin posisjon, for rask søking var actorMap;

Vi lager alle våre skuespillere og tildeler en tilfeldig ledig posisjon i kartet til hver:

funksjon randomInt (maks) return Math.floor (Math.random () * max);  funksjon initActors () // lage skuespillere på tilfeldige steder actorList = []; actorMap = ; for (var e = 0; e 

Det er på tide å vise skuespillerne! Vi skal tegne alle fiender som e og spillerens karakter som antall treffpunkter:

funksjon 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 bruker de funksjonene vi nettopp skrev til å initialisere og tegne alle aktører i vår skape() funksjon:

funksjon opprette () ... // initiere skuespillere initActors (); ... drawActors (); 

Vi kan nå se vår spiller karakter og fiender spredt ut i nivået!


Klikk for å se spillet så langt.

Blokkering og gangbare fliser

Vi må sørge for at våre skuespillere ikke løper av skjermen og gjennom vegger, så la oss legge til denne enkle kontrollen for å se i hvilken retning en gitt skuespiller kan gå:

funksjon canGo (skuespiller, 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] == '.'; 

Bevegelse og bekjempelse

Vi har endelig kommet til noe samspill: bevegelse og kamp! Siden, i klassiske roguelikes, utløses det grunnleggende angrepet ved å flytte inn i en annen skuespiller, vi håndterer begge disse på samme sted, vår flytte til() funksjon, som tar en skuespiller og en retning (retningen er den ønskede forskjellen i x og y til stillingen skuespilleren går inn i):

funksjon flytte til (skuespiller, dir) // se om skuespiller kan bevege seg i den angitte retningen hvis (! canGo (skuespiller, dir)) returnerer falsk; // flytter skuespiller til det nye stedet var newKey = (actor.y + dir.y) + '_' + (actor.x + dir.x); // hvis destinasjonsflisen har en skuespiller i den hvis (actorMap [newKey]! = null) // decrement hitpoints av skuespilleren på destinasjonsflisen var offer = actorMap [newKey]; victim.hp--; // hvis det er død fjerner referansen hvis (victim.hp == 0) actorMap [newKey] = null; actorList [actorList.indexOf (offer)] = null; hvis (offer! = spiller) livingEnemies--; hvis (livingEnemies == 0) // seiersmelding var seier = game.add.text (game.world.centerX, game.world.centerY, 'Victory! \ nCtrl + r for å starte på nytt', fill: '# 2e2 ', juster: "senter"); victory.anchor.setTo (0.5,0.5);  annet // fjern referanse til skuespillers gamle posisjon actorMap [actor.y + '_' + actor.x] = null; // oppdateringsposisjon actor.y + = dir.y; actor.x + = dir.x; // legg til referanse til skuespillers nye posisjon actorMap [actor.y + '_' + actor.x] = actor;  returnere sann; 

I utgangspunktet:

  1. Vi sørger for at skuespilleren prøver å flytte inn i en gyldig posisjon.
  2. Hvis det er en annen skuespiller i den posisjonen, angriper vi den (og dreper den hvis HP-tallet når 0).
  3. Hvis det ikke er en annen skuespiller i den nye stillingen, beveger vi oss der.

Legg merke til at vi også viser en enkel seiersmelding når den siste fienden er blitt drept, og returnere falsk eller ekte avhengig av om vi klarte å utføre et gyldig trekk.

Nå, la oss gå tilbake til vår onKeyUp () funksjonen og endre den slik at hver gang brukeren trykker på en tast, sletter vi forrige skuespillerens posisjoner fra skjermen (ved å tegne kartet øverst), flytter spillerens tegn til det nye stedet og redragerer skuespillerne:

funksjon onKeyUp (event) // tegne kart for å overskrive tidligere skuespillere posisjoner drawMap (); // act on player input var acted = false; bytt (event.keyCode) tilfelle Phaser.Keyboard.LEFT: acted = moveTo (spiller, x: -1, y: 0); gå i stykker; tilfelle Phaser.Keyboard.RIGHT: acted = moveTo (spiller, x: 1, y: 0); gå i stykker; tilfelle Phaser.Keyboard.UP: acted = moveTo (spiller, x: 0, y: -1); gå i stykker; tilfelle Phaser.Keyboard.DOWN: acted = moveTo (spiller, x: 0, y: 1); gå i stykker;  // tegne skuespillere i nye stillinger drawActors (); 

Vi vil snart bruke handlet variabel for å vite om fiender skal opptre etter hver spillerinngang.


Klikk for å se spillet så langt.

Grunnleggende kunstig intelligens

Nå som vår spillers karakter beveger seg og angriper, la oss til og med oddsen ved å gjøre fienderne til å fungere etter en veldig enkel bane, så lenge spilleren er seks trinn eller færre fra dem. (Hvis spilleren er lengre unna, går fienden tilfeldig.)

Legg merke til at vår angrepskode ikke bryr oss om hvilken skuespilleren angriper; dette betyr at hvis du justerer dem akkurat, vil fiender angripe hverandre mens du prøver å forfølge spillerens karakter, Doom-stil!

funksjon aiAct (skuespiller) var retning = [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; // hvis spilleren er langt unna, gå tilfeldig hvis (Math.abs (dx) + Math.abs (dy)> 6) // prøv å gå i tilfeldige retninger til du lykkes en gang mens (! moveTo (skuespiller, retninger [randomInt (retning. lengde)])) ; / ellers gå mot spiller hvis (Math.abs (dx)> Math.abs (dy)) hvis (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 også lagt til et spill over meldingen, som vises hvis en av fiender dreper spilleren.

Nå er alt som er igjen å gjøre, få fiender til å handle hver gang spilleren beveger seg, noe som krever å legge til følgende til slutten av vår onKeyUp () Funksjoner, rett før tegning av skuespillerne i sin nye posisjon:

funksjon onKeyUp (event) ... // fiender opptrer hver gang spilleren gjør hvis (handlet) for (var fiende i actorList) // hoppe over spilleren hvis (fiende == 0) fortsette; var e = actorList [fiende]; hvis (e! = null) aiAct (e);  // tegne skuespillere i nye stillinger drawActors (); 

Klikk for å se spillet så langt.

Bonus: Haxe Versjon

Jeg skrev opprinnelig denne opplæringen i et Haxe, et flott multiplattformsspråk som samler til JavaScript (blant andre språk). Selv om jeg oversatte versjonen ovenfor for hånden for å sikre at vi får idiosynkratisk JavaScript, hvis du, som jeg, foretrekker Haxe til JavaScript, kan du finne Haxe-versjonen i haxe mappe av kilde nedlasting.

Du må først installere haxe-kompilatoren og kan bruke hvilken som helst tekstredigerer du ønsker og kompilere haxekoden ved å ringe haxe build.hxml eller dobbeltklikk på build.hxml fil. Jeg har også inkludert et FlashDevelop-prosjekt hvis du foretrekker en fin IDE til en tekstredigerer og kommandolinje; bare åpen rl.hxproj og trykk F5 å løpe.


Sammendrag

Det er det! Vi har nå en komplett enkel roguelike, med tilfeldig kartgenerering, bevegelse, kamp, ​​AI og både seier og tap.

Her er noen ideer for nye funksjoner du kan legge til i spillet ditt:

  • flere nivåer
  • power ups
  • inventar
  • forbruksvarer
  • utstyr

Nyt!