Comment faire votre premier Roguelike

Roguelikes a récemment été à l'honneur, avec des jeux tels que Dungeons of Dredmor, Spelunky, The Binding of Isaac et FTL, qui ont attiré un large public et ont été salués par la critique. Longtemps appréciés par les joueurs hardcore dans une petite niche, les éléments fantastiques de diverses combinaisons permettent maintenant d’apporter plus de profondeur et de rejouabilité à de nombreux genres existants..


Wayfarer, un roguelike 3D en développement.

Dans ce didacticiel, vous apprendrez à créer un roguelike traditionnel à l'aide de JavaScript et du moteur de jeu HTML 5 Phaser. À la fin, vous aurez un jeu simple et fonctionnel entièrement fonctionnel, jouable dans votre navigateur! (Pour nos besoins, un roguelike traditionnel est défini comme un dungeon-crawler à tour de rôle à un joueur, randomisé, avec permadeath.)


Cliquez pour jouer au jeu. Articles Similaires
  • Comment apprendre le moteur de jeu Phaser HTML5

Remarque: bien que le code de ce didacticiel utilise JavaScript, HTML et Phaser, vous devriez pouvoir utiliser la même technique et les mêmes concepts dans presque tous les autres langages de codage et moteurs de jeu..


Se préparer

Pour ce tutoriel, vous aurez besoin d'un éditeur de texte et d'un navigateur. J'utilise Notepad ++ et je préfère Google Chrome pour ses outils de développement complets, mais le flux de travail sera pratiquement le même quel que soit l'éditeur de texte ou le navigateur que vous choisissez..

Vous devez ensuite télécharger les fichiers source et commencer par le init dossier; cela contient Phaser et les fichiers HTML et JS de base pour notre jeu. Nous allons écrire notre code de jeu dans le champ vide rl.js fichier.

le index.html file charge simplement Phaser et notre fichier de code de jeu susmentionné:

  tutoriel roguelike    

Initialisation et Définitions

Pour le moment, nous utiliserons les graphiques ASCII pour notre roguelike. Dans le futur, nous pourrions les remplacer par des graphiques bitmap, mais pour le moment, utiliser du simple ASCII nous facilite la vie..

Définissons des constantes pour la taille de la police, les dimensions de notre carte (c'est-à-dire le niveau) et le nombre d'acteurs qui y apparaissent:

 // taille de la police var FONT = 32; // dimensions de la carte var ROWS = 10; var COLS = 15; // nombre d'acteurs par niveau, y compris le joueur var ACTORS = 10;

Initialisons également Phaser et écoutons les événements de frappe au clavier, car nous allons créer un jeu au tour par tour et vouloir agir une fois pour chaque coup de touche:

// initialise le phaser, appelle create () une fois terminé var game = new Phaser.Game (COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, create: create); function create () // commandes de clavier initiales game.input.keyboard.addCallbacks (null, null, onKeyUp);  function onKeyUp (event) switch (event.keyCode) case Keyboard.LEFT: case Keyboard.RIGHT: cas Keyboard.UP: cas Keyboard.DOWN:

Comme les polices monospaces par défaut ont tendance à être environ 60% plus larges qu’elles sont hautes, nous avons initialisé la taille de la zone de travail 0.6 * la taille de la police * le nombre de colonnes. Nous disons également à Phaser qu’il devrait appeler notre créer() fonctionner immédiatement après son initialisation, nous initialisons alors les commandes du clavier.

Vous pouvez voir le jeu jusqu'à présent ici-pas qu'il y a beaucoup à voir!


La carte

La mappe de tuiles représente notre aire de jeu: un tableau 2D discret (par opposition à continu) de tuiles, ou cellules, chacune représentée par un caractère ASCII pouvant désigner soit un mur (#: bloque le mouvement) ou au sol (.: ne bloque pas le mouvement):

 // la structure de la carte var map;

Utilisons la forme la plus simple de génération procédurale pour créer nos cartes: décider aléatoirement quelle cellule doit contenir un mur et quel étage:

function initMap () // crée une nouvelle carte aléatoire map = =]; pour (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); 
Articles Similaires
  • Comment utiliser les arbres BSP pour générer des cartes de jeu
  • Générer des niveaux de cavité aléatoires à l'aide d'automates cellulaires

Cela devrait nous donner une carte où 80% des cellules sont des murs et le reste des planchers.

Nous initialisons la nouvelle carte pour notre jeu dans le créer() immédiatement après la configuration des écouteurs d’événements du clavier:

function create () // commandes de clavier initiales game.input.keyboard.addCallbacks (null, null, onKeyUp); // initialise la carte initMap (); 

Vous pouvez voir la démo ici - bien que, encore une fois, il n'y a rien à voir, car nous n'avons pas encore rendu la carte.


L'écran

Il est temps de dessiner notre carte! Notre écran sera un tableau 2D d'éléments de texte, chacun contenant un seul caractère:

 // l'affichage ascii, sous la forme d'un tableau 2d de caractères var asciidisplay;

Dessiner la carte remplira le contenu de l'écran avec les valeurs de la carte, car il s'agit de simples caractères ASCII:

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

Enfin, avant de dessiner la carte, nous devons initialiser l’écran. Nous revenons à notre créer() une fonction:

 function create () // commandes de clavier initiales game.input.keyboard.addCallbacks (null, null, onKeyUp); // initialise la carte initMap (); // initialise l'écran asciidisplay = []; pour (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); 

Vous devriez maintenant voir une carte aléatoire affichée lorsque vous exécutez le projet.


Cliquez pour voir le jeu jusqu'à présent.

Acteurs

Viennent ensuite les acteurs: notre personnage et les ennemis qu’ils doivent vaincre. Chaque acteur sera un objet avec trois champs: X et y pour son emplacement sur la carte, et hp pour ses points de vie.

Nous gardons tous les acteurs dans le actorList tableau (dont le premier élément est le joueur). Nous gardons également un tableau associatif avec les emplacements des acteurs comme clés de recherche rapide, de sorte que nous n’ayons pas à parcourir toute la liste des acteurs pour trouver quel acteur occupe un emplacement donné; cela nous aidera lorsque nous coderons le mouvement et le combat.

// une liste de tous les acteurs; 0 est le joueur var player; var actorList; var livingEnemies; // pointe vers chaque acteur dans sa position, pour une recherche rapide var actorMap;

Nous créons tous nos acteurs et attribuons une position libre aléatoire sur la carte à chacun:

function randomInt (max) return Math.floor (Math.random () * max);  function initActors () // créer des acteurs à des emplacements aléatoires actorList = []; actorMap = ; pour (var e = 0; e 

Il est temps de montrer les acteurs! Nous allons attirer tous les ennemis comme e et le personnage du joueur en nombre de points de vie:

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

Nous utilisons les fonctions que nous venons d’écrire pour initialiser et dessiner tous les acteurs de notre créer() une fonction:

function create () … // initialise les acteurs initActors ();… drawActors (); 

Nous pouvons maintenant voir notre personnage et nos ennemis répartis dans le niveau!


Cliquez pour voir le jeu jusqu'à présent.

Tuiles bloquantes et praticables

Nous devons nous assurer que nos acteurs ne sortent pas de l'écran et ne traversent pas les murs. Ajoutons donc cette vérification simple pour voir dans quelles directions un acteur donné peut marcher:

fonction canGo (acteur, répertoire) 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] == '.'; 

Mouvement et combat

Nous sommes enfin arrivés à une certaine interaction: mouvement et combat! Comme, dans les roguelikes classiques, l’attaque de base est déclenchée par le passage à un autre acteur, nous traitons les deux au même endroit, notre déménager à() fonction, qui prend un acteur et une direction (la direction est la différence souhaitée dans X et y à la position dans laquelle l'acteur intervient):

function moveTo (actor, dir) // vérifie si l'acteur peut se déplacer dans la direction donnée si (! canGo (acteur, dir)) renvoie false; // déplace l'acteur vers le nouvel emplacement var newKey = (actor.y + dir.y) + '_' + (actor.x + dir.x); // si la tuile de destination contient un acteur if (actorMap [newKey]! = null) // décrémente les repères de l'acteur sur la tuile de destination var victim = actorMap [newKey]; victime.hp--; // s'il est mort, supprime sa référence if (victim.hp == 0) actorMap [newKey] = null; actorList [actorList.indexOf (victime)] = null; si (victime! = joueur) livingEnemies--; if (livingEnemies == 0) // message de victoire var victoire = game.add.text (game.world.centerX, game.world.centerY, 'Victory! \ nCtrl + r pour redémarrer', fill: '# 2e2 ', aligner: "center"); victoire.anchor.setTo (0.5,0.5);  else // supprime la référence à l'ancienne position de l'acteur actorMap [actor.y + '_' + actor.x] = null; // met à jour la position actor.y + = dir.y; actor.x + = dir.x; // ajoute une référence à la nouvelle position de l'acteur actorMap [actor.y + '_' + actor.x] = actor;  return true; 

Fondamentalement:

  1. Nous nous assurons que l'acteur essaie de passer à une position valide.
  2. S'il y a un autre acteur dans cette position, nous l'attaquerons (et le tuerons si son nombre de PV atteint 0).
  3. S'il n'y a pas un autre acteur dans le nouveau poste, nous y allons.

Notez que nous montrons également un message simple de victoire une fois que le dernier ennemi a été tué, et revenons faux ou vrai selon que nous ayons réussi ou non à effectuer un déménagement valide.

Maintenant, revenons à notre onKeyUp () la fonction et la modifier de sorte que chaque fois que l'utilisateur appuie sur une touche, nous effaçons de l'écran les positions des acteurs précédents (en traçant la carte au-dessus), nous déplaçons le personnage du joueur vers le nouvel emplacement, puis nous redessinions les acteurs:

function onKeyUp (event) // dessine la carte pour écraser les positions des acteurs précédents drawMap (); // agir sur l'entrée du joueur var acted = false; switch (event.keyCode) case Phaser.Keyboard.LEFT: agi = moveTo (lecteur, x: -1, y: 0); Pause; case Phaser.Keyboard.RIGHT: agi = moveTo (joueur, x: 1, y: 0); Pause; case Phaser.Keyboard.UP: acted = moveTo (joueur, x: 0, y: -1); Pause; case Phaser.Keyboard.DOWN: acted = moveTo (lecteur, x: 0, y: 1); Pause;  // dessine des acteurs dans de nouvelles positions drawActors (); 

Nous allons bientôt utiliser le a agi variable pour savoir si les ennemis doivent agir après chaque entrée de joueur.


Cliquez pour voir le jeu jusqu'à présent.

Intelligence artificielle de base

Maintenant que notre personnage est en mouvement et en attaque, équilibrons les chances en faisant en sorte que les ennemis agissent selon un chemin très simple, à condition que le joueur se trouve à six pas ou moins de lui. (Si le joueur est plus loin, l'ennemi marche au hasard.)

Notez que notre code d’attaque ne s’intéresse pas à qui l’acteur s’attaque; cela signifie que, si vous les alignez parfaitement, les ennemis s’attaqueront en essayant de poursuivre le personnage du joueur, à la Doom!

fonction aiAct (acteur) 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; // si le joueur est loin, marche au hasard si (Math.abs (dx) + Math.abs (dy)> 6) // essaie de marcher dans des directions aléatoires jusqu'à ce que tu réussisses une fois (! moveTo (acteur, directions [randomInt (directions.length)])) ; // sinon marche vers le joueur si (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);  

Nous avons également ajouté un message "Game Over", qui s'affiche si l'un des ennemis tue le joueur..

Maintenant, tout ce qui reste à faire est de faire agir les ennemis à chaque fois que le joueur bouge, ce qui nécessite d’ajouter ce qui suit à la fin de notre onKeyUp () fonctions, juste avant de dessiner les acteurs dans leur nouvelle position:

function onKeyUp (événement) … // les ennemis agissent à chaque fois que le joueur agit si (agit) pour (var ennemi dans actorList) // ignore le joueur si (ennemi == 0) continue; var e = actorList [ennemi]; if (e! = null) aiAct (e);  // dessine des acteurs dans de nouvelles positions drawActors (); 

Cliquez pour voir le jeu jusqu'à présent.

Bonus: Version Haxe

J'ai initialement écrit ce didacticiel dans Haxe, un excellent langage multi-plateforme compilant JavaScript (entre autres langages). Bien que j’ai traduit manuellement la version ci-dessus pour nous assurer que nous obtenons un code JavaScript idiosyncratique, si, comme moi, vous préférez Haxe à JavaScript, vous pouvez trouver la version Haxe dans la haxe dossier du téléchargement source.

Vous devez d’abord installer le compilateur haxe. Vous pouvez utiliser l’éditeur de texte de votre choix et compiler le code haxe en appelant haxe build.hxml ou en double-cliquant sur le build.hxml fichier. J'ai également inclus un projet FlashDevelop si vous préférez un IDE agréable à un éditeur de texte et à une ligne de commande; viens d'ouvrir rl.hxproj et appuyez sur F5 courir.


Résumé

C'est tout! Nous avons maintenant un roguelike complet et simple, avec génération de carte aléatoire, mouvement, combat, IA et conditions gagnantes et perdantes.

Voici quelques idées de nouvelles fonctionnalités que vous pouvez ajouter à votre jeu:

  • plusieurs niveaux
  • mises sous tension
  • inventaire
  • consommables
  • équipement

Prendre plaisir!