So machen Sie Ihren ersten Roguelike

In letzter Zeit standen Roguelikes im Rampenlicht, wobei Spiele wie Dungeons of Dredmor, Spelunky, The Binding of Isaac und FTL ein breites Publikum erreichten und von Kritikern begrüßt wurden. In einer winzigen Nische waren Hardcore-Spieler schon lange beliebt. Roguelike-Elemente in verschiedenen Kombinationen sorgen jetzt für mehr Tiefe und Wiederspielbarkeit in vielen vorhandenen Genres.


Wayfarer, ein 3D-Rennfahrer, der sich gerade in der Entwicklung befindet.

In diesem Lernprogramm erfahren Sie, wie Sie mit JavaScript und der HTML 5-Spielengine Phaser ein traditionelles Roguelike erstellen. Am Ende haben Sie ein voll funktionsfähiges einfaches roguelike-Spiel, das Sie in Ihrem Browser spielen können! (Für unsere Zwecke wird ein traditionelles Roguelike als ein randomisierter, rundenbasierter Dungeon-Crawler mit Permadeath definiert.)


Klicke, um das Spiel zu spielen. zusammenhängende Posts
  • So lernen Sie die Phaser HTML5 Game Engine

Hinweis: Obwohl der Code in diesem Lernprogramm JavaScript, HTML und Phaser verwendet, sollten Sie in der Lage sein, dieselbe Technik und Konzepte in fast allen anderen Programmiersprachen und Spiele-Engines zu verwenden.


Fertig werden

Für dieses Tutorial benötigen Sie einen Texteditor und einen Browser. Ich benutze Notepad ++ und bevorzuge Google Chrome für seine umfangreichen Entwickler-Tools. Der Workflow ist jedoch bei jedem Texteditor und Browser, den Sie auswählen, ziemlich gleich.

Sie sollten dann die Quelldateien herunterladen und mit der drin Mappe; Dieses enthält Phaser und die grundlegenden HTML- und JS-Dateien für unser Spiel. Wir werden unseren Spielcode in die derzeit leere Spalte schreiben rl.js Datei.

Das index.html Datei lädt einfach Phaser und unsere zuvor genannte Spielcode-Datei:

  Roguelike Tutorial    

Initialisierung und Definitionen

Momentan verwenden wir ASCII-Grafiken für unser Roguelike. In Zukunft könnten wir diese durch Bitmap-Grafiken ersetzen. Die Verwendung von einfachem ASCII macht uns das Leben jedoch leichter.

Definieren Sie einige Konstanten für die Schriftgröße, die Abmessungen unserer Karte (dh die Ebene) und die Anzahl der Darsteller, die darin erscheinen:

 // Schriftgröße var FONT = 32; // Kartenabmessungen var ROWS = 10; var COLS = 15; // Anzahl der Akteure pro Ebene, einschließlich Spielervariable ACTORS = 10;

Lassen Sie uns auch Phaser initialisieren und auf Key-Up-Events der Tastatur achten, da wir ein rundenbasiertes Spiel erstellen und für jeden Tastenanschlag einmal handeln wollen:

// Phaser initialisieren, einmal create () aufrufen var game = new Phaser.Game (COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, create: create); function create () // Init-Tastaturbefehle 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:

Da Standard-Monospace-Schriftarten in der Regel etwa 60% so breit wie hoch sind, haben wir die Leinwandgröße als initialisiert 0,6 * die Schriftgröße * die Anzahl der Spalten. Wir sagen Phaser auch, dass er unser nennen soll erstellen() Funktion unmittelbar nach der Initialisierung, woraufhin wir die Tastatursteuerungen initialisieren.

Sie können das Spiel hier so weit sehen, nicht viel zu sehen!


Die Karte

Die Kachelkarte stellt unseren Spielbereich dar: ein diskretes (im Gegensatz zu einem kontinuierlichen) 2D-Array von Kacheln oder Zellen, die jeweils durch ein ASCII-Zeichen dargestellt werden, das entweder eine Wand (#: blockiert Bewegung) oder Boden (.: blockiert nicht die Bewegung):

 // die Struktur der Karte var map;

Lassen Sie uns die einfachste Form der prozeduralen Generierung verwenden, um unsere Karten zu erstellen: Wählen Sie zufällig aus, welche Zelle eine Wand und welche Etage enthalten soll:

function initMap () // Eine neue Zufallskarte erstellen map = []; für (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 (newRow); 
zusammenhängende Posts
  • So verwenden Sie BSP-Bäume zur Erstellung von Game Maps
  • Erzeugen Sie zufällige Höhlenebenen mithilfe von Zellularautomaten

Dies sollte uns eine Karte geben, auf der 80% der Zellen Wände und der Rest Fußböden sind.

Wir initialisieren die neue Karte für unser Spiel im erstellen() Funktion unmittelbar nach dem Einrichten der Keyboard-Event-Listener:

function create () // Init-Tastaturbefehle game.input.keyboard.addCallbacks (null, null, onKeyUp); // Karte initialisieren initMap (); 

Sie können die Demo hier sehen - obwohl es wieder nichts zu sehen gibt, da wir die Karte noch nicht gerendert haben.


Der Bildschirm

Es ist Zeit, unsere Karte zu zeichnen! Unser Bildschirm wird aus einem 2D-Array von Textelementen bestehen, die jeweils ein einzelnes Zeichen enthalten:

 // die ASCII-Anzeige als 2d-Array von Zeichen var asciidisplay;

Beim Zeichnen der Karte wird der Bildschirminhalt mit den Werten der Karte gefüllt, da beide einfache ASCII-Zeichen sind:

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

Bevor wir die Karte zeichnen, müssen wir den Bildschirm initialisieren. Wir gehen zurück zu unserem erstellen() Funktion:

 function create () // Init-Tastaturbefehle game.input.keyboard.addCallbacks (null, null, onKeyUp); // Karte initialisieren initMap (); // Bildschirm initialisieren 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); 

Sie sollten jetzt eine zufällige Karte sehen, wenn Sie das Projekt ausführen.


Klicken Sie hier, um das Spiel bis jetzt anzuzeigen.

Schauspieler

Als nächstes stehen die Schauspieler: unser Spielercharakter und die Feinde, die sie besiegen müssen. Jeder Schauspieler ist ein Objekt mit drei Feldern: x und y für seinen Standort in der Karte und hp für seine Trefferpunkte.

Wir behalten alle Schauspieler im SchauspielerListe Array (dessen erstes Element der Spieler ist). Wir haben auch ein assoziatives Array mit den Standorten der Schauspieler als Schlüssel für die schnelle Suche, so dass wir nicht die gesamte Schauspielerliste durchlaufen müssen, um herauszufinden, welcher Schauspieler einen bestimmten Ort belegt. Dies wird uns helfen, wenn wir die Bewegung und den Kampf kodieren.

// eine Liste aller Akteure; 0 ist der Spieler var player; var actorList; var livingEnemies; // zeigt auf jeden Akteur in seiner Position, um schnell zu suchen var actorMap;

Wir erstellen alle unsere Schauspieler und weisen jedem eine zufällige freie Position in der Karte zu:

Funktion randomInt (max) return Math.floor (Math.random () * max);  function initActors () // Akteure an zufälligen Orten erstellen actorList = []; actorMap = ; für (var e = 0; e 

Es ist Zeit, den Schauspielern zu zeigen! Wir werden alle Feinde als zeichnen e und der Spielercharakter als Anzahl der Trefferpunkte:

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

Wir nutzen die Funktionen, die wir gerade geschrieben haben, um alle Akteure in unseren zu initialisieren und zu zeichnen erstellen() Funktion:

Funktion create () … // Akteure initialisieren initActors ();… drawActors (); 

Wir können jetzt sehen, wie sich unser Spielercharakter und unsere Feinde im Level ausbreiten!


Klicken Sie hier, um das Spiel bis jetzt anzuzeigen.

Blockieren und begehbare Fliesen

Wir müssen sicherstellen, dass unsere Darsteller nicht vom Bildschirm und durch Wände laufen. Fügen Sie also diese einfache Überprüfung hinzu, um zu sehen, in welche Richtung ein bestimmter Darsteller gehen kann:

Funktion 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] == '.'; 

Bewegung und Kampf

Wir sind endlich zu einer Interaktion gekommen: Bewegung und Kampf! Da bei klassischen Roguelikes der Grundangriff durch den Wechsel zu einem anderen Schauspieler ausgelöst wird, behandeln wir beide an derselben Stelle, unserer ziehen nach() Funktion, die einen Schauspieler und eine Richtung übernimmt (die Richtung ist die gewünschte Differenz in x und y zu der Position, in die der Schauspieler eintritt):

function moveTo (actor, dir) // prüfe, ob sich der Schauspieler in die angegebene Richtung bewegen kann, wenn (! canGo (actor, dir)) false zurückgibt; // verschiebt den Schauspieler an den neuen Speicherort var newKey = (actor.y + dir.y) + '_' + (actor.x + dir.x); // wenn auf der Zielkachel ein Akteur enthalten ist if (actorMap [newKey]! = null) // Herabsetzen der Trefferpunkte des Schauspielers an der Zielkachel var victim = actorMap [newKey]; victim.hp--; // Wenn es tot ist, entferne seine Referenz if (victim.hp == 0) actorMap [newKey] = null; actorList [actorList.indexOf (victim)] = null; if (Opfer! = Spieler) livingEnemies--; if (livingEnemies == 0) // Siegmeldung var victory = game.add.text (game.world.centerX, game.world.centerY, 'Sieg! \ nCtrl + r zum Neustart'), fill: '# 2e2 ', im Zentrum anordnen"  ); Victory.anchor.setTo (0,5,0,5);  else // Verweis auf die alte Position des Schauspielers entfernen actorMap [actor.y + '_' + actor.x] = null; // aktualisiere die Position actor.y + = dir.y; actor.x + = dir.x; // Verweis auf die neue Position des Schauspielers hinzufügen actorMap [actor.y + '_' + actor.x] = actor;  return true; 

Grundsätzlich gilt:

  1. Wir stellen sicher, dass der Schauspieler versucht, sich in eine gültige Position zu begeben.
  2. Wenn sich ein anderer Schauspieler in dieser Position befindet, greifen wir ihn an (und töten ihn, wenn sein HP-Wert 0 erreicht)..
  3. Wenn es keinen anderen Schauspieler in der neuen Position gibt, ziehen wir dorthin.

Beachten Sie, dass auch eine einfache Siegesmeldung angezeigt wird, wenn der letzte Gegner getötet wurde, und kehren Sie zurück falsch oder wahr je nachdem, ob wir einen gültigen Umzug durchgeführt haben oder nicht.

Kommen wir zu unserem zurück onKeyUp () funktionieren und ändern Sie es so, dass jedes Mal, wenn der Benutzer eine Taste drückt, die Positionen des vorherigen Schauspielers vom Bildschirm gelöscht wird (indem Sie die Karte oben zeichnen), den Spieler an die neue Position bewegen und die Akteure dann neu zeichnen:

Funktion onKeyUp (Ereignis) // Karte zeichnen, um die vorherigen Schauspieler zu überschreiben Positionen drawMap (); // Act on Player-Eingabe var acted = false; switch (event.keyCode) case Phaser.Keyboard.LEFT: acted = moveTo (Spieler, x: -1, y: 0); brechen; case Phaser.Keyboard.RIGHT: acted = moveTo (Spieler, x: 1, y: 0); brechen; case Phaser.Keyboard.UP: acted = moveTo (Spieler, x: 0, y: -1); brechen; case Phaser.Keyboard.DOWN: acted = moveTo (Spieler, x: 0, y: 1); brechen;  // Akteure in neue Positionen zeichnen drawActors (); 

Wir werden bald das verwenden handelte Variable, um zu wissen, ob die Feinde nach jeder Spielereingabe wirken sollen.


Klicken Sie hier, um das Spiel bis jetzt anzuzeigen.

Künstliche Grundintelligenz

Nun, da unser Spielercharakter sich bewegt und angreift, lassen Sie uns sogar die Chancen einschränken, indem Sie die Feinde nach sehr einfacher Pfadfindung verhalten, solange der Spieler sechs Schritte oder weniger von ihnen entfernt ist. (Wenn der Spieler weiter entfernt ist, läuft der Feind zufällig.)

Beachten Sie, dass unser Angriffscode sich nicht darum kümmert, wen der Schauspieler angreift. Dies bedeutet, dass, wenn Sie sie genau richtig ausrichten, die Feinde sich gegenseitig angreifen, während sie versuchen, den Spielercharakter im Doom-Stil zu verfolgen!

Funktion aiAct (Darsteller) Var Richtungen = [x: -1, y: 0, x: 1, y: 0, x: 0, y: -1, x: 0, y: 1 ]; var dx = player.x - actor.x; var dy = Spieler.y - Schauspieler.y; // Wenn der Spieler weit weg ist, gehen Sie nach dem Zufallsprinzip, wenn (Math.abs (dx) + Math.abs (dy)> 6) // versuchen, in zufällige Richtungen zu laufen, bis Sie einmal erfolgreich sind (! moveTo (Schauspieler, Richtungen [randomInt (direction.length)])) ; // Ansonsten gehe in Richtung Spieler 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);  

Wir haben auch eine Game Over-Nachricht hinzugefügt, die angezeigt wird, wenn einer der Feinde den Spieler tötet.

Jetzt müssen Sie nur noch die Feinde dazu bringen, sich jedes Mal zu bewegen, wenn sich der Spieler bewegt onKeyUp () Funktionen vor dem Zeichnen der Akteure in ihrer neuen Position:

function onKeyUp (event) … // Feinde handeln jedes Mal, wenn der Spieler das tut, wenn (gespielt wird) (var Feind in der Schauspielerliste) // den Spieler überspringen, wenn (Feind == 0) continue; var e = actorList [Feind]; if (e! = null) aiAct (e);  // Akteure in neue Positionen zeichnen drawActors (); 

Klicken Sie hier, um das Spiel bis jetzt anzuzeigen.

Bonus: Haxe Version

Ich habe dieses Tutorial ursprünglich in einer Haxe geschrieben, einer großartigen Multi-Plattform-Sprache, die JavaScript (unter anderen Sprachen) kompiliert. Obwohl ich die obige Version von Hand übersetzt habe, um sicherzustellen, dass wir idiosynkratisches JavaScript erhalten, wenn Sie, wie ich, Haxe gegenüber JavaScript bevorzugen, finden Sie die Haxe-Version in der haxe Ordner des Quelldownloads.

Sie müssen zuerst den haxe-Compiler installieren und können einen beliebigen Texteditor verwenden, um den haxe-Code durch Aufrufen zu kompilieren haxe build.hxml oder doppelklicken Sie auf die build.hxml Datei. Ich habe auch ein FlashDevelop-Projekt hinzugefügt, wenn Sie eine nette IDE einem Texteditor und einer Befehlszeile vorziehen. einfach offen rl.hxproj und drücke F5 zu rennen.


Zusammenfassung

Das ist es! Wir haben jetzt ein komplettes einfaches Roguelike mit zufälliger Kartenerstellung, Bewegung, Kampf, KI und sowohl Gewinn- als auch Verlustbedingungen.

Hier sind einige Ideen für neue Funktionen, die Sie Ihrem Spiel hinzufügen können:

  • mehrere Ebenen
  • Einschalten
  • Inventar
  • Verbrauchsmaterial
  • Ausrüstung

Genießen!