Cum să faci primul tău Roguelike

Roguelikes sa aflat recent în centrul atenției, cu jocuri cum ar fi Dungeons of Dredmor, Spelunky, The Binding of Isaac și FTL care au atins publicul larg și au primit recunoștință critică. Îmbunătățită de jucătorii hardcore într-o nișă mică, elementele roguelike în diferite combinații ajută acum să aducă mai multă profunzime și replayability la multe genuri existente.


Wayfarer, un roguelike 3D în prezent în curs de dezvoltare.

În acest tutorial, veți învăța cum să faceți un roguelike tradițional folosind JavaScript și motorul de joc HTML 5 Phaser. Până la sfârșit, veți avea un joc simplu, roguelike simplu, care poate fi redat în browserul dvs.! (Pentru scopurile noastre, un roguelike tradițional este definit ca un crainic cu un permadeat cu un singur jucător, randomizat, pe bază de turnuri).)


Faceți clic pentru a juca acest joc. postări asemănatoare
  • Cum să învățați motorul de jocuri Phaser HTML5

Notă: Deși codul din acest tutorial utilizează JavaScript, HTML și Phaser, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice alt limbaj de codare și motor de joc.


Se pregateste

Pentru acest tutorial, veți avea nevoie de un editor de text și de un browser. Folosesc Notepad ++ și prefer Google Chrome pentru instrumentele sale extensibile de dezvoltatori, dar fluxul de lucru va fi aproape același cu orice editor de text și browser pe care îl alegeți.

Ar trebui să descărcați fișierele sursă și să începeți cu init pliant; aceasta conține Phaser și fișierele de bază HTML și JS pentru jocul nostru. Vom scrie codul nostru de joc în gol rl.js fişier.

index.html fișierul încarcă pur și simplu Phaser și fișierul nostru de cod de joc menționat anterior:

  roguelike tutorial    

Inițializare și definiții

Pentru moment, vom folosi grafice ASCII pentru roguelike - în viitor, le putem înlocui cu grafice bitmap, dar pentru moment, folosirea ASCII simplu face viața mai ușoară.

Să definim câteva constante pentru mărimea fontului, dimensiunile hărții noastre (adică, nivelul) și câți actori se aruncă în ea:

 // dimensiune font var FONT = 32; // hartă dimensiuni var ROWS = 10; var COLS = 15; // numărul de actori pe nivel, inclusiv playerul ACTORS = 10;

Să începem, de asemenea, Phaser și să ascultăm evenimentele tastaturii tastaturii, deoarece vom crea un joc bazat pe întoarcere și vom dori să acționăm o singură dată pentru fiecare accident cheie:

// inițializați phaser, apelați crea () o dată terminat var joc = nou Phaser.Game (COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, create: create); create () // comenzi pentru tastatură init game.input.keyboard.addCallbacks (null, null, onKeyUp);  funcția onKeyUp (eveniment) comutator (event.keyCode) caz Keyboard.LEFT: caz Keyboard.RIGHT: caz Keyboard.UP: case Keyboard.DOWN:

Deoarece fonturile monospace implicite tind să fie aproximativ 60% la fel de largi ca acestea sunt mari, am inițializat dimensiunea pânzei pentru a fi 0.6 * dimensiunea fontului * numărul de coloane. Spunem, de asemenea, lui Phaser că ar trebui să ne cheme crea() funcționează imediat după terminarea inițializării, moment în care inițializăm comenzile de la tastatură.

Puteți vedea jocul până acum aici - nu că există multe de văzut!


Harta

Harta țiglelor reprezintă aria noastră de joc: o matrice discretă (spre deosebire de continuă) de 2D de dale sau celule, fiecare reprezentată de un caracter ASCII care poate semnifica fie un perete (#: mișcări de blocuri) sau podea (.: nu blochează mișcarea):

 // structura hărții hartă var;

Să folosim cea mai simplă formă de generare procedurală pentru a crea hărțile noastre: decideți aleatoriu ce celula trebuie să conțină un perete și care are un podea:

funcția initMap () // a crea o nouă hartă aleatoare a hărții = []; pentru (var y = 0; y < ROWS; y++)  var newRow = []; for (var x = 0; x < COLS; x++)  if (Math.random() > 0.8) newRow.push ('#'); altceva newRow.push ('.');  map.push (newRow); 
postări asemănatoare
  • Cum să utilizați copaci BSP pentru a genera hărți de jocuri
  • Generați nivele aleatoare de peșteri utilizând automatele celulare

Acest lucru ar trebui să ne dea o hartă unde 80% din celule sunt pereți, iar restul sunt podele.

Inițializăm noua hartă pentru jocul nostru în crea() , imediat după configurarea ascultătorilor evenimentului tastaturii:

create () // comenzi pentru tastatură init game.input.keyboard.addCallbacks (null, null, onKeyUp); // inițializați harta initMap (); 

Puteți vedea demo-ul aici - deși, din nou, nu este nimic de văzut, deoarece nu am redat harta încă.


Monitorul

E timpul să ne atragem harta! Ecranul nostru va fi o matrice 2D de elemente de text, fiecare conținând un singur caracter:

 // afișarea ascii, ca o matrice 2d de caractere var asciidisplay;

Desenarea hărții va completa conținutul ecranului cu valorile hărții, deoarece ambele sunt caractere simple ASCII:

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

În cele din urmă, înainte de a extrage harta, trebuie să inițializăm ecranul. Ne întoarcem la noi crea() funcţie:

 create () // comenzi pentru tastatură init game.input.keyboard.addCallbacks (null, null, onKeyUp); // inițializați harta initMap (); // inițializați ecranul asciidisplay = []; pentru (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); 

Ar trebui să vedeți acum o hartă aleatorie afișată atunci când executați proiectul.


Faceți clic pentru a vedea jocul până acum.

actori

Următoarele sunt actorii: caracterul jucătorului nostru și dușmanii pe care trebuie să îi învingă. Fiecare actor va fi un obiect cu trei câmpuri: X și y pentru locația sa în hartă și CP pentru punctele sale de lovit.

Păstrăm toți actorii din actorList matrice (primul element al căruia este playerul). De asemenea, păstrăm o matrice asociativă cu locațiile actorilor ca chei pentru căutarea rapidă, astfel încât să nu trebuie să repetăm ​​întreaga listă de actori pentru a afla care actor ocupă o anumită locație; acest lucru ne va ajuta atunci când vom codifica mișcarea și lupta.

// o listă a tuturor actorilor; 0 este playerul var player; var actorList; var livingEnemies; // indică fiecărui actor în poziția sa, pentru căutarea rapidă a actorului;

Creați toți actorii noștri și alocați o poziție liberă aleatorie în hartă fiecăruia:

funcția randomInt (max) retur Math.floor (Math.random () * max);  funcția initActors () // a crea actori în locații aleatorii actorList = []; actorMap = ; pentru (var e = 0; 

E timpul să-i arăți actorilor! Vom atrage toți dușmanii e și caracterul jucătorului ca număr de puncte hit-uri:

funcția drawActors () pentru (var a în actorList) if (actorList [a] .hp> 0) asciidisplay [actorList [a] .y] [actorList [a] .x] .content = a == 0? + player.hp: "e";

Folosim funcțiile pe care tocmai le-am scris pentru a inițializa și atrage toți actorii din noi crea() funcţie:

create () ... // inițializa actorii initActori (); ... drawActors (); 

Acum putem vedea caracterul jucătorului nostru și dușmanii răspândiți în acest nivel!


Faceți clic pentru a vedea jocul până acum.

Blocarea și dale de tabla

Trebuie să ne asigurăm că actorii noștri nu sunt difuzați de pe ecran și prin pereți, deci să adăugăm acest simplu control pentru a vedea în ce direcții un anumit actor poate merge:

funcția canGo (actor, dir) retur 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] == '.'; 

Mișcare și luptă

Am ajuns în sfârșit la o anumită interacțiune: mișcare și luptă! Deoarece, în roguelikes clasic, atacul de bază este declanșat prin mutarea într-un alt actor, ne ocupăm de ambele în același loc, MoveTo () funcția, care ia un actor și o direcție (direcția este diferența dorită în X și y în poziția în care actorul intră):

funcția moveTo (actor, dir) // verificați dacă actorul se poate deplasa în direcția dată dacă (! canGo (actor, dir)) return false; // muta actorul la noua locație var newKey = (actor.y + dir.y) + '_' + (actor.x + dir.x); // dacă placa de destinație are un actor în ea dacă (actorMap [newKey]! = null) // decrement hitpoints ale actorului de la destinația var victim = actorMap [newKey]; victim.hp--; // dacă este mort, eliminați referința dacă (victim.hp == 0) actorMap [newKey] = null; actorList [actorList.indexOf (victimă)] = null; dacă (victim! = player) livingEnemies--; dacă livingEnemies == 0) // mesajul victory var victory = game.add.text (game.world.centerX, game.world.centerY, 'Victory! \ nCtrl + r pentru a reporni', fill: '# 2e2 ', Aliniere la centru"  ); victory.anchor.setTo (0.5,0.5);  altceva // eliminați referința la poziția veche actorului actorMap [actor.y + '_' + actor.x] = null; // actualizați poziția actor.y + = dir.y; actor.x + = dir.x; // adăugați o referință la noua poziție actorului actorMap [actor.y + '_' + actor.x] = actor;  return true; 

Pe scurt:

  1. Asigurăm că actorul încearcă să se mute într-o poziție valabilă.
  2. Dacă există un alt actor în acea poziție, îl atacăm (și o ucidem dacă numărul său de HP ajunge la 0).
  3. Dacă nu există un alt actor în noua poziție, ne mutăm acolo.

Observați că prezentăm un mesaj simplu de victorie odată ce ultimul dușman a fost ucis și sa întors fals sau Adevărat în funcție de faptul dacă am reușit sau nu să realizăm o mutare validă.

Acum, să ne întoarcem la noi onkeyup () funcția și modificați-o astfel încât, de fiecare dată când utilizatorul apasă o cheie, ștergem pozițiile actorului anterior de pe ecran (prin desenarea hărții deasupra), mutăm caracterul jucătorului în noua locație și apoi reproiectăm actorii:

funcția onKeyUp (eveniment) // trageți hartă pentru a suprascrie pozițiile anterioare anterioare drawMap (); // acționa pe intrarea jucătorului var acted = false; comutator (event.keyCode) caz Phaser.Keyboard.LEFT: acted = moveTo (player, x: -1, y: 0); pauză; cazul Phaser.Keyboard.RIGHT: acted = moveTo (player, x: 1, y: 0); pauză; caz Phaser.Keyboard.UP: acted = moveTo (player, x: 0, y: -1); pauză; cazul Phaser.Keyboard.DOWN: acted = moveTo (player, x: 0, y: 1); pauză;  / / trageți actorii în poziții noi drawActors (); 

Vom folosi în curând a acționat variabilă pentru a ști dacă dușmanii ar trebui să acționeze după fiecare intrare a jucătorului.


Faceți clic pentru a vedea jocul până acum.

Inteligenta artificiala de baza

Acum, că caracterul jucătorului nostru se mișcă și atacă, să lăsăm chiar și cotele, făcând dușmanii să acționeze în conformitate cu o simplă cărare a traseului, atâta timp cât jucătorul are șase pași sau mai puțin de la ei. (Dacă jucătorul este mai departe, dușmanul umblă la întâmplare.)

Observați că codul nostru de atac nu-i pasă cine atacă actorul; acest lucru înseamnă că, dacă le aliniați la dreapta, dușmanii se vor ataca unul pe altul în timp ce încearcă să urmărească caracterul jucătorului, Doom-style!

(x: 0, y: 1), x: 0, y: 1 (actor) var = ]; var dx = player.x - actor.x; var dy = player.y - actor.y; // dacă jucătorul este departe, mergeți aleatoriu dacă (Math.abs (dx) + Math.abs (dy)> 6) // încercați să umblați în direcții aleatorii până când reușiți o dată în timp ce (! moveTo (actor, directions [randomInt (directions.length)])) ; // merge altfel spre jucator daca (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);  

Am adăugat, de asemenea, un joc pe mesaj, care este afișat dacă unul dintre dușmani ucide playerul.

Acum tot ce trebuie să faci este să faci dușmanii să acționeze de fiecare dată când jucătorul se mișcă, ceea ce presupune adăugarea următorului la sfârșitul onkeyup () înainte de a desena actorii în noua lor poziție:

function onKeyUp (eveniment) ... // inamicii actioneaza de fiecare data cand jucatorul face daca (a actionat) pentru (var inamic in actorList) // sari peste player daca (inamicul == 0) continua; var e = actorList [inamic]; dacă (e! = nulă) aiAct (e);  / / trageți actorii în poziții noi drawActors (); 

Faceți clic pentru a vedea jocul până acum.

Bonus: versiunea Haxe

Am scris inițial acest tutorial într-un limbaj Haxe, un mare limbaj multi-platformă care se compilează în JavaScript (printre alte limbi). Deși am tradus versiunea de mai sus cu mâna pentru a ne asigura că avem JavaScript idiosincratic, dacă, ca mine, preferați Haxe pentru JavaScript, puteți găsi versiunea Haxe în haXe folderul sursei de descărcare.

Trebuie să instalați mai întâi compilatorul haxe și să utilizați orice editor de text dorit și să compilați codul haxe apelând haxe build.hxml sau făcând dublu clic pe build.hxml fişier. De asemenea, am inclus un proiect FlashDevelop dacă preferați un IDE frumos într-un editor de text și linie de comandă; doar deschis rl.hxproj și apăsați F5 a alerga.


rezumat

Asta e! Acum avem un simplu roguelike simplu, cu generarea aleatoare a hărților, mișcarea, lupta, AI și ambele câștig și pierde condiții.

Iată câteva idei pentru funcțiile noi pe care le puteți adăuga la joc:

  • niveluri multiple
  • Power ups
  • inventar
  • consumabile
  • echipament

se bucura!