Cómo hacer tu primer Roguelike

Los Roguelikes han estado en el centro de atención recientemente, con juegos como Dungeons of Dredmor, Spelunky, The Binding of Isaac, y FTL que llegan a un gran público y son aclamados por la crítica. Disfrutados durante mucho tiempo por jugadores hardcore en un pequeño nicho, los elementos roguelike en varias combinaciones ahora ayudan a brindar más profundidad y reproducibilidad a muchos géneros existentes.


Wayfarer, un roguelike 3D actualmente en desarrollo.

En este tutorial, aprenderás cómo hacer un roguelike tradicional usando JavaScript y el motor de juego HTML 5 Phaser. Al final, tendrás un juego de roguelike simple completamente funcional, que puedes jugar en tu navegador. (Para nuestros propósitos, un roguelike tradicional se define como un rastreador de mazmorras aleatorio basado en turnos para un solo jugador con permadeath).


Haz click para jugar el juego.. Artículos Relacionados
  • Cómo aprender el motor de juego Phaser HTML5

Nota: Aunque el código de este tutorial utiliza JavaScript, HTML y Phaser, debería poder usar la misma técnica y conceptos en casi cualquier otro lenguaje de codificación y motor de juego..


Preparandose

Para este tutorial, necesitará un editor de texto y un navegador. Utilizo Notepad ++, y prefiero Google Chrome por sus amplias herramientas de desarrollo, pero el flujo de trabajo será prácticamente el mismo con cualquier editor de texto y navegador que elija..

A continuación, debe descargar los archivos de origen y comenzar con la en eso carpeta; Esto contiene Phaser y los archivos HTML y JS básicos para nuestro juego. Escribiremos nuestro código de juego en el vacío actual. rl.js expediente.

los index.html el archivo simplemente carga Phaser y nuestro archivo de código de juego mencionado anteriormente:

  tutorial roguelike    

Inicialización y definiciones

Por el momento, usaremos gráficos ASCII para nuestros roguelike. En el futuro, podríamos reemplazarlos con gráficos de mapa de bits, pero por ahora, usar ASCII simple hace nuestra vida más fácil.

Definamos algunas constantes para el tamaño de fuente, las dimensiones de nuestro mapa (es decir, el nivel) y la cantidad de actores que generan:

 // fuente tamaño var FONT = 32; // dimensiones del mapa var ROWS = 10; var COLS = 15; // número de actores por nivel, incluyendo jugador var ACTORES = 10;

También inicialicemos Phaser y escuchemos eventos de teclado, ya que crearemos un juego basado en turnos y querremos actuar una vez por cada golpe de tecla:

// inicializar phaser, llamar a create () una vez hecho var game = new Phaser.Game (COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, create: create); function create () // init keyboard command 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:

Dado que las fuentes monoespaciadas predeterminadas tienden a ser aproximadamente un 60% tan amplias como altas, hemos inicializado el tamaño del lienzo para que sea 0.6 * el tamaño de letra * el número de columnas. También le estamos diciendo a Phaser que debería llamar a nuestro crear() funciona inmediatamente después de que se haya terminado de inicializar, momento en el que inicializamos los controles del teclado.

Puedes ver el juego hasta aquí, no hay mucho que ver.!


El mapa

El mapa de mosaicos representa nuestra área de juego: una matriz 2D discreta (en lugar de continua) de mosaicos o celdas, cada una representada por un carácter ASCII que puede significar una pared (#: bloqueos de movimiento) o piso (.: no bloquea el movimiento):

 // la estructura del mapa var map;

Usemos la forma más simple de generación de procedimientos para crear nuestros mapas: decidir aleatoriamente qué celda debe contener una pared y cuál piso:

function initMap () // crear un nuevo mapa de mapa aleatorio = []; para (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); 
Artículos Relacionados
  • Cómo usar los árboles BSP para generar mapas de juegos
  • Generar niveles de cueva aleatorios utilizando autómatas celulares

Esto nos debe dar un mapa donde el 80% de las celdas son paredes y el resto son pisos..

Inicializamos el nuevo mapa para nuestro juego en el crear() función, inmediatamente después de configurar los escuchas de eventos de teclado:

function create () // init keyboard command game.input.keyboard.addCallbacks (null, null, onKeyUp); // inicializar el mapa initMap (); 

Puede ver la demostración aquí, aunque, nuevamente, no hay nada que ver, ya que aún no hemos renderizado el mapa..


La pantalla

¡Es hora de dibujar nuestro mapa! Nuestra pantalla será una matriz 2D de elementos de texto, cada uno con un único carácter:

 // la pantalla ascii, como una matriz de caracteres 2d var asciidisplay;

Al dibujar el mapa se completará el contenido de la pantalla con los valores del mapa, ya que ambos son caracteres ASCII simples:

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

Finalmente, antes de dibujar el mapa tenemos que inicializar la pantalla. Volvemos a nuestro crear() función:

 function create () // init keyboard command game.input.keyboard.addCallbacks (null, null, onKeyUp); // inicializar el mapa initMap (); // inicializar la pantalla asciidisplay = []; para (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); 

Ahora debería ver un mapa aleatorio que se muestra cuando ejecuta el proyecto.


Haz clic para ver el juego hasta ahora..

Los actores

Los siguientes son los actores: nuestro personaje de jugador y los enemigos que deben derrotar. Cada actor será un objeto con tres campos: X y y por su ubicación en el mapa, y hp por sus puntos de golpe.

Mantenemos a todos los actores en el actorLista array (cuyo primer elemento es el jugador). También mantenemos una matriz asociativa con las ubicaciones de los actores como claves para una búsqueda rápida, de modo que no tengamos que recorrer toda la lista de actores para encontrar qué actor ocupa una determinada ubicación; Esto nos ayudará cuando codifiquemos el movimiento y combatamos..

// una lista de todos los actores; 0 es el jugador var player; var actorList; var livingEnemies; // apunta a cada actor en su posición, para una búsqueda rápida var actorMap;

Creamos a todos nuestros actores y asignamos una posición libre aleatoria en el mapa a cada uno:

function randomInt (max) return Math.floor (Math.random () * max);  function initActors () // crea actores en ubicaciones aleatorias actorList = []; actorMap = ; para (var e = 0; e 

¡Es hora de mostrar a los actores! Vamos a dibujar a todos los enemigos como mi y el personaje del jugador como su número de puntos de golpe:

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

Hacemos uso de las funciones que acabamos de escribir para inicializar y dibujar a todos los actores en nuestro crear() función:

function create () … // inicializa actores initActors ();… drawActors (); 

Ahora podemos ver nuestro personaje de jugador y enemigos dispersos en el nivel!


Haz clic para ver el juego hasta ahora..

Azulejos de bloqueo y transitables

Debemos asegurarnos de que nuestros actores no estén saliendo de la pantalla y por las paredes, así que agreguemos esta simple comprobación para ver en qué dirección puede caminar un actor determinado:

función 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] == '.'; 

Movimiento y combate

Finalmente hemos llegado a alguna interacción: movimiento y combate! Ya que, en los roguelikes clásicos, el ataque básico se desencadena al pasar a otro actor, manejamos ambos en el mismo lugar, nuestro mover a() función, que toma un actor y una dirección (la dirección es la diferencia deseada en X y y a la posición en la que interviene el actor):

function moveTo (actor, dir) // verifica si el actor puede moverse en la dirección dada si (! canGo (actor, dir)) devuelve false; // mueve al actor a la nueva ubicación var newKey = (actor.y + dir.y) + '_' + (actor.x + dir.x); // si la ficha de destino tiene un actor si (actorMap [newKey]! = null) // disminuye los puntos de golpe del actor en la ficha de destino var victim = actorMap [newKey]; victim.hp--; // si está muerto, elimina su referencia if (victim.hp == 0) actorMap [newKey] = null; actorList [actorList.indexOf (víctima)] = nulo; if (victim! = player) livingEnemies--; if (livingEnemies == 0) // victory message var victory = game.add.text (game.world.centerX, game.world.centerY, 'Victory! \ nCtrl + r para reiniciar', fill: '# 2e2 ', Alinear al centro"  ); victory.anchor.setTo (0.5,0.5);  else // eliminar la referencia a la posición anterior del actor actorMap [actor.y + '_' + actor.x] = null; // actualizar posición actor.y + = dir.y; actor.x + = dir.x; // añadir referencia a la nueva posición del actor actorMap [actor.y + '_' + actor.x] = actor;  devuelve true; 

Básicamente:

  1. Nos aseguramos de que el actor esté tratando de moverse a una posición válida.
  2. Si hay otro actor en esa posición, lo atacamos (y lo matamos si su cuenta de HP llega a 0).
  3. Si no hay otro actor en la nueva posición, nos mudamos allí..

Observe que también mostramos un simple mensaje de victoria una vez que el último enemigo ha sido asesinado, y regrese falso o cierto Dependiendo de si logramos o no realizar un movimiento válido.

Ahora, volvamos a nuestra onKeyUp () Funcione y modifíquelo para que, cada vez que el usuario presione una tecla, borre las posiciones del actor anterior de la pantalla (dibujando el mapa en la parte superior), movamos el personaje del jugador a la nueva ubicación y luego redibujemos a los actores:

function onKeyUp (event) // dibujar mapa para sobrescribir las posiciones de los actores anteriores drawMap (); // actuar sobre la entrada del jugador var actuado = falso; switch (event.keyCode) case Phaser.Keyboard.LEFT: actuado = moveTo (jugador, x: -1, y: 0); descanso; caso Phaser.Keyboard.RIGHT: actuado = moveTo (jugador, x: 1, y: 0); descanso; caso Phaser.Keyboard.UP: actuado = moveTo (jugador, x: 0, y: -1); descanso; caso Phaser.Keyboard.DOWN: actuado = moveTo (jugador, x: 0, y: 1); descanso;  // dibujar actores en nuevas posiciones drawActors (); 

Pronto usaremos el actuó Variable para saber si los enemigos deben actuar después de cada entrada del jugador..


Haz clic para ver el juego hasta ahora..

Inteligencia Artificial Básica

Ahora que el personaje de nuestro jugador se está moviendo y atacando, vamos a igualar las probabilidades haciendo que los enemigos actúen de acuerdo con un proceso de búsqueda muy simple, siempre que el jugador esté a seis pasos o menos de ellos. (Si el jugador está más lejos, el enemigo camina al azar).

Tenga en cuenta que a nuestro código de ataque no le importa a quién ataca el actor; Esto significa que, si los alineas correctamente, los enemigos se atacarán entre sí mientras intentan perseguir al personaje del jugador, al estilo Doom.!

función aiAct (actor) direcciones var = = 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 el jugador está lejos, camina al azar si (Math.abs (dx) + Math.abs (dy)> 6) // intenta caminar en direcciones aleatorias hasta que tengas éxito una vez mientras (! moveTo (actor, direcciones [randomInt (Directions.length)])) ; // de lo contrario, camina hacia el jugador 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);  

También hemos agregado un juego sobre mensaje, que se muestra si uno de los enemigos mata al jugador.

Ahora todo lo que queda por hacer es hacer que los enemigos actúen cada vez que el jugador se mueve, lo que requiere agregar lo siguiente al final de nuestra onKeyUp () Funciones, justo antes de dibujar a los actores en su nueva posición:

función onKeyUp (evento) … // los enemigos actúan cada vez que el jugador lo hace si (actuó) para (var enemigo en actorList) // omita al jugador si (enemigo == 0) continúa; var e = actorList [enemigo]; if (e! = null) aiAct (e);  // dibujar actores en nuevas posiciones drawActors (); 

Haz clic para ver el juego hasta ahora..

Bonus: Versión Haxe

Originalmente escribí este tutorial en un Haxe, un gran lenguaje multiplataforma que compila JavaScript (entre otros idiomas). Aunque traduje la versión anterior a mano para asegurarme de que obtuviéramos JavaScript idiosincrásico, si, como yo, prefieres Haxe a JavaScript, puedes encontrar la versión de Haxe en la haxe carpeta de la fuente de descarga.

Primero debe instalar el compilador haxe y puede usar el editor de texto que desee y compilar el código haxe llamando haxe build.hxml o haga doble clic en el build.hxml expediente. También incluí un proyecto FlashDevelop si prefieres un IDE agradable a un editor de texto y una línea de comandos; acaba de abrir rl.hxproj y presione F5 correr.


Resumen

¡Eso es! Ahora tenemos un roguelike simple y completo, con generación aleatoria de mapas, movimiento, combate, IA y condiciones de ganar y perder..

Aquí hay algunas ideas para nuevas características que puedes agregar a tu juego:

  • niveles múltiples
  • potenciadores
  • inventario
  • consumibles
  • equipo

Disfrutar!