Implémentation de Tetris Détection de collision

Je suis sûr qu'il est possible de créer un jeu Tetris avec un outil de jeu pointé-et-clic, mais je n'ai jamais trouvé comment. Aujourd'hui, je suis plus à l'aise de penser à un niveau d'abstraction plus élevé, où le tétromino que vous voyez à l'écran n'est qu'un représentation de ce qui se passe dans le jeu sous-jacent. Dans ce tutoriel, je vais vous montrer ce que je veux dire par une démonstration de la gestion de la détection de collision dans Tetris..

Remarque: Bien que le code de ce tutoriel soit écrit en AS3, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


La grille

Un terrain de jeu standard de Tetris a 16 lignes et 10 colonnes. Nous pouvons le représenter dans un tableau multidimensionnel contenant 16 sous-tableaux de 10 éléments:


Graphiques de ce grand tutoriel Vectortuts +.

Imaginez que l'image à gauche soit une capture d'écran du jeu. C'est ce que le jeu pourrait donner au joueur, après qu'un tetromino ait atterri mais avant qu'un autre ne soit apparu..

À droite, une représentation sous forme de tableau de l'état actuel du jeu. Appelons ça a atterri[], comme il se réfère à tous les blocs qui ont atterri. Un élément de 0 signifie qu'aucun bloc n'occupe cet espace; 1 signifie qu'un bloc a atterri dans cet espace.

Générons maintenant un O-tetromino au centre en haut du champ:

 tetromino.shape = [[1,1], [1,1]]; tetromino.topLeft = rangée: 0, col: 4;

le forme La propriété est une autre représentation multidimensionnelle sous forme de tableau de la forme de ce tétromino.. en haut à gauche donne la position du bloc en haut à gauche du tétromino: dans la rangée du haut, et la cinquième colonne dans.

Nous rendons tout. Tout d’abord, nous dessinons l’arrière-plan - c’est facile, c’est juste une image en grille statique.

Ensuite, nous tirons chaque bloc de la a atterri[] tableau:

 pour (var rangée = 0; rangée < 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   

Mes images de bloc sont 20x20px, donc pour dessiner les blocs, je pourrais simplement insérer une nouvelle image de bloc à (col * 20, rangée * 20). Les détails ne comptent pas vraiment.

Ensuite, nous dessinons chaque bloc du tétromino actuel:

 pour (var rangée = 0; rangée < 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   

Nous pouvons utiliser le même code de dessin ici, mais nous devons compenser les blocs par en haut à gauche.

Voici le résultat:

Notez que le nouveau O-tetromino n’apparaît pas dans le a atterri[] tableau - c'est parce que, eh bien, il n'a pas encore atterri.


Chute

Supposons que le joueur ne touche pas les commandes. À intervalles réguliers, disons toutes les demi-secondes, l'O-tetromino doit tomber d'une rangée..

Il est tentant d'appeler simplement:

 tetromino.topLeft.row ++;

… Et tout restituer à nouveau, mais cela ne détectera aucun chevauchement entre l'O-tetromino et les blocs qui ont déjà atterri.

Au lieu de cela, nous allons d'abord vérifier les collisions potentielles, puis ne déplacerons le tétromino que s'il est "sûr"..

Pour cela, nous devrons définir un potentiel nouvelle position pour le tetromino:

 tetromino.potentialTopLeft = rangée: 1, col: 4;

Maintenant, nous vérifions les collisions. Le moyen le plus simple de procéder consiste à parcourir en boucle tous les espaces de la grille que le tétromino occuperait dans sa nouvelle position potentielle et à vérifier la a atterri[] tableau pour voir si elles sont déjà prises:

 pour (var rangée = 0; rangée < 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    

Testons ceci:

 tetromino.shape = [[1,1], [1,1]]; tetromino.potentialTopLeft: rangée: 1, col: 4 ----------------------------------------- ------- rangée: 0, col: 0, tetromino.shape [0] [0]: 1, atterri [0 + 1] [0 + 4]: 0 rangée: 0, col: 1, tétromino. forme [0] [1]: 1, atterri [0 + 1] [1 + 4]: 0 rangée: 1, col: 0, tetromino.shape [1] [0]: 1, atterri [1 + 1] [ 0 + 4]: 0 rangée: 1, col: 1, tetromino.shape [1] [1]: 1, atterri [1 + 1] [1 + 4]: 0

Tous les zéros! Cela signifie qu'il n'y a pas de collision, de sorte que le tétromino puisse bouger.

Nous fixons:

 tetromino.topLeft = tetromino.potentialTopLeft;

… Et ensuite tout restituer:

Génial!


Atterrissage

Maintenant, supposons que le joueur laisse le tetromino tomber à ce point:

Le coin supérieur gauche est à rangée: 11, col: 4. Nous pouvons voir que le tétromino entrerait en collision avec les blocs débarqués s'il tombait davantage - mais notre code le résout-il? Voyons voir:

 tetromino.shape = [[1,1], [1,1]]; tetromino.potentialTopLeft: rangée: 12, col: 4 ----------------------------------------- ------- rangée: 0, col: 0, tetromino.shape [0] [0]: 1, atterri [0 + 12] [0 + 4]: 0 rangée: 0, col: 1, tétromino. forme [0] [1]: 1, atterri [0 + 12] [1 + 4]: 0 rangée: 1, col: 0, tetromino.shape [1] [0]: 1, atterri [1 + 12] [ 0 + 4]: 1 rangée: 1, col: 1, tetromino.shape [1] [1]: 1, atterri [1 + 12] [1 + 4]: 0

Il y a un 1, ce qui signifie qu'il y a une collision - en particulier, le tétromino entrerait en collision avec le bloc à atterri [13] [4].

Cela signifie que le tetromino a atterri, ce qui signifie que nous devons l'ajouter à la a atterri[] tableau. Nous pouvons le faire avec une boucle très similaire à celle utilisée pour vérifier les collisions potentielles:

 pour (var rangée = 0; rangée < 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];   

Voici le résultat:

Jusqu'ici tout va bien. Mais vous avez peut-être remarqué que nous ne traitons pas le cas où le tétromino atterrit sur le "sol" - nous ne traitons que les tétrominos qui atterrissent au-dessus d'autres tétrominos..

Il existe une solution assez simple à cela: lorsque nous vérifions les collisions potentielles, nous vérifions également si la nouvelle position potentielle de chaque bloc serait en dessous du bas du tableau:

 pour (var rangée = 0; rangée < 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) // ce bloc serait en dessous du terrain de jeu else if (atterri [rangée + tetromino.potentialTopLeft.row]! = 0 && atterri [col + tetromino.potentialTopLeft.col]! = 0) / / l'espace est pris

Bien sûr, si un bloc du tétromino se retrouvait sous le fond du terrain de jeu s'il tombait plus loin, nous le ferions "atterrir", comme si un bloc chevauchait un bloc qui avait déjà atterri..

Nous pouvons maintenant commencer le prochain tour avec un nouveau tétromino..


Déplacement et rotation

Cette fois, créons un J-Tetromino:

 tetromino.shape = [[0,1], [0,1], [1,1]]; tetromino.topLeft = rangée: 0, col: 4;

Rendez-le:

N'oubliez pas que toutes les demi-secondes, le tétromino va chuter d'un rang. Supposons que le joueur appuie sur la touche gauche quatre fois avant qu'une demi-seconde ne passe; nous voulons déplacer le tétromino laissé par une colonne à chaque fois.

Comment pouvons-nous nous assurer que le tétromino ne se heurtera à aucun des blocs débarqués? Nous pouvons réellement utiliser le même code d'avant!

Tout d'abord, nous modifions le nouveau poste potentiel:

 tetromino.potentialTopLeft = rangée: tetromino.topLeft, col: tetromino.topLeft - 1;

Maintenant, nous vérifions si l'un des blocs du tetromino se chevauchent avec les blocs débarqués, en utilisant la même vérification de base qu'auparavant (sans nous soucier de vérifier si un bloc est passé en dessous du terrain de jeu):

 pour (var rangée = 0; rangée < 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    

Exécuter les mêmes contrôles que nous effectuons habituellement et vous verrez que cela fonctionne bien. La grande différence est, nous devons nous rappeler ne pas ajouter les blocs du tetromino au a atterri[] tableau s'il y a une collision potentielle - au lieu de cela, nous ne devrions tout simplement pas changer la valeur de tetromino.topLeft.

Chaque fois que le joueur déplace le tétromino, nous devrions tout refaire. Voici le résultat final:

Qu'advient-il si le joueur frappe à gauche une fois de plus? Quand on appelle ça:

 tetromino.potentialTopLeft = rangée: tetromino.topLeft, col: tetromino.topLeft - 1;

… Nous finirons par essayer de définir tetromino.potentialTopLeft.col à -1 - et cela conduira à toutes sortes de problèmes plus tard.

Modifions notre contrôle de collision existant pour traiter ceci:

 pour (var rangée = 0; rangée < 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    

Simple - c'est la même idée que lorsque nous vérifions si l'un des blocs tombe en dessous du terrain de jeu.

Parlons aussi du côté droit:

 pour (var rangée = 0; rangée < 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 >= atterri [0] .length) // ce bloc serait à droite du terrain de jeu if (atterrir [rangée + tetromino.potentialTopLeft.row]! = 0 && atterri [col + tetromino.potentialTopLeft.col]! = 0) // l'espace est pris

Encore une fois, si le tétromino se déplaçait en dehors du terrain de jeu, nous ne modifions tout simplement pas tetromino.topLeft - pas besoin de faire autre chose.

D'accord, une demi-seconde doit déjà être passée, alors laissez ce tétromino tomber un rang:

 tetromino.shape = [[0,1], [0,1], [1,1]]; tetromino.topLeft = rangée: 1, col: 0;

Maintenant, supposons que le joueur appuie sur le bouton pour faire tourner le tetromino dans le sens des aiguilles d'une montre. C'est en fait assez facile à traiter - nous modifions simplement tetromino.shape, sans altérer tetromino.topLeft:

 tetromino.shape = [[1,0,0], [1,1,1]]; tetromino.topLeft = rangée: 1, col: 0;

nous pourrait Faites quelques calculs pour faire pivoter le contenu du tableau de blocs… mais il est beaucoup plus simple de stocker les quatre rotations possibles de chaque tétromino quelque part, comme ceci:

 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]]];

(Je vous laisserai trouver le meilleur endroit pour stocker cela dans votre code!)

Quoi qu'il en soit, une fois que nous aurons tout rendu à nouveau, cela ressemblera à ceci:

Nous pouvons le faire pivoter à nouveau (et supposons que nous effectuions ces deux rotations en une demi-seconde):

 tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rangée: 1, col: 0;

Rendre à nouveau:

Merveilleux. Laissons tomber quelques lignes de plus, jusqu'à ce que nous arrivions à cet état:

 tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rangée: 10, col: 0;

Soudainement, le joueur appuie à nouveau sur le bouton de rotation dans le sens des aiguilles d'une montre, sans raison apparente. En regardant la photo, nous pouvons voir que cela ne devrait permettre rien, mais nous n'avons pas encore de contrôle en place pour l'empêcher..

Vous pouvez probablement deviner comment nous allons résoudre ce problème. Nous allons introduire un tetromino.potentialShape, réglez-le sur la forme du tétromino pivoté et recherchez d'éventuels chevauchements avec des blocs déjà arrivés.

 tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = rangée: 10, col: 0; tetromino.potentialShape = [[1,1,1], [0,0,1]];
 pour (var rangée = 0; rangée < 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 >= atterri [0] .length) // ce bloc serait situé à droite du terrain de jeu if (row + tetromino.topLeft.row> = landed.length) // ce bloc serait sous le terrain de jeu if (atterri [rangée + tetromino.topLeft.row]! = 0 && atterri [col + tetromino.topLeft.col]! = 0) // l'espace est pris

S'il y a un chevauchement (ou si la forme pivotée serait en partie hors limites), nous ne permettons tout simplement pas au bloc de tourner. Ainsi, il peut se mettre en place une demi-seconde plus tard et s’ajouter à la a atterri[] tableau:

Excellent.


Garder le tout droit

Pour être clair, nous avons maintenant trois contrôles distincts.

La première vérification concerne le moment où un tétromino tombe et est appelée toutes les demi-secondes:

 // définit tetromino.potentialTopLeft sur une ligne sous tetromino.topLeft, then: for (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 (row + tetromino.potentialTopLeft.row >= landed.length) // ce bloc serait en dessous du terrain de jeu else if (atterri [rangée + tetromino.potentialTopLeft.row]! = 0 && atterri [col + tetromino.potentialTopLeft.col]! = 0) / / l'espace est pris

Si toutes les vérifications sont acceptées, nous définissons tetromino.topLeft à tetromino.potentialTopLeft.

Si l'un des contrôles échoue, alors nous faisons atterrir le tétromino, comme suit:

 pour (var rangée = 0; rangée < 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];   

La deuxième vérification concerne le moment où le joueur essaie de déplacer le tétromino vers la gauche ou la droite et est appelée lorsque le joueur appuie sur la touche de déplacement:

 // définit tetromino.potentialTopLeft sur une colonne à droite ou à gauche // de tetromino.topLeft, selon le cas, puis: pour (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 >= atterri [0] .length) // ce bloc serait à droite du terrain de jeu if (atterrir [rangée + tetromino.potentialTopLeft.row]! = 0 && atterri [col + tetromino.potentialTopLeft.col]! = 0) // l'espace est pris

Si (et seulement si) tous ces contrôles réussissent, nous définissons tetromino.topLeft à tetromino.potentialTopLeft.

La troisième vérification concerne le moment où le joueur essaie de faire pivoter le tetromino dans le sens horaire ou anti-horaire, et est appelée lorsque le joueur appuie sur la touche pour le faire:

 // définit tetromino.potentialShape pour qu'il corresponde à la version pivotée de tetromino.shape // (dans le sens des aiguilles d'une montre ou dans le sens contraire, selon le cas), puis: pour (var rangée = 0; rangée < 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 >= atterri [0] .length) // ce bloc serait situé à droite du terrain de jeu if (row + tetromino.topLeft.row> = landed.length) // ce bloc serait sous le terrain de jeu if (atterri [rangée + tetromino.topLeft.row]! = 0 && atterri [col + tetromino.topLeft.col]! = 0) // l'espace est pris

Si (et seulement si) tous ces contrôles réussissent, nous définissons tetromino.shape à tetromino.potentialShape.

Comparez ces trois vérifications - il est facile de les mélanger, car le code est très similaire.


Autres issues

Dimensions de la forme

Jusqu'ici, j'ai utilisé différentes tailles de tableaux pour représenter les différentes formes de tétrominoes (et les différentes rotations de ces formes): l'O-tetromino utilisait un tableau 2x2 et le J-tetromino utilisait un tableau 3x2 ou 2x3..

Pour des raisons de cohérence, je recommande d'utiliser la même taille de tableau pour tous les tétrominoes (et leurs rotations). En supposant que vous vous en teniez aux sept tétrominoes standard, vous pouvez le faire avec un tableau 4x4..

Il existe plusieurs façons d’organiser les rotations dans ce carré 4x4; Jetez un coup d'œil au wiki de Tetris pour plus d'informations sur l'utilisation des différents jeux..

Mur de coups de pied

Supposons que vous représentiez un I-tétromino vertical comme ceci:

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

… Et vous représentez sa rotation comme ceci:

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

Supposons maintenant qu’un tétromino vertical soit appuyé contre un mur comme celui-ci:

Que se passe-t-il si le joueur appuie sur la touche Rotation??

Eh bien, en utilisant notre code de détection de collision actuel, rien ne se passe - le bloc le plus à gauche du I-tetromino horizontal serait en dehors des limites.

C'est très bien - c'est comme cela que fonctionnait la version NES de Tetris - mais il existe une alternative: faites pivoter le tétromino et déplacez-le une fois l'espace vers la droite, comme suit:

Je vous laisse comprendre les détails, mais vous devez essentiellement vérifier si la rotation du tétromino le ferait sortir des limites et, le cas échéant, déplacez-le de gauche à droite ou d'un ou deux espaces si nécessaire. Cependant, vous devez vous rappeler de rechercher d'éventuelles collisions avec d'autres blocs après l'application de la rotation et le mouvement!

Blocs de couleurs différentes

J'ai utilisé des blocs de la même couleur tout au long de ce tutoriel pour simplifier les choses, mais il est facile de changer les couleurs..

Pour chaque couleur, choisissez un nombre pour la représenter; utilisez ces chiffres dans votre forme[] et a atterri[] des tableaux; modifiez ensuite votre code de rendu pour colorer les blocs en fonction de leur nombre.

Le résultat pourrait ressembler à ceci:


Conclusion

Séparer la représentation visuelle d'un objet du jeu de ses données est un concept très important à comprendre. il revient encore et encore dans d'autres jeux, en particulier lorsqu'il s'agit de détection de collision.

Dans mon prochain article, nous verrons comment implémenter l'autre fonctionnalité principale de Tetris: supprimer les lignes lorsqu'elles sont remplies. Merci d'avoir lu!