Come creare il vostro primo Roguelike
() 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.



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.)

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.

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!

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:
- Ci assicuriamo che l'attore stia cercando di entrare in una posizione valida.
- Se vi è un altro attore in quella posizione, attacchiamo (e uccidiamo, se il suo conteggio HP raggiunge 0).
- 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.

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 |
}
|

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!