Advertisement
  1. Game Development
  2. Roguelike

Cómo hacer su primer Roguelike

Scroll to top
Read Time: 13 min

() translation by (you can also view the original English article)

Roguelikes han estado en el centro de atención recientemente, con juegos como Dungeons of Dredmor, Spelunky, The Binding of Isaac y FTL llegando a un público amplio y recibiendo elogios de la crítica. Durante mucho tiempo disfrutado por los jugadores hardcore en un pequeño nicho, elementos roguelike en diversas combinaciones ahora ayudan a traer más profundidad y repetición a muchos géneros existentes.

Wayfarer a 3D roguelike currently in developmentWayfarer a 3D roguelike currently in developmentWayfarer a 3D roguelike currently in development
Wayfarer, un 3D roguelike actualmente en desarrollo.

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

Click to play the game
Haga clic para jugar el juego.

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


Preparándose

Para este tutorial, necesitará un editor de texto y un navegador. Utilizo Notepad++, y prefiero Google Chrome por sus extensas 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 y comenzar con la carpeta init; Contiene Phaser y los archivos HTML y JS básicos de nuestro juego. Escribiremos nuestro código de juego en el archivo rl.js actualmente vacío.

El archivo index.html simplemente carga Phaser y nuestro archivo de código de juego antes mencionado:

1
<!DOCTYPE html>
2
<head>
3
  <title>roguelike tutorial</title>
4
	<script src="phaser.min.js"></script>
5
	<script src="rl.js"></script>
6
</head>
7
</html>

Inicialización y definiciones

Por el momento, vamos a utilizar gráficos ASCII para nuestro roguelike—en el futuro, podríamos reemplazar estos con gráficos de mapa de bits, pero por ahora, el uso de ASCII simple hace nuestra vida más fácil.

Vamos a definir algunas constantes para el tamaño de la fuente, las dimensiones de nuestro mapa (es decir, el nivel) y cuántos actores aparecen en él:

1
        // font size

2
        var FONT = 32;
3
4
        // map dimensions

5
        var ROWS = 10;
6
        var COLS = 15;
7
8
        // number of actors per level, including player

9
        var ACTORS = 10;

También vamos a inicializar Phaser y escuchar los eventos  key-up del teclado, ya que estaremos creando un juego basado en turnos y queremos actuar una vez por cada golpe de tecla:

1
// initialize phaser, call create() once done

2
var game = new Phaser.Game(COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, {
3
        create: create
4
});
5
6
function create() {
7
        // init keyboard commands

8
        game.input.keyboard.addCallbacks(null, null, onKeyUp);
9
}
10
11
function onKeyUp(event) {
12
        switch (event.keyCode) {
13
                case Keyboard.LEFT:
14
15
                case Keyboard.RIGHT:
16
17
                case Keyboard.UP:
18
19
                case Keyboard.DOWN:
20
21
        }
22
}

Dado que las fuentes monospace predeterminadas tienden a ser alrededor del 60% tan anchas como altas, hemos inicializado el tamaño del lienzo para que sea 0,6 * el tamaño de fuente * el número de columnas. También le decimos a Phaser que debería llamar a nuestra función create() inmediatamente después de que haya terminado de inicializar, momento en el cual inicializaremos los controles del teclado.

Puedes ver el juego hasta ahora aquí—¡no que hay mucho que ver!


El mapa

El mapa representa nuestro área de juego: una matriz 2D discreta (en oposición a continua) de azulejos, o células, cada una representada por un carácter ASCII que puede significar una pared (#: bloquea el movimiento) o el piso (.: Bloqueo de movimiento):

1
        // the structure of the map

2
        var map;

Vamos a utilizar la forma más simple de la generación de procedimientos para crear nuestros mapas: decidir al azar qué celda debe contener una pared y que un piso:

1
function initMap() {
2
        // create a new random map

3
        map = [];
4
        for (var y = 0; y < ROWS; y++) {
5
                var newRow = [];
6
                for (var x = 0; x < COLS; x++) {
7
                     if (Math.random() > 0.8)
8
                        newRow.push('#');
9
                    else
10
                        newRow.push('.');
11
                }
12
                map.push(newRow);
13
        }
14
}

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 la función create(), inmediatamente después de configurar los oyentes de eventos de teclado:

1
function create() {
2
        // init keyboard commands

3
        game.input.keyboard.addCallbacks(null, null, onKeyUp);
4
5
        // initialize map

6
        initMap();
7
}

Puedes ver la demo aquí—aunque, de nuevo, no hay nada que ver, ya que no hemos procesado el mapa todavía.


La pantalla

¡Es hora de dibujar nuestro mapa! Nuestra pantalla será una matriz 2D de elementos de texto, cada uno de los cuales contiene un solo carácter:

1
        // the ascii display, as a 2d array of characters

2
        var asciidisplay;

Dibujar el mapa rellenará el contenido de la pantalla con los valores del mapa, ya que ambos son simples caracteres ASCII:

1
        function drawMap() {
2
            for (var y = 0; y < ROWS; y++)
3
                for (var x = 0; x < COLS; x++)
4
                    asciidisplay[y][x].content = map[y][x];
5
        }

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

1
        function create() {
2
                // init keyboard commands

3
                game.input.keyboard.addCallbacks(null, null, onKeyUp);
4
5
                // initialize map

6
                initMap();
7
8
                // initialize screen

9
                asciidisplay = [];
10
                for (var y = 0; y < ROWS; y++) {
11
                        var newRow = [];
12
                        asciidisplay.push(newRow);
13
                        for (var x = 0; x < COLS; x++)
14
                                newRow.push( initCell('', x, y) );
15
                }
16
                drawMap();
17
        }
18
19
        function initCell(chr, x, y) {
20
                // add a single cell in a given position to the ascii display

21
                var style = { font: FONT + "px monospace", fill:"#fff"};
22
                return game.add.text(FONT*0.6*x, FONT*y, chr, style);
23
        }

Ahora debería ver un mapa aleatorio que se muestra al ejecutar el proyecto.

Click to view the game so far
Haz clic para ver el juego hasta ahora.

Actores

A continuación de la fila están los actores: nuestro personaje jugador, y los enemigos que deben derrotar. Cada actor será un objeto con tres campos: x e y para su ubicación en el mapa, y hp para sus puntos de golpe.

Mantenemos todos los actores en la matriz actorList (el primer elemento del cual es el jugador). También mantenemos una matriz asociativa con los lugares de los actores como claves para la búsqueda rápida, de modo que no tengamos que iterar sobre la lista completa de actores para encontrar qué actor ocupa un determinado lugar; Esto nos ayudará cuando codifiquemos el movimiento y el combate.

1
// a list of all actors; 0 is the player

2
var player;
3
var actorList;
4
var livingEnemies;
5
6
// points to each actor in its position, for quick searching

7
var actorMap;

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

1
function randomInt(max) {
2
   return Math.floor(Math.random() * max);
3
}
4
5
function initActors() {
6
        // create actors at random locations

7
        actorList = [];
8
        actorMap = {};
9
        for (var e=0; e<ACTORS; e++) {
10
                // create new actor

11
                var actor = { x:0, y:0, hp:e == 0?3:1 };
12
                do {
13
                        // pick a random position that is both a floor and not occupied

14
                        actor.y=randomInt(ROWS);
15
                        actor.x=randomInt(COLS);
16
                } while ( map[actor.y][actor.x] == '#' || actorMap[actor.y + "_" + actor.x] != null );
17
18
                // add references to the actor to the actors list & map

19
                actorMap[actor.y + "_" + actor.x]= actor;
20
                actorList.push(actor);
21
        }
22
23
        // the player is the first actor in the list

24
        player = actorList[0];
25
        livingEnemies = ACTORS-1;
26
}

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

1
function drawActors() {
2
        for (var a in actorList) {
3
                if (actorList[a].hp > 0)
4
                        asciidisplay[actorList[a].y][actorList[a].x].content = a == 0?''+player.hp:'e';
5
        }
6
}

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

1
function create() {
2
	...
3
	// initialize actors

4
	initActors();
5
	...
6
	drawActors();
7
}

¡Ahora podemos ver a nuestro personaje jugador y enemigos extendidos en el nivel!

Click to view the game so far
Haz clic para ver el juego hasta ahora.

Bloqueo y baldosas

Tenemos que asegurarnos de que nuestros actores no están corriendo de la pantalla y a través de las paredes, así que vamos a agregar este simple cheque para ver en qué direcciones un actor dado puede caminar:

1
function canGo(actor,dir) {
2
	return 	actor.x+dir.x >= 0 &&
3
		actor.x+dir.x <= COLS - 1 &&
4
                actor.y+dir.y >= 0 &&
5
		actor.y+dir.y <= ROWS - 1 &&
6
		map[actor.y+dir.y][actor.x +dir.x] == '.';
7
}

Movimiento y combate

Finalmente hemos llegado a alguna interacción: ¡movimiento y combate! Puesto que, en los roguelikes clásicos, el ataque básico se dispara al moverse a otro actor, manejamos ambos en el mismo lugar, nuestra función moveTo(), que toma un actor y una dirección (la dirección es la diferencia deseada en x e Y a la posición en que el actor entra):

1
function moveTo(actor, dir) {
2
        // check if actor can move in the given direction

3
        if (!canGo(actor,dir))
4
                return false;
5
6
        // moves actor to the new location

7
        var newKey = (actor.y + dir.y) +'_' + (actor.x + dir.x);
8
        // if the destination tile has an actor in it

9
        if (actorMap[newKey] != null) {
10
                //decrement hitpoints of the actor at the destination tile

11
                var victim = actorMap[newKey];
12
                victim.hp--;
13
14
                // if it's dead remove its reference

15
                if (victim.hp == 0) {
16
                        actorMap[newKey]= null;
17
                        actorList[actorList.indexOf(victim)]=null;
18
                        if(victim!=player) {
19
                                livingEnemies--;
20
                                if (livingEnemies == 0) {
21
                                        // victory message

22
                                        var victory = game.add.text(game.world.centerX, game.world.centerY, 'Victory!\nCtrl+r to restart', { fill : '#2e2', align: "center" } );
23
                                        victory.anchor.setTo(0.5,0.5);
24
                                }
25
                        }
26
                }
27
        } else {
28
                // remove reference to the actor's old position

29
                actorMap[actor.y + '_' + actor.x]= null;
30
31
                // update position

32
                actor.y+=dir.y;
33
                actor.x+=dir.x;
34
35
                // add reference to the actor's new position

36
                actorMap[actor.y + '_' + actor.x]=actor;
37
        }
38
        return true;
39
}

Básicamente:

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

Tenga en cuenta que también mostramos un simple mensaje de victoria una vez que el último enemigo ha sido asesinado, y devolveremos false o true dependiendo de si hemos logrado o no realizar un movimiento válido.

Ahora, volvamos a nuestra función onKeyUp() y lo alteramos para que, cada vez que el usuario presione una tecla, borremos las posiciones del actor anterior desde la pantalla (dibujando el mapa en la parte superior), moveremos el personaje del jugador al nuevo Ubicación, y luego redibujar a los actores:

1
function onKeyUp(event) {
2
        // draw map to overwrite previous actors positions

3
        drawMap();
4
5
        // act on player input

6
        var acted = false;
7
        switch (event.keyCode) {
8
                case Phaser.Keyboard.LEFT:
9
                        acted = moveTo(player, {x:-1, y:0});
10
                        break;
11
12
                case Phaser.Keyboard.RIGHT:
13
                        acted = moveTo(player,{x:1, y:0});
14
                        break;
15
16
                case Phaser.Keyboard.UP:
17
                        acted = moveTo(player, {x:0, y:-1});
18
                        break;
19
20
                case Phaser.Keyboard.DOWN:
21
                        acted = moveTo(player, {x:0, y:1});
22
                        break;
23
        }
24
25
        // draw actors in new positions

26
        drawActors();
27
}

Pronto usaremos la variable acted para saber si los enemigos deben actuar después de cada entrada del jugador.

Click to view the game so far
Haz clic para ver el juego hasta ahora.

Inteligencia Artificial Básica

Ahora que nuestro personaje jugador se está moviendo y atacando, vamos a nivelar las probabilidades haciendo que los enemigos actúen de acuerdo a una ruta de búsqueda muy simple, siempre y cuando el jugador este a seis pasos o menos de ellos. (Si el jugador está más lejos, el enemigo camina aleatoriamente.)

Note que nuestro código de ataque no le importa a quién el actor está atacando; Esto significa que, si los alineas justo a la derecha, los enemigos se atacarán mientras tratan de perseguir al personaje del jugador, ¡estilo Doom!

1
function aiAct(actor) {
2
        var directions = [ { x: -1, y:0 }, { x:1, y:0 }, { x:0, y: -1 }, { x:0, y:1 } ];
3
        var dx = player.x - actor.x;
4
        var dy = player.y - actor.y;
5
6
        // if player is far away, walk randomly

7
        if (Math.abs(dx) + Math.abs(dy) > 6)
8
                // try to walk in random directions until you succeed once

9
                while (!moveTo(actor, directions[randomInt(directions.length)])) { };
10
11
        // otherwise walk towards player

12
        if (Math.abs(dx) > Math.abs(dy)) {
13
                if (dx < 0) {
14
                        // left

15
                        moveTo(actor, directions[0]);
16
                } else {
17
                        // right

18
                        moveTo(actor, directions[1]);
19
                }
20
        } else {
21
                if (dy < 0) {
22
                        // up

23
                        moveTo(actor, directions[2]);
24
                } else {
25
                        // down

26
                        moveTo(actor, directions[3]);
27
                }
28
        }
29
        if (player.hp < 1) {
30
                // game over message

31
                var gameOver = game.add.text(game.world.centerX, game.world.centerY, 'Game Over\nCtrl+r to restart', { fill : '#e22', align: "center" } );
32
                gameOver.anchor.setTo(0.5,0.5);
33
        }
34
}

También hemos añadido un mensaje de juego, 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 nuestras funciones onKeyUp(), justo antes de dibujar a los actores en su nueva posición:

1
function onKeyUp(event) {
2
        ...
3
        // enemies act every time the player does

4
        if (acted)
5
                for (var enemy in actorList) {
6
                        // skip the player

7
                        if(enemy==0)
8
                                continue;
9
10
                        var e = actorList[enemy];
11
                        if (e != null)
12
                                aiAct(e);
13
                }
14
15
        // draw actors in new positions

16
        drawActors();
17
}
Click to view the game so far
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). A pesar de que he traducido la versión de arriba a mano para asegurarse de que obtener Javascript idiosincrásico, si, como yo, usted prefiere Haxe a JavaScript, puede encontrar la versión Haxe en la carpeta haxe de descarga de la fuente.

Primero debe instalar el compilador haxe y puede usar cualquier editor de texto que desee y compilar el código haxe llamando a haxe build.hxml o haciendo doble clic en el archivo build.hxml. También incluí un proyecto FlashDevelop si prefiere un IDE agradable a un editor de texto y una línea de comandos; Simplemente abra rl.hxproj y presione F5 para ejecutar.


Resumen

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

Estas son algunas ideas para las nuevas características que puede agregar a su juego:

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

¡A disfrutar!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.