Come rendere il tuo primo Roguelike

Di recente Roguelikes è stato sotto i riflettori, con giochi come Dungeons of Dredmor, Spelunky, The Binding of Isaac e FTL che hanno raggiunto un vasto pubblico e ricevuto consensi dalla critica. A lungo goduto da giocatori hardcore in una nicchia minuscola, elementi roguelike in varie combinazioni ora aiutano a portare più profondità e rigiocabilità a molti generi esistenti.


Wayfarer, un roguelike 3D attualmente in fase di sviluppo.

In questo tutorial, imparerai come creare un roguelike tradizionale usando JavaScript e il motore di gioco HTML 5 Phaser. Alla fine, avrai un semplice gioco roguelike completamente funzionale, giocabile nel tuo browser! (Per i nostri scopi un roguelike tradizionale è definito come un dungeon-crawler a turni single-player, a turni con permadeath.)


Clicca per giocare. Post correlati
  • Come imparare il motore di gioco Phaser HTML5

Nota: sebbene il codice in questo tutorial utilizzi JavaScript, HTML e Phaser, dovresti essere in grado di utilizzare la stessa tecnica e concetti in quasi tutti gli altri linguaggi di programmazione e motore di gioco.


Prepararsi

Per questo tutorial, avrai bisogno di un editor di testo e un browser. Uso Notepad ++ e preferisco Google Chrome per i suoi ampi strumenti di sviluppo, ma il flusso di lavoro sarà praticamente lo stesso con qualsiasi editor di testo e browser che scegli.

Dovresti quindi scaricare i file sorgente e iniziare con dentro cartella; questo contiene Phaser e i file HTML e JS di base per il nostro gioco. Scriveremo il nostro codice di gioco nell'attuale vuoto rl.js file.

Il index.html il file carica semplicemente Phaser e il nostro file di codice di gioco sopra menzionato:

  tutorial roguelike    

Inizializzazione e definizioni

Per il momento utilizzeremo la grafica ASCII per il nostro roguelike, in futuro potremmo sostituirli con grafica bitmap, ma per ora, l'utilizzo di ASCII semplifica le nostre vite.

Definiamo alcune costanti per la dimensione del carattere, le dimensioni della nostra mappa (cioè il livello) e quanti attori si generano in esso:

 // dimensione carattere var FONT = 32; // map dimensions var ROWS = 10; var COLS = 15; // numero di attori per livello, incluso giocatore var ATTORI = 10;

Inizializziamo anche Phaser e ascoltiamo gli eventi key-up della tastiera, poiché creeremo un gioco a turni e vorremmo agire una volta per ogni tratto chiave:

// initialize phaser, chiama create () una volta fatto var game = new Phaser.Game (COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, create: create); function create () // init comandi da tastiera 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:

Poiché i font monospace predefiniti tendono ad essere circa il 60% tanto ampi quanto alti, abbiamo inizializzato le dimensioni del canvas 0.6 * la dimensione del carattere * il numero di colonne. Stiamo anche dicendo a Phaser che dovrebbe chiamare il nostro creare() funzione immediatamente dopo aver terminato l'inizializzazione, a quel punto inizializziamo i controlli della tastiera.

Puoi vedere il gioco finora qui, non che ci sia molto da vedere!


La mappa

La mappa delle tessere rappresenta la nostra area di gioco: una matrice 2D discreta (al contrario di continua) di tessere, o celle, ciascuna rappresentata da un carattere ASCII che può significare un muro (#: blocchi movimento) o piano (.: non blocca il movimento):

 // la struttura della mappa var map;

Usiamo la forma più semplice di generazione procedurale per creare le nostre mappe: decidere casualmente quale cella deve contenere un muro e quale piano:

function initMap () // crea una nuova mappa mappa casuale = []; per (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); 
Post correlati
  • Come utilizzare gli alberi BSP per generare mappe di gioco
  • Generare livelli di cavità casuali usando gli automi cellulari

Questo dovrebbe darci una mappa in cui l'80% delle celle sono muri e il resto sono piani.

Inizializziamo la nuova mappa per il nostro gioco in creare() funzione, subito dopo aver impostato gli ascoltatori di eventi della tastiera:

function create () // init comandi da tastiera game.input.keyboard.addCallbacks (null, null, onKeyUp); // inizializza la mappa initMap (); 

Puoi vedere la demo qui, anche se, ancora una volta, non c'è nulla da vedere, dato che non abbiamo ancora reso la mappa.


Lo schermo

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

 // il display ascii, come un array di caratteri 2d var asciidisplay;

Disegnare la mappa riempirà il contenuto dello schermo con i valori della mappa, poiché entrambi sono semplici caratteri ASCII:

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

Infine, prima di disegnare la mappa, dobbiamo inizializzare lo schermo. Torniamo al nostro creare() funzione:

 function create () // init comandi da tastiera game.input.keyboard.addCallbacks (null, null, onKeyUp); // inizializza la mappa initMap (); // initialize screen asciidisplay = []; per (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); 

Ora dovresti vedere una mappa casuale visualizzata quando esegui il progetto.


Clicca per vedere il gioco finora.

attori

Seguono gli attori: il nostro personaggio giocatore e i nemici che devono sconfiggere. Ogni attore sarà un oggetto con tre campi: X e y per la sua posizione sulla mappa, e CV per i suoi punti ferita.

Manteniamo tutti gli attori nel actorList array (il primo elemento di cui è il giocatore). Manteniamo anche un array associativo con le posizioni degli attori come chiavi per la ricerca rapida, in modo che non dobbiamo scorrere l'intera lista degli attori per trovare quale attore occupa una determinata posizione; questo ci aiuterà quando codificheremo il movimento e il combattimento.

// un elenco di tutti gli attori; 0 è il giocatore var player; var actorList; var livingEnemies; // punta a ciascun attore nella sua posizione, per una ricerca rapida var actorMap;

Creiamo tutti i nostri attori e assegniamo a ciascuno una posizione libera casuale nella mappa:

function randomInt (max) return Math.floor (Math.random () * max);  function initActors () // crea attori in posizioni casuali actorList = []; actorMap = ; per (var e = 0; e 

È tempo di mostrare gli attori! Disegneremo tutti i nemici come e e il personaggio del giocatore come numero di hitpoint:

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

Usiamo le funzioni che abbiamo appena scritto per inizializzare e disegnare tutti gli attori nel nostro creare() funzione:

function create () ... // initialize actor initActors (); ... drawActors (); 

Ora possiamo vedere il nostro personaggio e i nostri nemici sparsi nel livello!


Clicca per vedere il gioco finora.

Piastrelle bloccabili e calpestabili

Dobbiamo assicurarci che i nostri attori non stiano correndo dallo schermo e attraverso i muri, quindi aggiungiamo questo semplice controllo per vedere in quale direzione un determinato attore può camminare:

funzione 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] == '.'; 

Movimento e combattimento

Siamo finalmente arrivati ​​a una certa interazione: movimento e combattimento! Poiché, nei classici roguelikes, l'attacco base viene attivato spostandosi in un altro attore, gestiamo entrambi nello stesso punto, il nostro moveTo () funzione, che prende un attore e una direzione (la direzione è la differenza desiderata in X e y alla posizione in cui l'attore interviene):

function moveTo (actor, dir) // controlla se l'attore può muoversi nella direzione data se (! canGo (actor, dir)) restituisce false; // sposta l'attore nella nuova posizione var newKey = (actor.y + dir.y) + '_' + (actor.x + dir.x); // se la tessera di destinazione ha un attore in essa se (actorMap [newKey]! = null) // decrementa gli hitpoint dell'attore nella casella di destinazione var victim = actorMap [newKey]; victim.hp--; // se è morto rimuovi il suo riferimento se (victim.hp == 0) actorMap [newKey] = null; actorList [actorList.indexOf (vittima)] = null; se (vittima! = giocatore) livingEnemies--; if (livingEnemies == 0) // messaggio di vittoria var victory = game.add.text (game.world.centerX, game.world.centerY, 'Victory! \ nCtrl + r per riavviare', fill: '# 2e2 ', allinea al centro"  ); victory.anchor.setTo (0.5,0.5);  else // rimuove il riferimento all'attuale posizione dell'attore attoreMap [actor.y + '_' + actor.x] = null; // aggiorna posizione actor.y + = dir.y; actor.x + = dir.x; // aggiungi un riferimento alla nuova posizione dell'attore actorMap [actor.y + '_' + actor.x] = actor;  return true; 

Fondamentalmente:

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

Nota che mostriamo anche un semplice messaggio di vittoria una volta che l'ultimo nemico è stato ucciso, e ritorna falso o vero a seconda che siamo riusciti a eseguire una mossa valida o meno.

Ora, torniamo al nostro onKeyUp () funzione e modificarlo in modo che, ogni volta che l'utente preme un tasto, cancelliamo le posizioni dell'attore precedente dallo schermo (disegnando la mappa in alto), spostiamo il personaggio del giocatore nella nuova posizione e quindi ridisegniamo gli attori:

function onKeyUp (evento) // disegna la mappa per sovrascrivere le precedenti posizioni degli attori drawMap (); // act on player input var acted = false; switch (event.keyCode) case Phaser.Keyboard.LEFT: acted = moveTo (player, x: -1, y: 0); rompere; caso Phaser.Keyboard.RIGHT: acted = moveTo (player, x: 1, y: 0); rompere; case Phaser.Keyboard.UP: acted = moveTo (player, x: 0, y: -1); rompere; case Phaser.Keyboard.DOWN: acted = moveTo (player, x: 0, y: 1); rompere;  // disegna attori in nuove posizioni drawActors (); 

Presto useremo il agito variabile per sapere se i nemici dovrebbero agire dopo ogni input del giocatore.


Clicca per vedere il gioco finora.

Basic Artificial Intelligence

Ora che il nostro personaggio giocatore si sta muovendo e sta attaccando, facciamo anche le probabilità facendo agire i nemici in base a un percorso molto semplice, purché il giocatore sia a sei o meno passi da loro. (Se il giocatore è più lontano, il nemico cammina casualmente).

Si noti che il nostro codice di attacco non si cura di chi sta attaccando l'attore; questo significa che, se li allinei correttamente, i nemici si attaccheranno l'un l'altro mentre cercano di inseguire il personaggio del giocatore, in stile Doom!

function aiAct (actor) 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; // se il giocatore è lontano, cammina casualmente se (Math.abs (dx) + Math.abs (dy)> 6) // prova a camminare in direzioni casuali finché non riesci una volta mentre (! moveTo (attore, direzioni [randomInt (directions.length)])) ; // altrimenti cammina verso il giocatore se (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);  

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

Ora tutto ciò che resta da fare è far agire i nemici ogni volta che il giocatore si muove, il che richiede l'aggiunta di quanto segue alla fine del nostro onKeyUp () funzioni, giusto prima di disegnare gli attori nella loro nuova posizione:

funzione onKeyUp (evento) ... // i nemici agiscono ogni volta che il giocatore fa (agito) per (var nemico in actorList) // salta il giocatore se (nemico == 0) continua; var e = actorList [nemico]; if (e! = null) aiAct (e);  // disegna attori in nuove posizioni drawActors (); 

Clicca per vedere il gioco finora.

Bonus: versione Haxe

Originariamente ho scritto questo tutorial in un Haxe, un grande linguaggio multi-piattaforma che compila in JavaScript (tra le altre lingue). Anche se ho tradotto la versione qui sopra a mano per assicurarmi di ottenere JavaScript idiosincratico, se, come me, preferisci Haxe a JavaScript, puoi trovare la versione di Haxe nel haxe cartella del download sorgente.

Devi prima installare il compilatore haxe e puoi usare qualsiasi editor di testo che desideri e compilare il codice haxe chiamando haxe build.hxml o facendo doppio clic sul build.hxml file. Ho anche incluso un progetto FlashDevelop se preferisci un bel IDE a un editor di testo e una riga di comando; semplicemente aperto rl.hxproj e premere F5 correre.


Sommario

Questo è tutto! Ora abbiamo un roguelike semplice e completo, con generazione di mappe casuali, movimento, combattimento, intelligenza artificiale e condizioni di vittoria e sconfitta.

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

  • più livelli
  • potenziamento
  • inventario
  • materiali di consumo
  • attrezzatura

Godere!