Hoe je je eerste Roguelike kunt maken

Roguelikes staan ​​sinds kort in de schijnwerpers, met games als Dungeons of Dredmor, Spelunky, The Binding of Isaac en FTL die een breed publiek bereiken en lovende kritieken ontvangen. Lang genoten door hardcore spelers in een kleine nis, roguelike elementen in verschillende combinaties dragen nu bij aan meer diepte en herspeelbaarheid naar vele bestaande genres.


Wayfarer, een 3D roguelike die momenteel in ontwikkeling is.

In deze tutorial leer je hoe je een traditionele roguelike maakt met JavaScript en de HTML 5-game-engine Phaser. Tegen het einde, heb je een volledig functioneel eenvoudig roguelike spel, speelbaar in je browser! (Voor onze doeleinden wordt een traditionele roguelike gedefinieerd als een gerandomiseerde turn-based kerker-crawler voor één speler met permadeath.)


Klik om het spel te spelen. gerelateerde berichten
  • Hoe de Phaser HTML5 Game Engine te leren

Opmerking: hoewel de code in deze zelfstudie JavaScript, HTML en Phaser gebruikt, moet u dezelfde techniek en concepten in bijna elke andere codeertaal en game-engine kunnen gebruiken.


Klaar maken

Voor deze zelfstudie hebt u een teksteditor en een browser nodig. Ik gebruik Notepad ++ en ik geef de voorkeur aan Google Chrome voor zijn uitgebreide ontwikkelaarstools, maar de workflow zal vrijwel hetzelfde zijn met elke teksteditor en browser die je kiest.

Download de bronbestanden en begin met de in het map; dit bevat Phaser en de elementaire HTML- en JS-bestanden voor onze game. We zullen onze spelcode in de momenteel lege schrijven rl.js het dossier.

De index.html bestand laadt gewoon Phaser en ons eerder genoemde spelcodebestand:

  roguelike zelfstudie    

Initialisatie en definities

Voorlopig zullen we ASCII-graphics gebruiken voor onze roguelike. In de toekomst kunnen we deze vervangen door bitmapafbeeldingen, maar voor nu maakt het gebruik van eenvoudige ASCII ons leven gemakkelijker.

Laten we enkele constanten definiëren voor de lettergrootte, de afmetingen van onze kaart (dat wil zeggen, het niveau), en hoeveel actoren er in komen spawnen:

 // tekengrootte var FONT = 32; // kaartafmetingen var ROWS = 10; var COLS = 15; // aantal acteurs per niveau, inclusief speler var ACTORS = 10;

Laten we Phaser ook initialiseren en luisteren naar key-upgebeurtenissen op het toetsenbord, omdat we een turn-based game zullen maken en één keer willen handelen voor elke toetsaanslag:

// initialiseer phaser, call create () once done var game = new Phaser.Game (COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, create: create); function create () // init-toetsenbordopdrachten game.input.keyboard.addCallbacks (null, null, onKeyUp);  function onKeyUp (event) switch (event.keyCode) case Keyboard.LEFT: case Keyboard.RIGHT: case Keyboard.UP: case Keyboard.DOWN:

Omdat standaard monospace-lettertypen ongeveer 60% zo breed zijn als ze hoog zijn, hebben we de canvasgrootte geïnitialiseerd 0.6 * de lettergrootte * het aantal kolommen. We vertellen Phaser ook dat het ons moet bellen create () functie onmiddellijk nadat het klaar is met initialiseren, waarna we de toetsenbordknoppen initialiseren.

Je kunt de game hier tot nu toe bekijken, niet dat er veel te zien is!


De kaart

De tegelkaart vertegenwoordigt ons speelgebied: een discrete (in tegenstelling tot continue) 2D-array van tegels of cellen, elk vertegenwoordigd door een ASCII-teken dat een muur kan betekenen (#: blokkeert beweging) of vloer (.: blokkeert beweging niet):

 // de structuur van de map var map;

Laten we de eenvoudigste vorm van proceduregeneratie gebruiken om onze kaarten te maken: willekeurig bepalen welke cel een muur moet bevatten en met welke verdieping:

function initMap () // maak een nieuwe willekeurige kaart aan = []; voor (var y = 0; y < ROWS; y++)  var newRow = []; for (var x = 0; x < COLS; x++)  if (Math.random() > 0.8) newRow.push ('#'); else newRow.push ('.');  map.push (nieuw); 
gerelateerde berichten
  • Hoe BSP-bomen te gebruiken om gamekaarten te genereren
  • Genereer Random Cave Levels met behulp van Cellular Automata

Dit zou ons een kaart moeten geven waar 80% van de cellen wanden zijn en de rest vloeren.

We initialiseren de nieuwe kaart voor onze game in de create () functie, onmiddellijk na het instellen van de toetsenbordgebeurtenislisteners:

function create () // init-toetsenbordopdrachten game.input.keyboard.addCallbacks (null, null, onKeyUp); // initialiseer kaart initMap (); 

Je kunt de demo hier bekijken, hoewel er ook niets te zien is, omdat we de kaart nog niet hebben gerenderd.


Het scherm

Het is tijd om onze kaart te tekenen! Ons scherm is een 2D-array van tekstelementen, elk met een enkel teken:

 // de ascii-weergave, als een 2d reeks tekens var asciidisplay;

Door de kaart te tekenen, vult u de inhoud van het scherm in met de waarden van de kaart, omdat beide eenvoudige ASCII-tekens zijn:

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

Eindelijk, voordat we de kaart tekenen, moeten we het scherm initialiseren. We gaan terug naar onze create () functie:

 function create () // init-toetsenbordopdrachten game.input.keyboard.addCallbacks (null, null, onKeyUp); // initialiseer kaart initMap (); // initialiseer het scherm asciidisplay = []; voor (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); 

U zou nu een willekeurige kaart moeten zien verschijnen wanneer u het project uitvoert.


Klik om het spel tot nu toe te bekijken.

Acteurs

Volgende in de rij staan ​​de acteurs: ons spelerpersonage en de vijanden die ze moeten verslaan. Elke acteur zal een object zijn met drie velden: X en Y voor de locatie op de kaart, en pK voor zijn hitpoints.

We houden alle acteurs in de actorList array (het eerste element hiervan is de speler). We houden ook een associatieve array met de locaties van de acteurs als sleutels voor snel zoeken, zodat we niet de hele acteurslijst hoeven te herhalen om te zien welke acteur een bepaalde locatie bezet; dit zal ons helpen wanneer we de beweging en de strijd coderen.

// een lijst met alle actoren; 0 is de speler var speler; var actorList; var livingEnemies; // wijst naar elke actor in zijn positie, voor snel zoeken var actorMap;

We creëren al onze acteurs en kennen een willekeurige vrije positie toe aan elke kaart:

functie randomInt (max) return Math.floor (Math.random () * max);  function initActors () // maak acteurs op willekeurige locaties actorList = []; actorMap = ; voor (var e = 0; e 

Het is tijd om de acteurs te laten zien! We gaan alle vijanden tekenen als e en het karakter van de speler als zijn aantal hitpoints:

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

We maken gebruik van de functies die we zojuist hebben beschreven om alle actoren in ons te initialiseren en te tekenen create () functie:

functie create () ... // initialiseer actoren initActors (); ... drawActors (); 

We kunnen nu ons spelerpersonage en vijanden in het level verspreiden!


Klik om het spel tot nu toe te bekijken.

Blokkerende en beloopbare tegels

We moeten ervoor zorgen dat onze acteurs niet van het scherm en door muren lopen, dus laten we deze eenvoudige controle toevoegen om te zien in welke richting een bepaalde acteur kan lopen:

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

Beweging en bestrijding

We zijn eindelijk tot een bepaalde interactie gekomen: beweging en gevechten! Omdat bij klassieke roguelikes de basisaanval wordt geactiveerd door naar een andere acteur te gaan, behandelen we beide op dezelfde plek, onze moveTo () functie, die een actor en een richting vereist (de richting is het gewenste verschil in X en Y naar de positie van de acteur):

function moveTo (actor, dir) // controleer of acteur in de gegeven richting kan bewegen als (! canGo (actor, dir)) false retourneert; // verplaatst de acteur naar de nieuwe locatie var newKey = (actor.y + dir.y) + '_' + (actor.x + dir.x); // als de bestemmingsegel een acteur bevat als (actorMap [newKey]! = null) // de hitpoints van de acteur op de bestemmingspan verlagen; var victim = actorMap [newKey]; victim.hp--; // als het dood is, verwijder de referentie als (victim.hp == 0) actorMap [newKey] = null; actorList [actorList.indexOf (slachtoffer)] = null; if (victim! = player) livingEnemies--; if (livingEnemies == 0) // overwinningsbericht var victory = game.add.text (game.world.centerX, game.world.centerY, 'Victory! \ nCtrl + r to restart', fill: '# 2e2 ', align: "center"); victory.anchor.setTo (0.5,0.5);  else // verwijder verwijzing naar de oude positie van de acteur actorMap [actor.y + '_' + actor.x] = null; // update positie actor.y + = dir.y; actor.x + = dir.x; // voeg referentie toe aan de nieuwe positie actorMap van de acteur [actor.y + '_' + actor.x] = actor;  return true; 

Eigenlijk:

  1. We zorgen ervoor dat de acteur probeert een geldige positie in te nemen.
  2. Als er een andere acteur in die positie is, vallen we hem aan (en dood hem als zijn HP-aantal 0 bereikt).
  3. Als er geen andere acteur in de nieuwe positie is, gaan we daarheen.

Merk op dat we ook een eenvoudig overwinningsbericht laten zien zodra de laatste vijand is gedood en terugkeren vals of waar afhankelijk van het feit of we erin geslaagd zijn om een ​​geldige zet uit te voeren.

Laten we nu teruggaan naar onze onkeyup () functie en verander het zodat elke keer dat de gebruiker op een toets drukt, we de positie van de vorige acteur wissen van het scherm (door de kaart bovenaan te tekenen), het personage van de speler naar de nieuwe locatie verplaatsen en de acteurs opnieuw tekenen:

function onKeyUp (event) // trek kaart om vorige acteursposities te overschrijven drawMap (); // act op spelerinvoer var acted = false; switch (event.keyCode) case Phaser.Keyboard.LEFT: acted = moveTo (player, x: -1, y: 0); breken; case Phaser.Keyboard.RIGHT: acted = moveTo (speler, x: 1, y: 0); breken; case Phaser.Keyboard.UP: acted = moveTo (player, x: 0, y: -1); breken; case Phaser.Keyboard.DOWN: acted = moveTo (speler, x: 0, y: 1); breken;  // teken acteurs in nieuwe posities drawActors (); 

We zullen binnenkort de gehandeld variabele om te weten of de vijanden na elke invoer van een speler moeten handelen.


Klik om het spel tot nu toe te bekijken.

Basis kunstmatige intelligentie

Nu het karakter van onze speler beweegt en aanvalt, laten we zelfs de kansen verkleinen door de vijanden te laten handelen volgens een zeer eenvoudige padvinden, zolang de speler maar zes stappen of minder daarvan verwijderd is. (Als de speler verder weg is, loopt de vijand willekeurig.)

Merk op dat onze aanvalscode er niet om geeft wie de acteur aanvalt; dit betekent dat, als je ze precies goed uitlijnt, de vijanden elkaar zullen aanvallen terwijl ze het personage van de speler proberen te achtervolgen, Doom-stijl!

function aiAct (actor) var directions = [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; // als speler ver weg is, loop dan willekeurig (Math.abs (dx) + Math.abs (dy)> 6) // probeer in willekeurige richtingen te lopen totdat je eenmaal slaagt (! moveTo (actor, richtingen [randomInt (directions.length)])) ; // anders loop je naar player if (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);  

We hebben ook een game over bericht toegevoegd, dat wordt getoond als een van de vijanden de speler doodt.

Nu hoef je alleen nog maar te zorgen dat de vijand elke keer dat de speler beweegt handelt, wat betekent dat het volgende moet worden toegevoegd aan het einde van onze onkeyup () functies, vlak voordat de acteurs in hun nieuwe positie worden getekend:

function onKeyUp (event) ... // vijanden handelen elke keer als de speler doet (doet) voor (var vijand in actorList) // sla de speler over als (vijand == 0) doorgaan; var e = actorLijst [vijand]; if (e! = null) aiAct (e);  // teken acteurs in nieuwe posities drawActors (); 

Klik om het spel tot nu toe te bekijken.

Bonus: Haxe-versie

Ik heb deze zelfstudie oorspronkelijk geschreven in een Haxe, een geweldige multi-platformtaal die compileert met JavaScript (onder andere talen). Hoewel ik de bovenstaande versie handmatig heb vertaald om ervoor te zorgen dat we idiosyncratisch JavaScript krijgen, als, net als ik, je Haxe de voorkeur geeft aan JavaScript, kun je de Haxe-versie vinden in de haxe map van de brondownload.

Je moet eerst de haxe-compiler installeren en elke gewenste teksteditor gebruiken en de haxe-code compileren door te bellen haxe build.hxml of dubbelklikken op de build.hxml het dossier. Ik heb ook een FlashDevelop-project toegevoegd als je de voorkeur geeft aan een mooie IDE voor een teksteditor en opdrachtregel; net open rl.hxproj en druk op F5 rennen.


Samenvatting

Dat is het! We hebben nu een complete eenvoudige roguelike, met willekeurige kaartgeneratie, beweging, gevechten, AI en zowel win- als verliescondities.

Hier zijn enkele ideeën voor nieuwe functies die u aan uw game kunt toevoegen:

  • meerdere niveaus
  • power-ups
  • inventaris
  • verbruiksgoederen
  • uitrusting

Genieten!