Sunt sigur că este posibil să creați un joc Tetris cu un instrument gamedev point-and-click, dar nu am putut niciodată să-mi dau seama cum. Astăzi, mă gândesc mai confortabil la un nivel superior de abstractizare, unde tetromino-ul pe care îl vedeți pe ecran este doar un reprezentare a ceea ce se întâmplă în jocul de bază. În acest tutorial vă voi arăta ce vreau să spun, demonstrând cum să se ocupe de detectarea coliziunilor în Tetris.
Notă: Deși codul din acest tutorial este scris folosind AS3, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.
Un câmp standard de joc Tetris are 16 rânduri și 10 coloane. Putem reprezenta aceasta într-o matrice multidimensională, care conține 16 sub-tablouri de 10 elemente:
Imaginați-vă că imaginea din stânga este o captură de ecran din joc - este modul în care jocul ar putea privi la jucător, după ce un tetromino a aterizat, dar înainte de a fi lansat altul.
În partea dreaptă este o reprezentare matrice a stării actuale a jocului. Să spunem asta a aterizat[]
, deoarece se referă la toate blocurile care au aterizat. Un element de 0
înseamnă că nici un bloc nu ocupă acest spațiu; 1
înseamnă că un bloc a aterizat în acel spațiu.
Acum să hrănim un O-tetromino în centrul din partea superioară a câmpului:
tetromino.shape = [[1,1], [1,1]]; tetromino.topLeft = rând: 0, col: 4;
formă
proprietatea este o altă reprezentare multidimensională a matricei formei acestui tetromino. stânga sus
dă poziția blocului din stânga sus al tetromino-ului: la rândul de sus și a cincea coloană din.
Noi facem totul. În primul rând, desenați fundalul - acest lucru este ușor, este doar o imagine statică a rețelei.
Apoi, desenăm fiecare bloc din a aterizat[]
matrice:
pentru (var rând = 0; rând < landed.length; row++) for (var col = 0; col < landed[row].length; col++) if (landed[row][col] != 0) //draw block at position corresponding to row and col //remember, row gives y-position, col gives x-position
Imaginile blocului meu sunt de 20x20px, astfel încât să desenez blocurile pe care aș putea să le introduc doar la o nouă imagine bloc (col * 20, rând * 20)
. Detaliile nu contează.
Apoi, desenăm fiecare bloc din actualele tetromino:
pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) //draw block at position corresponding to //row + topLeft.row, and //col + topLeft.col
Putem folosi același cod de desen aici, dar trebuie să compensăm blocurile stânga sus
.
Iată rezultatul:
Rețineți că noul O-tetromino nu apare în a aterizat[]
array - asta pentru că, bine, nu a aterizat încă.
Să presupunem că playerul nu atinge comenzile. La intervale regulate - să spunem că fiecare jumătate de secundă - O-tetromino trebuie să coboare într-un rând.
Este tentant să sunați:
tetromino.topLeft.row ++;
... și apoi face totul din nou, dar acest lucru nu va detecta nici o suprapunere între O-tetromino și blocurile care au aterizat deja.
În schimb, vom verifica întâi coliziuni potențiale și apoi vom muta numai tetromino dacă este "în siguranță".
Pentru aceasta, va trebui să definim a potenţial noua poziție pentru tetromino:
tetromino.potentialTopLeft = rând: 1, col: 4;
Acum verificăm coliziuni. Cea mai simplă modalitate de a face acest lucru este de a trece prin toate spațiile din grilă pe care tetromino le-ar ocupa în noua poziție potențială și verificați a aterizat[]
pentru a vedea dacă sunt deja luate:
pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (landed[row + tetromino.potentialTopLeft.row] != 0 && landed[col + tetromino.potentialTopLeft.col] != 0) //the space is taken
Să încercăm acest lucru:
tetromino.shape = [[1,1], [1,1]]; tetromino.potentialTopLeft: rând: 1, col: 4 ------------------------------------- ------- rând: 0, col: 0, tetromino.shape [0] [0]: 1, a aterizat [0 + 1] [0 + 4]: 0 rând: 0; forma [0] [1]: 1, a aterizat [0 + 1] [1 + 4]: 0 rând: 1, col: 0, tetromino.shape [1] 0 + 4]: 0 rândul: 1, col: 1, tetromino.shape [1] [1]: 1, a aterizat [1 + 1] [1 + 4]: 0
Toate zerouri! Asta înseamnă că nu există o coliziune, așa că tetromino-ul se poate mișca.
Noi am stabilit:
tetromino.topLeft = tetromino.potentialTopLeft;
... și apoi redau totul:
Grozav!
Acum, să presupunem că jucătorul lasă tetromino să cadă la acest punct:
Stânga sus este la rând: 11, col: 4
. Putem vedea că tetromino-ul s-ar ciocni cu blocurile aterizate, dacă ar mai cădea - dar codul nostru ne dă seama? Sa vedem:
tetromino.shape = [[1,1], [1,1]]; tetromino.potentialTopLeft: rând: 12, col: 4 ------------------------------------- ------- rând: 0, col: 0, tetromino.shape [0] [0]: 1, a aterizat [0 + 12] [0 + 4]: 0 rând: 0, col: 1 tetromino. forma [0] [1]: 1, a aterizat [0 + 12] [1 + 4]: 0 rând: 1, col: 0, tetromino.shape [1] [1] 0 + 4]: 1 rând: 1, col: 1, tetromino.shape [1] [1]: 1, debarcat [1 + 12] [1 + 4]: 0
E a 1
, ceea ce înseamnă că există o coliziune - în special, tetromino-ul s-ar ciocni cu blocul la aterizat [13] [4]
.
Asta înseamnă că tetromino a aterizat, ceea ce înseamnă că trebuie să-l adăugăm la a aterizat[]
matrice. Putem face acest lucru cu o buclă foarte asemănătoare cu cea pe care am folosit-o pentru a verifica eventualele coliziuni:
pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) landed[row + tetromino.topLeft.row][col + tetromino.topLeft.col] = tetromino.shape[row][col];
Iată rezultatul:
Până acum, bine. Dar poate ați observat că nu avem de-a face cu cazul în care tetromino-ul se îndreaptă spre "pământ" - avem de-a face numai cu tetrominoși care se așează pe lângă alte tetrominoze.
Există o soluție destul de simplă pentru aceasta: când verificăm eventualele coliziuni, verificăm și dacă noua poziție potențială a fiecărui bloc ar fi sub fundul câmpului de joc:
pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (row + tetromino.potentialTopLeft.row >= landed.length) // acest bloc ar fi sub câmpul de joc altfel dacă (a aterizat [rând + tetromino.potentialTopLeft.row]! = 0 && a aterizat [col + tetromino.potentialTopLeft.col]! = 0) / spațiul este luat
Desigur, dacă un bloc din tetromino se va sfârși sub fundul terenului de joc, dacă ar cădea mai departe, vom face "terenul" tetromino, ca și cum orice bloc ar suprapune un bloc care deja a aterizat.
Acum putem începe următoarea rundă, cu un nou tetromino.
De data aceasta, să hrănim un J-tetromino:
tetromino.shape = [[0,1], [0,1], [1,1]]; tetromino.topLeft = rând: 0, col: 4;
Render it:
Amintiți-vă, la fiecare jumătate de secundă, tetromino va cădea cu un singur rând. Să presupunem că jucătorul lovește cheia din stânga de patru ori înainte să treacă o jumătate de secundă; vrem să mutăm tetromino-ul la stânga cu câte o coloană de fiecare dată.
Cum putem să ne asigurăm că tetromino-ul nu se va ciocni cu niciunul dintre blocurile debarcate? Putem folosi de fapt același cod de la început!
În primul rând, modificăm noua poziție potențială:
tetromino.potentialTopLeft = rând: tetromino.topLeft, col: tetromino.topLeft - 1;
Acum, verificăm dacă vreunul dintre blocurile din tetromino se suprapune cu blocurile așezați, folosind același control de bază ca înainte (fără a vă deranja să verificați dacă un bloc a trecut sub câmpul de joc):
pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (landed[row + tetromino.potentialTopLeft.row] != 0 && landed[col + tetromino.potentialTopLeft.col] != 0) //the space is taken
Rulați-l prin aceleași verificări pe care le efectuăm de obicei și veți vedea că acest lucru funcționează bine. Marea diferență este, trebuie să ne amintim nu pentru a adăuga blocurile tetromino la a aterizat[]
array dacă există o coliziune potențială - în schimb, nu ar trebui să schimbăm valoarea lui tetromino.topLeft
.
De fiecare dată când jucătorul mișcă tetromino-ul, ar trebui să redăm totul. Iată rezultatul final:
Ce se întâmplă dacă jucătorul lovește încă o dată? Când numim aceasta:
tetromino.potentialTopLeft = rând: tetromino.topLeft, col: tetromino.topLeft - 1;
... vom ajunge să încercăm să ne fixăm tetromino.potentialTopLeft.col
la -1
- și care va duce mai târziu la tot felul de probleme.
Să modificăm verificarea noastră de coliziune existentă pentru a face față acestei situații:
pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (col + tetromino.potentialTopLeft.col < 0) //this block would be to the left of the playing field if (landed[row + tetromino.potentialTopLeft.row] != 0 && landed[col + tetromino.potentialTopLeft.col] != 0) //the space is taken
Simplu - este aceeași idee ca atunci când verificăm dacă vreunul dintre blocuri ar cădea sub câmpul de joc.
Să ne ocupăm și de partea dreaptă:
pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (col + tetromino.potentialTopLeft.col < 0) //this block would be to the left of the playing field if (col + tetromino.potentialTopLeft.col >= landed [0] .length) // acest bloc ar fi în partea dreaptă a câmpului de joc dacă (a aterizat [row + tetromino.potentialTopLeft.row]! = 0 && a aterizat [col + tetromino.potentialTopLeft.col]! = 0) // spațiul este luat
Din nou, dacă tetromino-ul se va mișca în afara câmpului de joc, nu schimbăm nimic tetromino.topLeft
- nu este nevoie să faceți altceva.
Bine, o jumătate de secundă trebuie să fi trecut până acum, așa că să lăsăm tetromino să cadă un rând:
tetromino.shape = [[0,1], [0,1], [1,1]]; tetromino.topLeft = rând: 1, col: 0;
Acum, să presupunem că jucătorul lovește butonul pentru a face tetromino-ul să se rotească în sensul acelor de ceasornic. De fapt, acest lucru este destul de ușor de rezolvat - noi doar modificăm tetromino.shape
, fără a modifica tetromino.topLeft
:
tetromino.shape = [[1,0,0], [1,1,1]]; tetromino.topLeft = rând: 1, col: 0;
Noi ar putea utilizați unele matematici pentru a roti conținutul matricei de blocuri ... dar este mult mai simplu doar să stocați cele patru rotații posibile ale fiecărui tetromino undeva, după cum urmează:
jTetromino.rotations = [[0,1], [0,1], [1,1]], [[1,0,0], [1,1,1]], [[ [1,0], [1,0]], [[1,1,1], [0,0,1]]];
(Vă voi lăsa să vă dați seama unde să depozitați cel mai bine acest lucru în codul dvs.)
Oricum, odată ce vom reda totul, va arăta astfel:
Putem o roti din nou (și să presupunem că facem ambele rotații în jumătate de secundă):
tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rând: 1, col: 0;
Răsturnați din nou:
Minunat. Să lăsăm să cadă mai multe rânduri, până ajungem în această stare:
tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rând: 10, col: 0;
Deodată, jucătorul atinge din nou butonul Rotire în sens orar, fără nici un motiv evident. Putem vedea, din perspectiva imaginii, că acest lucru nu ar trebui să permită nimic să se întâmple, dar nu avem încă nicio verificare pentru a preveni acest lucru.
Probabil că puteți ghici cum vom rezolva acest lucru. Vom introduce a tetromino.potentialShape
, setați-o la forma tetromino-ului rotit și căutați eventuale suprapuneri cu blocuri care au deja aterizat.
tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rând: 10, col: 0; tetromino.potentialShape = [[1,1,1], [0,0,1]];
pentru (var rând = 0; rând < tetromino.potentialShape.length; row++) for (var col = 0; col < tetromino.potentialShape[row].length; col++) if (tetromino.potentialShape[row][col] != 0) if (col + tetromino.topLeft.col < 0) //this block would be to the left of the playing field if (col + tetromino.topLeft.col >= landed [0] .length) // acest bloc ar fi în dreapta câmpului de joc dacă (rând + tetromino.topLeft.row> = landed.length) // acest bloc ar fi sub câmpul de joc dacă [a aterizat [row + tetromino.topLeft.row]! = 0 && a aterizat [col + tetromino.topLeft.col]! = 0) // spațiul este luat
Dacă există o suprapunere (sau dacă forma rotită ar fi parțial în afara limitelor), pur și simplu nu lăsăm blocul să se rotească. Astfel, ea poate intra într-o jumătate de secundă mai târziu, și se adaugă la a aterizat[]
matrice:
Excelent.
Pentru a fi clar, avem acum trei verificări separate.
Primul control este când un tetromino cade și se numește la fiecare jumătate de secundă:
// set tetromino.potentialTopLeft a fi un rând sub tetromino.topLeft, apoi: pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (row + tetromino.potentialTopLeft.row >= landed.length) // acest bloc ar fi sub câmpul de joc altfel dacă (a aterizat [rând + tetromino.potentialTopLeft.row]! = 0 && a aterizat [col + tetromino.potentialTopLeft.col]! = 0) / spațiul este luat
Dacă toate cecurile trec, atunci ne-am fixat tetromino.topLeft
la tetromino.potentialTopLeft
.
Dacă oricare dintre verificări eșuează, atunci facem teren tetromino, așa cum este cazul:
pentru (var rând = 0; rând < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) landed[row + tetromino.topLeft.row][col + tetromino.topLeft.col] = tetromino.shape[row][col];
Cea de-a doua verificare se referă la momentul în care jucătorul încearcă să miște tetromino la stânga sau la dreapta și se cheamă când jucătorul atinge cheia de mișcare:
// set tetromino.potentialTopLeft a fi o coloană la dreapta sau la stânga // a tetromino.topLeft, după caz, apoi: pentru (var row = 0; row < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (col + tetromino.potentialTopLeft.col < 0) //this block would be to the left of the playing field if (col + tetromino.potentialTopLeft.col >= landed [0] .length) // acest bloc ar fi în partea dreaptă a câmpului de joc dacă (a aterizat [row + tetromino.potentialTopLeft.row]! = 0 && a aterizat [col + tetromino.potentialTopLeft.col]! = 0) // spațiul este luat
Dacă (și numai dacă) toate aceste verificări trec, am setat tetromino.topLeft
la tetromino.potentialTopLeft
.
Cea de-a treia verificare se întâmplă când jucătorul încearcă să rotească tetromino-ul în sensul acelor de ceasornic sau în sens invers acelor de ceasornic și se sună când jucătorul atinge cheia pentru a face acest lucru:
// set tetromino.potentialShape a fi versiunea rotativă a tetromino.shape // (în sensul acelor de ceasornic sau în sens antiorar, după caz), apoi: pentru (var rând = 0; rând < tetromino.potentialShape.length; row++) for (var col = 0; col < tetromino.potentialShape[row].length; col++) if (tetromino.potentialShape[row][col] != 0) if (col + tetromino.topLeft.col < 0) //this block would be to the left of the playing field if (col + tetromino.topLeft.col >= landed [0] .length) // acest bloc ar fi în dreapta câmpului de joc dacă (rând + tetromino.topLeft.row> = landed.length) // acest bloc ar fi sub câmpul de joc dacă [a aterizat [row + tetromino.topLeft.row]! = 0 && a aterizat [col + tetromino.topLeft.col]! = 0) // spațiul este luat
Dacă (și numai dacă) toate aceste verificări trec, am setat tetromino.shape
la tetromino.potentialShape
.
Comparați aceste trei verificări - este ușor să le amestecați, deoarece codul este foarte asemănător.
Până în prezent, am folosit diferite dimensiuni de matrice pentru reprezentarea diferitelor forme de tetrominoși (și rotații diferite ale acestor forme): O-tetromino a folosit o matrice 2x2, iar J-tetromino a folosit o matrice 3x2 sau 2x3.
Pentru consistență, recomand să folosiți aceeași matrice pentru toate tetrominoamele (și rotațiile acestora). Presupunând că țineți de cele șapte tetrominoase standard, puteți face acest lucru cu o matrice de 4x4.
Există mai multe modalități diferite de a organiza rotațiile în interiorul acestei pătraturi de 4x4; aruncați o privire la Wiki-ul Tetris pentru mai multe informații despre utilizarea diferitelor jocuri.
Să presupunem că reprezentați un tetromino vertical ca acesta:
[[0,1,0,0], [0,1,0,0], [0,1,0,0], [0,1,0,0]];
... și tu reprezinți rotația sa astfel:
[[0,0,0,0], [0,0,0,0], [1,1,1,1], [0,0,0,0]];
Acum presupuneți că un I-tetromino vertical este apăsat pe un perete de genul acesta:
Ce se întâmplă dacă jucătorul atinge tasta Rotire?
Folosind codul actual de detectare a coliziunilor, nu se întâmplă nimic - blocul din stânga al tetromino-ului orizontal I ar fi în afara limitelor.
Acest lucru este, fără îndoială, bine - așa funcționează în versiunea NES a lui Tetris - dar există o alternativă: rotiți tetromino-ul și mutați-l odată cu spațiul spre dreapta, după cum urmează:
Vă voi lăsa să vă dați seama de detaliile, dar, în esență, trebuie să verificați dacă rotirea tetromino-ului o va mișca din limite și, dacă este așa, mutați-l în stânga sau în dreapta unul sau două spații, după cum este necesar. Cu toate acestea, trebuie să vă amintiți că ați verificat eventualele coliziuni cu alte blocuri după aplicarea rotației și miscarea!
Am folosit blocuri de aceeași culoare în tot acest tutorial pentru a păstra lucrurile simple, dar este ușor să schimbi culorile.
Pentru fiecare culoare, alegeți un număr care să o reprezinte; utilizați acele numere în dvs. formă[]
și a aterizat[]
matrice; apoi modificați codul de redare la blocurile de culori pe baza numerelor acestora.
Rezultatul ar putea arăta astfel:
Separarea reprezentării vizuale a unui obiect în joc de la datele sale este un concept foarte important de înțeles; el vine din nou și din nou în alte jocuri, în special când se ocupă de detectarea coliziunilor.
În următorul post, vom examina modul de implementare a altei caracteristici principale a Tetris: eliminarea liniilor atunci când sunt completate. Vă mulțumim pentru lectură!