Gjennomføring av Tetris Kollisjonsdeteksjon

Jeg er sikker på at det er mulig å lage et Tetris-spill med et punkt-og-klikk-gamedev-verktøy, men jeg kunne aldri finne ut hvordan. I dag er jeg mer komfortabel å tenke på et høyere nivå av abstraksjon, hvor tetrominoen du ser på skjermen, er bare en representasjon av hva som foregår i det underliggende spillet. I denne veiledningen vil jeg vise deg hva jeg mener, ved å demonstrere hvordan man håndterer kollisjonsdeteksjon i Tetris.

Merk: Selv om koden i denne opplæringen er skrevet ved hjelp av AS3, bør du kunne bruke de samme teknikkene og begrepene i nesten hvilket som helst spillutviklingsmiljø.


Gitteret

Et standard Tetris-spillerfelt har 16 rader og 10 kolonner. Vi kan representere dette i et flerdimensjonalt utvalg, som inneholder 16 delrapporter på 10 elementer:


Grafikk fra denne flotte Vectortuts + opplæringen.

Tenk på bildet til venstre er et skjermbilde fra spillet - det er hvordan spillet kan se til spilleren, etter at en tetromino har landet, men før en annen har blitt spunnet.

Til høyre er en representasjon av spillets nåværende tilstand. La oss kalle det landet [], som det refererer til alle blokkene som har landet. Et element av 0 betyr at ingen blokk opptar dette rommet; 1 betyr at en blokk har landet i det rommet.

La oss gyte en O-tetromino i midten på toppen av feltet:

 tetromino.shape = [[1,1], [1,1]]; tetromino.topLeft = rad: 0, col: 4;

De form eiendom er en annen flerdimensjonal array representasjon av formen av denne tetromino. øverst til venstre gir posisjonen til den øverste venstre tverrsnittet av tetromino: i øverste rad, og den femte kolonnen i.

Vi gjengir alt. Først tegner vi bakgrunnen - dette er enkelt, det er bare et statisk rutenettbilde.

Deretter tegner vi hver blokk fra landet [] matrise:

 for (var row = 0; rad < 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   

Mine blokkbilder er 20x20px, så for å tegne blokkene kan jeg bare sette inn et nytt blokkbilde på (kol * 20, rad * 20). Detaljerene spiller ingen rolle.

Deretter tegner vi hver blokk i gjeldende tetromino:

 for (var row = 0; rad < 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   

Vi kan bruke samme tegningskode her, men vi må kompensere blokkene av øverst til venstre.

Her er resultatet:

Merk at den nye O-tetromino ikke vises i landet [] array - det er fordi, vel, det har ikke landet ennå.


Falling

Anta at spilleren ikke berører kontrollene. Med jevne mellomrom - la oss si hvert halve sekund - O-tetromino må falle ned en rad.

Det er fristende å bare ringe:

 tetromino.topLeft.row ++;

... og deretter gjengis alt igjen, men dette vil ikke oppdage overlapper mellom O-tetromino og blokkene som allerede har landet.

I stedet vil vi se etter potensielle kollisjoner først, og flytt bare tetrominoen hvis den er "trygg".

For dette må vi definere en potensial Ny posisjon for tetromino:

 tetromino.potentialTopLeft = rad: 1, kol: 4;

Nå ser vi etter kollisjoner. Den enkleste måten å gjøre dette på er å løpe gjennom alle rom i rutenettet som tetromino ville ta opp i sin potensielle nye posisjon, og se på landet [] array for å se om de allerede er tatt:

 for (var row = 0; rad < 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    

La oss teste dette ut:

 tetromino.shape = [[1,1], [1,1]]; tetromino.potentialTopLeft: rad: 1, kol: 4 ------------------------------------- ------- rad: 0, col: 0, tetromino.shape [0] [0]: 1, landet [0 + 1] [0 + 4]: 0 rad: 0, col: 1, tetromino. form [0] [1]: 1, landet [0 + 1] [1 + 4]: 0 rad: 1, col: 0, tetromino.shape [1] [0]: 1, landet [1 + 1] [ 0 + 4]: 0 rad: 1, kol: 1, tetromino.shape [1] [1]: 1, landet [1 + 1] [1 + 4]: 0

Alle nuller! Dette betyr at det ikke er kollisjon, så tetromino kan bevege seg.

Vi setter:

 tetromino.topLeft = tetromino.potentialTopLeft;

... og deretter gjengi alt igjen:

Flott!


Landing

Anta nå at spilleren lar tetrominoen falle til dette punktet:

Øverst til venstre er på rad: 11, kol: 4. Vi kan se at tetromino ville kollidere med de landte blokkene hvis det falt mer - men viser vår kode det ut? La oss se:

 tetromino.shape = [[1,1], [1,1]]; tetromino.potentialTopLeft: rad: 12, kol: 4 ------------------------------------- ------- rad: 0, col: 0, tetromino.shape [0] [0]: 1, landet [0 + 12] [0 + 4]: 0 rad: 0, col: 1, tetromino. form [0] [1]: 1, landet [0 + 12] [1 + 4]: 0 rad: 1, kol: 0, tetromino.shape [1] [0]: 1, landet [1 + 12] [ 0 + 4]: 1 rad: 1, kol: 1, tetromino.shape [1] [1]: 1, landet [1 + 12] [1 + 4]: 0

Det er en 1, noe som betyr at det er en kollisjon - spesielt, ville tetromino kollidere med blokken på landet [13] [4].

Dette betyr at tetromino har landet, noe som betyr at vi må legge det til landet [] array. Vi kan gjøre dette med en veldig lik sløyfe til den vi pleide å sjekke for potensielle kollisjoner:

 for (var row = 0; rad < 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];   

Her er resultatet:

Så langt så bra. Men du har kanskje lagt merke til at vi ikke håndterer saken der tetromino lander på "bakken" - vi tar bare opp med tetrominoer landing på toppen av andre tetrominoer.

Det er en ganske enkel løsning for dette: Når vi ser etter potensielle kollisjoner, kontrollerer vi også om den potensielle nye posisjonen til hver blokk vil være under bunnen av spillefeltet:

 for (var row = 0; rad < 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) // denne blokken vil være under spillfeltet ellers hvis (landet [rad + tetromino.potentialTopLeft.row]! = 0 && landet [col + tetromino.potentialTopLeft.col]! = 0) / / plassen er tatt

Selvfølgelig, hvis noen blokk i tetromino ville ende opp under bunnen av spillerom hvis den falt lenger, gjør vi tetromino "land", akkurat som om en blokk ville overlappe en blokk som allerede hadde landet.

Nå kan vi starte neste runde, med en ny tetromino.


Flytter og roterer

Denne gangen, la oss gyte en J-tetromino:

 tetromino.shape = [[0,1], [0,1], [1,1]]; tetromino.topLeft = rad: 0, col: 4;

Gi det ut:

Husk hvert tiende sekund, at tetromino kommer til å falle med en rad. La oss anta at spilleren treffer venstre-tasten fire ganger før et halvt sekund går; Vi ønsker å flytte tetromino igjen av en kolonne hver gang.

Hvordan kan vi sørge for at tetromino ikke kolliderer med noen av de landede blokkene? Vi kan faktisk bruke samme kode fra før!

Først endrer vi den potensielle nye posisjonen:

 tetromino.potentialTopLeft = rad: tetromino.topLeft, col: tetromino.topLeft - 1;

Nå kontrollerer vi om noen av blokkene i tetromino overlapper de landede blokkene, ved å bruke samme grunnleggende sjekk som før (uten å plage å kontrollere om noen blokk har gått under spillfeltet):

 for (var row = 0; rad < 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    

Kjør det gjennom de samme kontrollene vi vanligvis utfører, og du vil se at dette fungerer bra. Den store forskjellen er, vi må huske ikke å legge til tetromino blokkene til landet [] array hvis det er en potensiell kollisjon - i stedet bør vi ikke endre verdien av tetromino.topLeft.

Hver gang spilleren beveger tetromino, bør vi gjengjøre alt. Her er det endelige resultatet:

Hva skjer hvis spilleren treffer igjen igjen? Når vi kaller dette:

 tetromino.potentialTopLeft = rad: tetromino.topLeft, col: tetromino.topLeft - 1;

... vi vil ende opp med å prøve å sette tetromino.potentialTopLeft.col til -1 - og det vil føre til alle slags problemer senere.

La oss endre vår eksisterende kollisjonskontroll for å håndtere dette:

 for (var row = 0; rad < 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    

Enkelt - det er den samme ideen som når vi sjekker om noen av blokkene vil falle under spillfeltet.

La oss også håndtere høyre side:

 for (var row = 0; rad < 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 >= landet [0] .length) // denne blokken ville være til høyre for spillefeltet hvis (landet [rad + tetromino.potentialTopLeft.row]! = 0 && landet [col + tetromino.potentialTopLeft.col]! = 0) // plassen er tatt

Igjen, hvis tetromino ville bevege seg utenfor lekeplassen, endrer vi ikke bare tetromino.topLeft - trenger ikke å gjøre noe annet.

Ok, et halvt sekund må ha passert nå, så la oss la at tetromino faller en rad:

 tetromino.shape = [[0,1], [0,1], [1,1]]; tetromino.topLeft = rad: 1, kol: 0;

Anta nå at spilleren treffer knappen for å få tetrominoen til å rotere med klokken. Dette er faktisk ganske enkelt å håndtere - vi endrer bare tetromino.shape, uten å endre tetromino.topLeft:

 tetromino.shape = [[1,0,0], [1,1,1]]; tetromino.topLeft = rad: 1, kol: 0;

Vi kunne bruk noen matte til å rotere innholdet i mengden blokker ... men det er mye enklere bare å lagre de fire mulige rotasjonene til hver tetromino et sted, slik som dette:

 jTetromino.rotations = [[[0,1], [0,1], [1,1]], [[1,0,0], [1,1,1]], [[1,1], [1,0], [1,0]], [[1,1,1], [0,0,1]]];

(Jeg vil la deg finne ut hvor best det er å lagre det i koden din!)

Uansett, når vi gjengir alt igjen, vil det se slik ut:

Vi kan rotere den igjen (og antar at vi gjør begge disse rotasjonene innen et halvt sekund):

 tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rad: 1, kol: 0;

Gi igjen:

Herlig. La oss slippe noen få rader til vi kommer til denne tilstanden:

 tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rad: 10, kol: 0;

Plutselig treffer spilleren Rotate Clockwise-knappen igjen, uten tilsynelatende grunn. Vi kan se fra å se på bildet at dette ikke skal tillate noe å skje, men vi har ingen kontroller på plass ennå for å hindre det.

Du kan sikkert gjette hvordan vi skal løse dette. Vi presenterer en tetromino.potentialShape, sett den til formen på den roterte tetrominoen, og se etter eventuelle mulige overlapper med blokker som allerede har landet.

 tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rad: 10, kol: 0; tetromino.potentialShape = [[1,1,1], [0,0,1]];
 for (var row = 0; rad < 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 >= landte [0] .length) // denne blokken ville være til høyre for spillefeltet hvis (rad + tetromino.topLeft.row> = landed.length) // denne blokken vil være under spillfeltet hvis (landet [rad + tetromino.topLeft.row]! = 0 && landet [col + tetromino.topLeft.col]! = 0) // plassen er tatt

Hvis det er en overlapping (eller hvis den roterte formen vil være delvis ubegrenset), tillater vi ikke at blokken roterer. Dermed kan det falle på plass et halvt sekund senere, og bli lagt til landet [] matrise:

Utmerket.


Holde alt rett

For å være klar har vi nå tre separate sjekker.

Den første sjekken gjelder for når en tetromino faller, og kalles hver halve sekund:

 // sett tetromino.potentialTopLeft å være en rad under tetromino.topLeft, da: for (var row = 0; rad < 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) // denne blokken vil være under spillfeltet ellers hvis (landet [rad + tetromino.potentialTopLeft.row]! = 0 && landet [col + tetromino.potentialTopLeft.col]! = 0) / / plassen er tatt

Hvis alle sjekker passerer, setter vi inn tetromino.topLeft til tetromino.potentialTopLeft.

Hvis noen av kontrollene feiler, gjør vi tetromino-landet, slik som:

 for (var row = 0; rad < 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];   

Den andre kontrollen er for når spilleren forsøker å flytte tetromino til venstre eller høyre, og kalles når spilleren treffer bevegelsesknappen:

 // sett tetromino.potentialTopLeft å være en kolonne til høyre eller venstre // av tetromino.topLeft, etter behov, da: for (var rad = 0; rad < 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 >= landet [0] .length) // denne blokken ville være til høyre for spillefeltet hvis (landet [rad + tetromino.potentialTopLeft.row]! = 0 && landet [col + tetromino.potentialTopLeft.col]! = 0) // plassen er tatt

Hvis (og bare hvis) alle disse kontrollene passerer, setter vi inn tetromino.topLeft til tetromino.potentialTopLeft.

Den tredje sjekk er for når spilleren forsøker å rotere tetromino med eller mot klokken, og kalles når spilleren treffer nøkkelen til å gjøre det:

 // sett tetromino.potentialShape for å være den roterte versjonen av tetromino.shape // (med klokken eller mot klokken som det passer), da: for (var row = 0; rad < 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 >= landte [0] .length) // denne blokken ville være til høyre for spillefeltet hvis (rad + tetromino.topLeft.row> = landed.length) // denne blokken vil være under spillfeltet hvis (landet [rad + tetromino.topLeft.row]! = 0 && landet [col + tetromino.topLeft.col]! = 0) // plassen er tatt

Hvis (og bare hvis) alle disse kontrollene passerer, setter vi inn tetromino.shape til tetromino.potentialShape.

Sammenlign disse tre kontrollene - det er lett å få dem blandet opp, fordi koden er veldig lik.


Andre problemer

Form dimensjoner

Så langt har jeg brukt forskjellige størrelser av arrays for å representere ulike former for tetrominoer (og de forskjellige rotasjonene av disse figurene): O-tetromino brukte et 2x2-array, og J-tetromino brukte en 3x2 eller en 2x3-serie.

For konsistens anbefaler jeg at du bruker samme størrelsesorden for alle tetrominoene (og rotasjoner derav). Forutsatt at du stikker med de sju standard tetrominoene, kan du gjøre dette med en 4x4-serie.

Det er flere forskjellige måter du kan ordne rotasjonene innenfor denne 4x4-kvadraten; ta en titt på Tetris Wiki for mer informasjon om hvilke forskjellige spill som brukes.

Wall Kicking

Anta at du representerer en vertikal I-tetromino slik:

 [[0,1,0,0], [0,1,0,0], [0,1,0,0], [0,1,0,0]];

... og du representerer rotasjonen slik:

 [[0,0,0,0], [0,0,0,0], [1,1,1,1], [0,0,0,0]];

Anta nå at en vertikal I-tetromino er presset opp mot en vegg som dette:

Hva skjer hvis spilleren treffer Rotate-tasten?

Vel, ved hjelp av vår nåværende kollisjonsdetekteringskode, skjer ingenting - den venstre blokk av den horisontale I-tetromino vil være utenfor grensene.

Dette er uten tvil fint - det var slik det virket i NES-versjonen av Tetris - men det er et alternativ: roter tetromino, og flytt det en gang til høyre til høyre, slik som:

Jeg vil la deg finne ut detaljene, men i hovedsak må du sjekke om roterende tetromino vil flytte den ut av grensen, og i så fall flytte den til venstre eller høyre ett eller to mellomrom etter behov. Du må imidlertid huske å sjekke for potensielle kollisjoner med andre blokker etter å ha brukt begge rotasjonene og bevegelsen!

Ulike fargede blokker

Jeg har brukt blokker av samme farge i hele denne opplæringen for å holde ting enkelt, men det er lett å endre fargene.

For hver farge, velg et tall for å representere det; bruk disse tallene i din form[] og landet [] arrays; Endre deretter renderingskoden til fargeblokker basert på tallene deres.

Resultatet kan se slik ut:


Konklusjon

Å skille den visuelle representasjonen av et in-game-objekt fra dataene er et veldig viktig konsept å forstå; det kommer igjen og igjen i andre spill, spesielt når det gjelder kollisjonsdeteksjon.

I mitt neste innlegg ser vi på hvordan du implementerer den andre kjernen i Tetris: fjern linjer når de er fylt. Takk for at du leste!