Comment faire correspondre les formes de puzzle à l'aide de masques de bitume

Dans ce tutoriel, je vais vous expliquer comment analyser une planche de tuiles, les parcourir et les trouver. Nous allons créer un jeu où vous devez connecter des lignes pour former des chemins complètement fermés sans extrémité ouverte. Afin de simplifier les choses, nous allons utiliser le masquage de bits dans le cadre de notre algorithme en attribuant à chaque mosaïque (plus sa rotation) son propre numéro de masque binaire. Ne vous inquiétez pas si vous ne savez pas ce que c'est que le bitmasking. C'est en fait très simple!

Articles Similaires
  • Comprendre les opérateurs au niveau des bits
  • Systèmes de numération: Introduction à binaire, hexadécimal et plus
  • Faire un match-3 dans Construct 2: détection de match

Jouer la démo

Je vais créer le projet en C # en utilisant Unity avec le framework Futile, mais le code sera applicable à pratiquement tout framework 2D avec peu de modifications. Voici le repo Github avec l’ensemble du projet Unity. Et ci-dessous se trouve une démo jouable du jeu que nous allons réaliser:


Cliquez sur les flèches pour faire glisser les lignes et les colonnes. Essayez de faire des formes fermées.

Au-delà du match 3

Quand j'ai commencé à créer Polymer, je voulais créer autre chose qu'un jeu de correspondance. Mon surnom interne pour cela était un jeu "match-any". Les jeux de puzzle de Match-3 sont partout. Bien qu'ils puissent être amusants, ils sont si courants parce que l'algorithme permettant de trouver une correspondance de trois tuiles est assez simple..

Je voulais être capable de faire correspondre plusieurs tuiles qui pourraient entrelacer des rangées et des colonnes, se faufiler à travers le tableau. Non seulement cela, mais je ne voulais pas d'un simple jeu de correspondance des couleurs. Je voulais que les allumettes soient basées sur des côtés spécifiques des carreaux (par exemple, une forme ne peut se connecter qu’à d’autres formes à gauche et à droite, mais pas en haut et en bas.) Cela s’est avéré bien plus complexe que un algorithme match-3 normal.

Ce tutoriel sera divisé en trois sections: La tuile, Le groupe de match et Le plateau de jeu. Dans ce tutoriel, je vais essayer d'éviter autant que possible le code spécifique à Futile. Si vous voulez voir les choses spécifiques à Futile, regardez le code source. De plus, je ne vais pas montrer toutes les méthodes et variables de ce post. Juste les plus importants. Donc, si vous pensez que quelque chose manque, encore une fois, regardez le code source.

Qu'est-ce qu'un masque de bits?

Le mot «masque de bits» fait référence à la manière dont vous pouvez stocker une série de valeurs true / false dans une seule variable numérique. Étant donné que les nombres sont représentés par des uns et des zéros lorsqu'ils sont représentés en binaire, vous pouvez activer ou désactiver les valeurs en changeant le nombre en changeant si un bit est 1 ou 0.

Pour plus de détails, veuillez consulter cet article sur les opérateurs au niveau des bits et cet article sur les nombres binaires.


La tuile

Notre première classe s'appelle LineTile. Avant le début du cours, définissons chaque type de mosaïque.

 // Les différents types de tuiles: enum public LineTileType Nub, Line, Corner, Threeway, Cross, MAX

Voici à quoi ressemblent les pièces:

Ensuite, puisque nous n'autoriserons que des rotations de 90 degrés, faisons une enum pour la rotation.

 // J'utilise ceci au lieu de degrés exacts car les // mosaïques ne devraient avoir que quatre rotations distinctes: énumération publique RotationType Rotation0, Rotation90, Rotation180, Rotation270, MAX

Suivant est un struct appelé TileIndex, qui est fondamentalement la même chose qu'un Vecteur2, sauf avec ints au lieu de floats. Il sera utilisé pour savoir où se trouve une tuile sur le plateau de jeu..

 structure publique TileIndex public int xIndex; public int yIndex; TileIndex public (int xIndex, int yIndex) this.xIndex = xIndex; this.yIndex = yIndex; 

Enfin, définissons les trois types de connexions entre deux tuiles.

 public enum TileConnectionType // Une incompatibilité. Non valide, // Les tuiles ne se connectent pas directement, // mais pas à cause d'un bord sans correspondance. ValidWithOpenSide, // Les tuiles se connectent directement. ValidWithSolidMatch

Ensuite, dans la classe elle-même, définissez un masque de bits de chaque côté d'une mosaïque générique..

 // Voici les bits que j'ai assignés de chaque côté de la tuile: // ===== 1 ===== // | | // | | // 8 2 // | | // | | // ===== 4 ===== // 1 == 0001 en binaire // 2 == 0010 en binaire // 4 == 0100 en binaire // 8 == 1000 en binaire public const int kBitmaskNone = 0; public const int kBitmaskTop = 1; public const int kBitmaskRight = 2; public const int kBitmaskBottom = 4; public const int kBitmaskLeft = 8;

Puis définissez les variables d’instance que chaque tuile aura.

 // La représentation sprite de la tuile: public FSprite sprite; // Le type de la tuile: public LineTileType lineTileType get; private set; // Rotation de la tuile: public RotationType rotationType get; private set; // Le masque binaire qui représente la mosaïque avec sa rotation: public int bitmask get; private set; // Emplacement de la tuile sur le tableau: public TileIndex tileIndex = new TileIndex ();

Pour le constructeur, créez le sprite et configurez-le avec la rotation correcte. Il y a du code spécifique à Futile ici mais il devrait être très facile à comprendre.

 public LineTile (LineTileType lineTileType, RotationType rotationType) this.lineTileType = lineTileType; this.rotationType = rotationType; // Configuration de l'image-objet: switch (lineTileType) case LineTileType.Nub: sprite = new FSprite ("lineTileNub"); Pause; case LineTileType.Line: sprite = new FSprite ("lineTileLine"); Pause; case LineTileType.Corner: sprite = new FSprite ("lineTileCorner"); Pause; case LineTileType.Threeway: sprite = new FSprite ("lineTileThreeway"); Pause; case LineTileType.Cross: sprite = new FSprite ("lineTileCross"); Pause; défaut: lancer une nouvelle FutileException ("type de tuile de ligne invalide");  AddChild (sprite); // Configuration de la rotation des images-objets: switch (rotationType) case RotationType.Rotation0: sprite.rotation = 0; Pause; case RotationType.Rotation90: sprite.rotation = 90; Pause; case RotationType.Rotation180: sprite.rotation = 180; Pause; case RotationType.Rotation270: sprite.rotation = 270; Pause; défaut: lancer une nouvelle FutileException ("type de rotation invalide"); 

Maintenant, l'une des parties les plus importantes. Nous assignons à chaque mosaïque, associée à sa rotation, un masque de bits qui est déterminé par ses côtés solides et ceux qui sont ouverts..

 // Configure le masque de bits en effectuant une opération OU au niveau du bit, avec chaque côté inclus dans la forme. // Ainsi, par exemple, une mosaïque dont les quatre côtés sont pleins (par exemple la mosaïque croisée) serait // 1 | 2 | 4 | 8 = 15, ce qui équivaut à 0001 | 0010 | 0100 | 1000 = 1111 en binaire. if (lineTileType == LineTileType.Nub) if (rotationType == RotationType.Rotation0) bitmask = kBitmaskTop; if (rotationType == RotationType.Rotation90) bitmask = kBitmaskRight; if (rotationType == RotationType.Rotation180) bitmask = kBitmaskBottom; if (rotationType == RotationType.Rotation270) bitmask = kBitmaskLeft;  if (lineTileType == LineTileType.Line) if (rotationType == RotationType.Rotation0 || rotationType == RotationType.Rotation180) bitmask = kBitmaskTop | kBitmaskBottom; if (rotationType == RotationType.Rotation90 || rotationType == RotationType.Rotation270) bitmask = kBitmaskRight | kBitmaskLeft;  if (lineTileType == LineTileType.Corner) if (rotationType == RotationType.Rotation0) bitmask = kBitmaskTop | kBitmaskRight; if (rotationType == RotationType.Rotation90) bitmask = kBitmaskRight | kBitmaskBottom; if (rotationType == RotationType.Rotation180) bitmask = kBitmaskBottom | kBitmaskLeft; if (rotationType == RotationType.Rotation270) bitmask = kBitmaskLeft | kBitmaskTop;  if (lineTileType == LineTileType.Threeway) if (rotationType == RotationType.Rotation0) bitmask = kBitmaskTop | kBitmaskRight | kBitmaskBottom; if (rotationType == RotationType.Rotation90) bitmask = kBitmaskRight | kBitmaskBottom | kBitmaskLeft; if (rotationType == RotationType.Rotation180) bitmask = kBitmaskBottom | kBitmaskLeft | kBitmaskTop; if (rotationType == RotationType.Rotation270) bitmask = kBitmaskLeft | kBitmaskTop | kBitmaskRight;  if (lineTileType == LineTileType.Cross) bitmask = kBitmaskTop | kBitmaskRight | kBitmaskBottom | kBitmaskLeft; 

Nos tuiles sont installées et nous sommes prêts à commencer à les assortir!


Le groupe de match

Les groupes de correspondance ne sont que cela: des groupes de carreaux qui correspondent (ou non). Vous pouvez commencer sur n'importe quelle tuile d'un groupe de correspondance et atteindre n'importe quelle autre tuile via ses connexions. Toutes ses tuiles sont connectées. Chacune des différentes couleurs indique un groupe de correspondance différent. Le seul qui est complété est le bleu au centre - il n'a pas de connexions non valides.

La classe de groupe de correspondance elle-même est extrêmement simple. Il s’agit essentiellement d’une collection de carreaux avec quelques fonctions d’aide. C'est ici:

 Classe publique MatchGroup liste publique carrelage; public bool isClosed = true; public MatchGroup () tiles = new List();  public void SetTileColor (Couleur couleur) foreach (tuile LineTile dans les tuiles) tile.sprite.color = color;  public void Destroy () tiles.Clear (); 

Le jeu

C'est de loin la partie la plus compliquée de ce processus. Nous devons analyser le tableau en entier, en le divisant en groupes de correspondance individuels, puis déterminer ceux qui sont complètement fermés. Je vais appeler cette classe BitmaskPuzzleGame, car c'est la classe principale qui englobe la logique de jeu.

Avant de commencer par sa mise en œuvre, définissons quelques éléments. Le premier est un simple enum que les flèches seront assignées en fonction de la direction dans laquelle elles se trouvent:

 // Pour nous aider à déterminer quelle flèche a été actionnée: enum public Direction Haut, Droite, Bas, Gauche

Suivant est un struct cela sera envoyé par une flèche sur laquelle on appuie afin que nous puissions déterminer où il se trouve dans le tableau et dans quelle direction il fait face:

 // Quand une flèche est enfoncée, elle contiendra ces données pour savoir quoi faire avec le tableau: public struct ArrowData public Direction direction; public int index; ArrowData public (Direction direction, index int) this.direction = direction; this.index = index; 

Ensuite, dans la classe, définissez les variables d'instance dont nous avons besoin:

 // contient toutes les tuiles de la carte: public LineTile [] [] tileMap; // contient tous les groupes de tuiles connectées: liste publique matchGroups = nouvelle liste(); // Quand une ligne / colonne est décalée, elle est définie sur true pour que HandleUpdate sache actualiser: private bool matchGroupsAreDirty = true; // La largeur du tableau est de plusieurs tuiles: private int tileMapWidth; // Le nombre de carreaux du tableau est le suivant: private int tileMapHeight;

Voici une fonction qui prend une tuile et retourne toutes les tuiles environnantes (celles du dessus, du dessous, à gauche et à droite de celle-ci):

 // Méthode d'assistance pour obtenir toutes les tuiles situées au-dessus / en dessous / à droite / à gauche d'une tuile spécifique: liste privée GetTilesSurroundingTile (tuile LineTile) Liste surroundingTiles = nouvelle liste(); int xIndex = tile.tileIndex.xIndex; int yIndex = tile.tileIndex.yIndex; if (xIndex> 0) SurroundTiles.Add (tileMap [xIndex - 1] [yIndex]); si (xIndex < tileMapWidth - 1) surroundingTiles.Add(tileMap[xIndex + 1][yIndex]); if (yIndex > 0) surroundTiles.Add (tileMap [xIndex] [yIndex - 1]); si (yIndex < tileMapHeight - 1) surroundingTiles.Add(tileMap[xIndex][yIndex + 1]); return surroundingTiles; 

Maintenant, deux méthodes qui renvoient toutes les tuiles d'une colonne ou d'une ligne afin que nous puissions les déplacer:

 // Méthode d'assistance pour obtenir toutes les mosaïques d'une colonne spécifique: private LineTile [] GetColumnTiles (int columnIndex) if (columnIndex < 0 || columnIndex >= tileMapWidth) lève une nouvelle FutileException ("invalid column:" + columnIndex); LineTile [] columnTiles = new LineTile [tileMapHeight]; pour (int j = 0; j < tileMapHeight; j++) columnTiles[j] = tileMap[columnIndex][j]; return columnTiles;  // Helper method to get all the tiles in a specific row: private LineTile[] GetRowTiles(int rowIndex)  if (rowIndex < 0 || rowIndex >= tileMapHeight) jette new FutileException ("invalid column:" + rowIndex); LineTile [] rowTiles = new LineTile [tileMapWidth]; pour (int i = 0; i < tileMapWidth; i++) rowTiles[i] = tileMap[i][rowIndex]; return rowTiles; 

Maintenant, deux fonctions qui décaleront une colonne ou une rangée de carreaux dans une direction spécifique. Lorsqu'une tuile se décale d'un bord, elle fait une boucle de l'autre côté. Par exemple, un décalage à droite sur une ligne de Nub, Cross, Line entraînera une ligne de Ligne, Nub, Cross.

 // Déplace les tuiles d'une colonne vers le haut ou le bas (avec un wrapping). Void privé ShiftColumnInDirection (int columnIndex, Direction dir) LineTile [] currentColumnArrangement = GetColumnTiles (columnIndex); int nextIndex; // Déplace les tuiles afin qu'elles se trouvent aux bons endroits du tableau tileMap. if (dir == Direction.Up) pour (int j = 0; j < tileMapHeight; j++)  nextIndex = (j + 1) % tileMapHeight; tileMap[columnIndex][nextIndex] = currentColumnArrangement[j]; tileMap[columnIndex][nextIndex].tileIndex = new TileIndex(columnIndex, nextIndex);   else if (dir == Direction.Down)  for (int j = 0; j < tileMapHeight; j++)  nextIndex = j - 1; if (nextIndex < 0) nextIndex += tileMapHeight; tileMap[columnIndex][nextIndex] = currentColumnArrangement[j]; tileMap[columnIndex][nextIndex].tileIndex = new TileIndex(columnIndex, nextIndex);   else throw new FutileException("can't shift column in direction: " + dir.ToString()); // Once the tileMap array is set up, actually visually move the tiles to their correct spots. for (int j = 0; j < tileMapHeight; j++)  tileMap[columnIndex][j].y = (j + 0.5f) * tileSize;  matchGroupsAreDirty = true;  // Shift the tiles in a row either right or left one (with wrapping). private void ShiftRowInDirection(int rowIndex, Direction dir)  LineTile[] currentRowArrangement = GetRowTiles(rowIndex); int nextIndex; // Move the tiles so they are in the correct spots in the tileMap array. if (dir == Direction.Right)  for (int i = 0; i < tileMapWidth; i++)  nextIndex = (i + 1) % tileMapWidth; tileMap[nextIndex][rowIndex] = currentRowArrangement[i]; tileMap[nextIndex][rowIndex].tileIndex = new TileIndex(nextIndex, rowIndex);   else if (dir == Direction.Left)  for (int i = 0; i < tileMapWidth; i++)  nextIndex = i - 1; if (nextIndex < 0) nextIndex += tileMapWidth; tileMap[nextIndex][rowIndex] = currentRowArrangement[i]; tileMap[nextIndex][rowIndex].tileIndex = new TileIndex(nextIndex, rowIndex);   else throw new FutileException("can't shift row in direction: " + dir.ToString()); // Once the tileMap array is set up, actually visually move the tiles to their correct spots. for (int i = 0; i < tileMapWidth; i++)  tileMap[i][rowIndex].x = (i + 0.5f) * tileSize;  matchGroupsAreDirty = true; 

Lorsque nous cliquons sur une flèche (c.-à-d. Lorsque le bouton fléché est relâché), nous devons déterminer quelle ligne ou colonne déplacer et dans quelle direction..

 // Quand une flèche est pressée et relâchée, décale une colonne vers le haut ou le bas ou une ligne droite / gauche. public void ArrowButtonReleased (bouton FButton) ArrowData arrowData = (ArrowData) button.data; if (arrowData.direction == Direction.Up || arrowData.direction == Direction.Down) ShiftColumnInDirection (arrowData.index, arrowData.direction);  else if (arrowData.direction == Direction.Right || arrowData.direction == Direction.Left) ShiftRowInDirection (arrowData.index, arrowData.direction); 

Les deux méthodes suivantes sont les plus importantes du jeu. La première prend deux tuiles et détermine le type de connexion qu’elles ont. Il base la connexion sur le premier mosaïque entrée dans la méthode (appelée baseTile). Cette distinction est importante. le baseTile pourrait avoir un ValidWithOpenSide connexion avec le autre tuile, mais si vous les entrez dans l'ordre inverse, cela pourrait retourner Invalide.

 // Il existe trois types de connexions pouvant être associées à deux mosaïques: // 1. ValidWithSolidMatch: cela signifie que les mosaïques sont correctement appariées avec leurs côtés solides connectés. // 2. ValidWithOpenSide - Cela signifie que la baseTile a un côté ouvert touchant l'autre tuile, de sorte que le choix de l'autre tuile n'a pas d'importance. // 3. Invalide: cela signifie que le côté solide de la baseTile correspond au côté ouvert de l'autre tuile, ce qui entraîne une non-concordance. private TileConnectionType TileConnectionTypeBetweenTiles (LineTile baseTile, LineTile otherTile) int baseTileBitmaskSide = baseTile.bitmask; // Le masque de bits pour le côté baseTile spécifique qui touche l'autre tuile. int otherTileBitmaskSide = otherTile.bitmask; // Le masque de bits du côté otherTile spécifique qui touche la mosaïque de base. // En fonction du côté du pavé de base sur lequel l'autre pavé est allumé, bit à bit et chaque côté ensemble. avec // la constante au niveau du bit pour ce côté individuel. Si le résultat est 0, le côté est ouvert. Sinon, le côté est solide. if (otherTile.tileIndex.yIndex < baseTile.tileIndex.yIndex)  baseTileBitmaskSide &= LineTile.kBitmaskBottom; otherTileBitmaskSide &= LineTile.kBitmaskTop;  else if (otherTile.tileIndex.yIndex > baseTile.tileIndex.yIndex) baseTileBitmaskSide & = LineTile.kBitmaskTop; otherTileBitmaskSide & = LineTile.kBitmaskBottom;  else if (otherTile.tileIndex.xIndex < baseTile.tileIndex.xIndex)  baseTileBitmaskSide &= LineTile.kBitmaskLeft; otherTileBitmaskSide &= LineTile.kBitmaskRight;  else if (otherTile.tileIndex.xIndex > baseTile.tileIndex.xIndex) baseTileBitmaskSide & = LineTile.kBitmaskRight; otherTileBitmaskSide & = LineTile.kBitmaskLeft;  if (baseTileBitmaskSide == 0) renvoie TileConnectionType.ValidWithOpenSide; // le côté baseTile touchant otherTile est ouvert. else if (otherTileBitmaskSide! = 0) renvoie TileConnectionType.ValidWithSolidMatch; // baseTile side et otherTile side sont solides et appariés. else return TileConnectionType.Invalid; // le côté baseTile est solide mais le côté otherTile est ouvert. Décalage! 

finalement, Mise à jour des correspondances. C'est la méthode la plus importante de toutes. C’est celui qui parcourt le tableau, analyse toutes les pièces, détermine lesquelles se connectent les unes aux autres et quels groupes de correspondance sont complètement fermés. Tout est expliqué dans les commentaires.

 // Parcourez le tableau et analysez toutes les mosaïques à la recherche de correspondances: private void UpdateMatches () // Des groupes de correspondances sont mis à jour afin qu'ils ne soient plus sales: matchGroupsAreDirty = false; // Comme les colonnes et les lignes glissantes peuvent tout gâcher, nous devons nous débarrasser des anciens groupes de correspondance et recommencer. // N'oubliez pas qu'il existe probablement un moyen d'utiliser l'algorithme qui ne nécessite pas de supprimer toutes les correspondances et // de recommencer à chaque fois (par exemple, il suffit de mettre à jour les correspondances perturbées par un décalage), mais cela peut venir plus tard si vous devez améliorer les performances. foreach (MatchGroup matchGroup dans matchGroups) matchGroup.Destroy (); matchGroups.Clear (); // Nous allons commencer à analyser le tableau à partir de la vignette en bas à gauche. La tuile de base actuelle sera celle // à partir de laquelle nous partons actuellement et à partir de laquelle nous construisons des groupes de correspondance. LineTile currentBaseTile = tileMap [0] [0]; liste tileSurrounders; // Variable qui stockera les tuiles environnantes de diverses tuiles de base. liste checkedTiles = nouvelle liste(); // Nous allons stocker ici les carreaux de base une fois qu'ils ont été analysés afin de ne pas les réanalyser. MatchGroup currentMatchGroup; // Le groupe de correspondance que nous analysons inclut la mosaïque de base actuelle. // Boucle continuellement à travers le tableau, créant des groupes de correspondance jusqu'à ce qu'il ne reste plus de mosaïque. while (currentBaseTile! = null) // Créez un nouveau groupe de correspondance, ajoutez la mosaïque de base actuelle en tant que première mosaïque. currentMatchGroup = new MatchGroup (); currentMatchGroup.tiles.Add (currentBaseTile); // Parcourez les tuiles de base en partant de la tuile de base actuelle, analysez leurs connexions, trouvez une nouvelle tuile de base, // et bouclez à nouveau, etc. jusqu'à ce que vous ne trouviez plus aucune connexion possible avec l'une des tuiles du groupe de correspondance bool stillWorkingOnMatchGroup = vrai; while (stillWorkingOnMatchGroup) // Remplissez la liste tileSurrounders avec toutes les tuiles entourant la mosaïque de base actuelle: tileSurrounders = GetTilesSurroundingTile (currentBaseTile); // Parcourez toutes les mosaïques environnantes et vérifiez si leurs côtés pleins sont alignés sur les côtés solides de la mosaïque de base: foreach (LineTile SurroundTile dans tileSurrounders) TileConnectionType connectionType = TileConnectionTypeBetweenTiles (currentBaseTile, surroundingTile); // S'il existe une correspondance solide, ajoutez le cadre au groupe de correspondance. // S'il y a une incompatibilité, le groupe de correspondance n'est pas un groupe de correspondance "fermé" parfait. // S'il y a une incompatibilité due à un côté ouvert de la mosaïque de base, cela n'a pas d'importance // puisqu'il n'y a pas de côté solide coupé (ceci s'appelle TileConnectionType.ValidWithOpenSide). if (type de connexion == TileConnectionType.ValidWithSolidMatch) currentMatchGroup.tiles.Add (surroundTile); else if (TileConnectionTypeBetweenTiles (currentBaseTile, surroundingTile) == TileConnectionType.Invalid) currentMatchGroup.isClosed = false;  // Si le pavé de base a un côté fermé / solide qui touche le bord du tableau, le groupe de correspondance ne peut pas être fermé. if ((currentBaseTile.bitmask & LineTile.kBitmaskTop)! = 0 && currentBaseTile.tileIndex.yIndex == tileMapHeight - 1) || (currentBaseTile.bitmask & LineTile.kBitmaskRight)! = tileMapWidth - 1) || ((currentBaseTile.bitmask & LineTile.kBitmaskBottom)!! = 0 && currentBaseTile.tileIndex.yIndex == 0) || (currentBaseTile.bitmask & LineTile.kBitmaskLeft) == 0)) currentMatchGroup.isClosed = false; // Ajoute notre mosaïque de base à un tableau afin que nous ne le vérifions pas plus tard: if (! VérifiéTiles.Contains (currentBaseTile)) checkedTiles.Add (currentBaseTile); // Trouver une nouvelle tuile de base que nous avons ajoutée au groupe de correspondance mais que nous n'avons pas encore analysée: for (int i = 0; i < currentMatchGroup.tiles.Count; i++)  LineTile tile = currentMatchGroup.tiles[i]; // If the checkedTiles array has the tile in it already, check to see if we're on the last // tile in the match group. If we are, then there are no more base tile possibilities so we are // done with the match group. If checkedTiles DOESN'T have a tile in the array, it means // that tile is in the match group but hasn't been analyzed yet, so we need to set it as // the next base tile. if (checkedTiles.Contains(tile))  if (i == currentMatchGroup.tiles.Count - 1)  stillWorkingOnMatchGroup = false; matchGroups.Add(currentMatchGroup);   else  currentBaseTile = tile; break;    // We're done with a match group, so now we need to find a new un-analyzed tile that's // not in any match groups to start a new one from. So we'll set currentBaseTile to // null then see if we can find a new one: currentBaseTile = null; for (int i = 0; i < tileMapWidth; i++)  for (int j = 0; j < tileMapHeight; j++)  LineTile newTile = tileMap[i][j]; if (!TileIsAlreadyInMatchGroup(newTile))  currentBaseTile = newTile; break;   if (currentBaseTile != null) break;   

Il ne nous reste que le HandleUpdate une fonction! À chaque image, mettez à jour les groupes de correspondance s’ils ont besoin d’être mis à jour (i.e. matchGroupsAreDirty == true), et définir leurs couleurs.

 public void HandleUpdate () if (matchGroupsAreDirty) UpdateMatches (); 

Voici à quoi ressemblerait l'algorithme si chaque étape était animée:

Et c'est tout! Bien que certains codes soient spécifiques à Futile, il devrait être assez clair comment les étendre à n’importe quel langage ou moteur. Et pour réitérer, il y a beaucoup de choses non essentielles qui manquent dans ce post. S'il vous plaît regardez le code source pour voir comment tout cela fonctionne ensemble!