Advertisement
  1. Game Development
  2. Roguelike

Come creare il vostro primo Roguelike

Scroll to top
Read Time: 13 min

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

I giochi Roguelikes di recente sono stati sotto i riflettori, con giochi come Dungeons of Dredmor, Spelunky, The Binding of Isaac ed FTL che sta raggiungendo un vasto pubblico e ricevendo il plauso della critica. Dopo aver a lungo divertito una piccola nicchia di giocatori incalliti, gli elementi dei roguelike in varie combinazioni ora contribuiscono a portare maggiore profondità e rigiocabilità a molti generi esistenti.

Wayfarer a 3D roguelike currently in developmentWayfarer a 3D roguelike currently in developmentWayfarer a 3D roguelike currently in development
Wayfarer, un roguelike 3D attualmente in fase di sviluppo.

In questo tutorial, imparerete a fare un roguelike tradizionale utilizzando JavaScript ed il motore di gioco HTML5 Phaser. Alla fine, avrete un semplice gioco roguelike completamente funzionante, giocabile nel vostro browser! (Per i nostri scopi un roguelike tradizionale è definito come un single-player, randomizzato, a turni dungeon-crawler con permadeath.)

Click to play the game
Clicca per giocare.

Nota: Anche se il codice in questo tutorial utilizza JavaScript, HTML e Phaser, si dovrebbe essere in grado di utilizzare la stessa tecnica e concetti in quasi qualsiasi altro linguaggio di programmazione e motore di gioco.


Prepararsi

Per questo tutorial, avrete bisogno di un editor di testo e di un browser. Io uso Notepad++ e preferisco Google Chrome per i suoi ampi strumenti di sviluppo, ma il flusso di lavoro sarà più o meno lo stesso con qualsiasi editor di testo e browser scegliate.

Si dovrebbe quindi scaricare i sorgenti ed iniziare con la cartella init; questa contiene i file Phaser, HTML e JS di base per il nostro gioco. Scriveremo il nostro codice di gioco nel file rl.js per ora vuoto.

Il file index.html semplicemente carica Phaser ed il nostro file precedente col codice del gioco:

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>

Inizializzazione e definizioni

Per il momento, useremo la grafica ASCII per il nostro roguelike, in futuro potremmo sostituirla con grafica bitmap, ma per ora, utilizzando semplici ASCII avremo vita facile.

Definiamo alcune costanti per la dimensione del carattere, le dimensioni della nostra mappa (ovvero, il livello), e quanti personaggi si riprodurranno al suo interno:

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;

Avviamo anche l'inizializzare di Phaser e l'attesa per eventi della tastiera key-up (rilascio del tasto), creando un gioco a turni vogliamo agire una volta per ogni tasto premuto:

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
}

Dal momento che i font a spaziatura fissa predefinita tendono ad essere circa il 60% più larghi rispetto alla loro altezza, abbiamo inizializzato le dimensioni del canvas a 0,6 * la dimensione del carattere * il numero di colonne. Stiamo anche dicendo a Phaser che dovrebbe richiamare la nostra funzione di create() immediatamente dopo che è finita l'inizializzazione, a quel punto inizializziamo i controlli della tastiera.

È possibile visualizzare il gioco fino a qui, non che ci sia molto da vedere!


La Mappa

La tile map rappresenta la nostra area di gioco: una discreta (al contrario di continuo) gamma di piastrelle 2D, o celle, ognuna rappresentata da un carattere ASCII che può significare sia una parete (#: blocca il movimento) o un pavimento (.: non blocca il movimento):

1
        // the structure of the map

2
        var map;

Usiamo la forma più semplice di generazione procedurale per creare le nostre mappe: decidere a caso quale cella dovrebbe contenere un muro, e quale un pavimento:

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
}

Questo ci dovrebbe dare una mappa in cui l'80% delle celle sono pareti ed il resto pavimento.

Inizializziamo la nuova mappa per il nostro gioco nella funzione create(), subito dopo aver impostato i listener di eventi della tastiera:

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

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

6
        initMap();
7
}

È possibile visualizzare la demo qui, anche se, ancora una volta, non c'è niente da vedere, visto che ancora non abbiamo il rendering della mappa.


Lo Schermo

È il momento di disegnare la nostra mappa! Il nostro schermo sarà una matrice 2D di elementi di testo, ciascuno contenente un singolo carattere:

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

2
        var asciidisplay;

Disegnare la mappa comporta riempire lo schermo con i valori contenuti nella mappa, poiché entrambi sono semplici caratteri 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
        }

Infine, prima di disegnare la mappa dobbiamo inizializzare lo schermo. Torniamo alla nostra funzione 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
        }

Ora dovreste vedere una mappa casuale visualizzata quando si esegue il progetto.

Click to view the game so far
Clicca per vedere il gioco fino a qui.

Attori

Dopo in linea ci sono gli attori: il nostro personaggio, ed i nemici da sconfiggere. Ogni attore sarà un oggetto con tre campi: x e y per la sua posizione nella mappa, e hp per i suoi punti ferita.

Manteniamo tutti gli attori nella matrice actorList (di cui il primo elemento è il giocatore). Manteniamo anche un array associativo con le posizioni degli attori come chiave per la ricerca rapida, in modo da non dover iterare l'intero elenco degli attori per trovare quale attore occupa una certa posizione; questo ci aiuterà per il codice movimento e combattimento.

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;

Creiamo tutti i nostri attori e per ciascuno assegnamogli una posizione libera casuale nella mappa:

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
}

E' il momento di mostrare gli attori! Stiamo per disegnare tutti i nemici come una e ed il personaggio del giocatore, come il suo numero di punti ferita:

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
}

Facciamo uso delle funzioni che abbiamo appena scritto per inizializzare e disegnare tutti gli attori nella nostra funzione create():

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

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

Ora possiamo vedere il carattere del nostro giocatore ed nemici sparsi nel livello!

Click to view the game so far
Clicca per vedere il gioco fino a qui.

Tiles di blocco e calpestabili

Dobbiamo fare in modo che i nostri attori non corrano fuori dallo schermo e attraverso i muri, quindi cerchiamo di aggiungere questo semplice controllo per vedere in quali direzioni un determinato attore può camminare:

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
}

Movimento e combattimento

Siamo finalmente arrivati a una certa interazione: movimento e combattimento! Dal momento che, nei roguelikes classici, l'attacco di base è innescato muovendosi dentro un altro attore, gestiamoli entrambi nello stesso punto, la nostra funzione moveTo(), che prende un attore e una direzione (la direzione è la differenza desiderata in x e y per la posizione dove l'attore sta camminando):

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
}

In sostanza:

  1. Ci assicuriamo che l'attore stia cercando di entrare in una posizione valida.
  2. Se vi è un altro attore in quella posizione, attacchiamo (e uccidiamo, se il suo conteggio HP raggiunge 0).
  3. Se non c'è un altro attore nella nuova posizione, ci spostiamo lì.

Si noti che mostriamo anche un semplice messaggio di vittoria una volta che l'ultimo nemico è stato ucciso e torniamo false o true a seconda se non siamo riusciti ad eseguire una mossa valida.

Ora, torniamo alla nostra funzione onKeyUp() e modifichiamola in modo che, ogni volta che l'utente preme un tasto, cancelliamo la posizione precedente dell'attore dallo schermo (disegnandoci sopra la mappa), spostiamo il personaggio del giocatore nella nuova posizione e quindi ridisegnamo gli attori:

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
}

Presto useremo la variabile acted per sapere se i nemici debbano agire ad ogni ingresso del giocatore.

Click to view the game so far
Clicca per vedere il gioco fino a qui.

Intelligenza Artificiale di base

Ora che il nostro personaggio si muove ed attacca, diamo anche la probabilità che i nemici agiscano seguendo un semplice percorso di ricerca fino a quando il giocatore dista sei passi o meno da loro. (Se il giocatore è più lontano, il nemico passeggia in modo casuale.)

Si noti che il nostro codice di attacco non si preoccupa che l'attore stia attaccando; questo significa che, se l'allineamento è giusto, i nemici si attaccano a vicenda nel tentativo di inseguire il personaggio del giocatore, in stile 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
}

Abbiamo anche aggiunto un messaggio di fine gioco, che è mostrato se uno dei nemici uccide il giocatore.

Ora tutto quello che resta da fare è rendere i nemici capaci di agire ogni volta che il giocatore si muove, il che richiede l'aggiunta di quanto segue alla fine della nostra funzione onKeyUp() , giusto prima di disegnare gli attori nella loro nuova posizione:

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
Clicca per vedere il gioco fino qui.

Bonus: Versione Haxe

Originariamente ho scritto questo tutorial in un Haxe, un grande linguaggio multi-piattaforma che compila in JavaScript (oltre altri linguaggi). Anche se ho tradotto la versione sopra a mano per assicurarmi una idiosincrasia con il JavaScript, se, come me, preferite Haxe al JavaScript, è possibile trovare la versione Haxe nella cartella haxe nel download dei sorgenti.

È necessario installare prima il compilatore haxe ed è possibile utilizzare qualsiasi editor di testo desiderato, compilare il codice haxe chiamando build.hxml o fare doppio clic sul file build.hxml. Ho anche incluso un progetto FlashDevelop se si preferisce un bel IDE ad un editor di testo e la linea di comando; aprite solo rl.hxproj e premete F5 per eseguire.


Sommario

Questo è tutto! Ora abbiamo una roguelike semplice e completo, con generazione casuale della mappa, il movimento, il combattimento, AI e tutte le condizioni per vincere o perdere.

Ecco alcune idee per nuove funzionalità che è possibile aggiungere al tuo gioco:

  • Livelli multipli
  • Potenziamenti
  • Inventario
  • Materiali di consumo
  • Equipaggiamento

Divertitevi!

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.