Come abbinare forme di puzzle usando Bitmask

In questo tutorial, ti guiderò attraverso come analizzare un pannello di piastrelle, iterare attraverso di loro e trovare le corrispondenze. Creeremo un gioco in cui è necessario collegare le linee insieme per formare percorsi completamente chiusi senza estremità aperte. Per semplificare le cose, useremo il bitmasking come parte del nostro algoritmo assegnando a ciascuna piastrella (più la sua rotazione) il proprio numero di maschera di bit. Non preoccuparti se non sai cosa è il bitmasking. In realtà è molto semplice!

Post correlati
  • Comprensione degli operatori bit a bit
  • Sistemi numerici: introduzione a binario, esadecimale e altro
  • Crea un gioco Match-3 in Construct 2: Match Detection

Gioca la demo

Creerò il progetto in C # usando Unity con il framework Futile, ma il codice sarà applicabile praticamente a qualsiasi framework 2D con poche modifiche. Ecco il repository Github con l'intero progetto Unity. E qui sotto c'è una demo giocabile del gioco che realizzeremo:


Fai clic sulle frecce per far scorrere righe e colonne. Prova a creare forme chiuse.

Going Beyond Match-3

Quando ho iniziato a creare Polymer, volevo creare qualcosa di diverso da un gioco match-3. Il mio soprannome interno per questo era un gioco "match-any". I giochi puzzle di combinazione a 3 sono ovunque. Mentre possono certamente essere divertenti, una ragione per cui sono così comuni potrebbe essere perché l'algoritmo per trovare una corrispondenza di tre tessere è piuttosto semplice.

Volevo essere in grado di abbinare più tessere che potrebbero tessere dentro e fuori da righe e colonne, serpeggiando su tutta la linea. Non solo, ma non volevo un semplice gioco di abbinamento dei colori. Volevo che le corrispondenze si basassero su lati specifici delle tessere (ad esempio, una forma poteva connettersi solo ad altre forme sui lati sinistro e destro, ma non in alto e in basso.) Ciò si è rivelato molto più complesso del semplice un normale algoritmo match-3.

Questo tutorial sarà suddiviso in tre sezioni: The Tile, The Match Group e The Game Board. In questo tutorial, cercherò di evitare il più possibile il codice specifico di Futile. Se vuoi vedere le cose specifiche di Futile, guarda il codice sorgente. Inoltre, non ho intenzione di mostrare tutti i metodi e le variabili in questo post. Solo i più importanti. Quindi se pensi che manchi qualcosa, guarda ancora il codice sorgente.

Che cos'è una maschera di bit?

La parola "bitmask" si riferisce al modo in cui è possibile memorizzare una serie di valori vero / falso in una singola variabile numerica. Poiché i numeri sono rappresentati da uno e zero quando rappresentati in binario, cambiando il numero è possibile attivare o disattivare i valori commutando se un bit è 1 o 0.

Per ulteriori dettagli, consultare questo articolo sugli operatori bit a bit e questo articolo sui numeri binari.


La piastrella

La nostra prima classe si chiama LineTile. Prima dell'inizio della lezione, definiamo ogni tipo di tessera.

 // I diversi tipi di tile: public enum LineTileType Nub, Line, Corner, Trio, Croce, MAX

Ecco come appaiono i pezzi:

Quindi, poiché consentiremo rotazioni di 90 gradi, facciamo un enum per rotazione.

 // Sto usando questo invece dei gradi esatti in quanto le // tile dovrebbero avere solo quattro distinte rotazioni: public enum RotationType Rotation0, Rotation90, Rotation180, Rotation270, MAX

Il prossimo è un struct chiamato TileIndex, che è fondamentalmente lo stesso di a Vector2, tranne con gli interi invece dei galleggianti. Sarà usato per tenere traccia di dove una tessera si trova nella plancia di gioco.

 struttura pubblica TileIndex public int xIndex; pubblico int yIndex; TileIndex pubblico (int xIndex, int yIndex) this.xIndex = xIndex; this.yIndex = yIndex; 

Infine, definiamo i tre tipi di connessioni tra due tessere.

 public enum TileConnectionType // Una mancata corrispondenza. Invalido, // Le tessere non si connettono direttamente, // ma non a causa di un vantaggio ineguagliato. ValidWithOpenSide, // I tiles si connettono direttamente. ValidWithSolidMatch

Successivamente, all'interno della classe stessa, definire una maschera di bit su ciascun lato di una tessera generica.

 // Ecco i bit che ho assegnato ad ogni lato della tessera: // ===== 1 ===== // | | // | | // 8 2 // | | // | | // ===== 4 ===== // 1 == 0001 in binario // 2 == 0010 in binario // 4 == 0100 in binario // 8 == 1000 in binario 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;

Quindi definire le variabili di istanza che ogni tessera avrà.

 // La rappresentazione sprite della tessera: sprite pubblico di FSprite; // Il tipo di tile: public LineTileType lineTileType get; private set; // La rotazione della tessera: public RotationType rotationType get; set privato; // La maschera di bit che rappresenta la tessera con la sua rotazione: public int bitmask get; private set; // La posizione della tessera sulla lavagna: tile TileIndex pubblicaIndex = new TileIndex ();

Per il costruttore, crea lo sprite e impostalo alla rotazione corretta. C'è un codice specifico per Futile qui, ma dovrebbe essere molto facile da capire.

 public LineTile (LineTileType lineTileType, RotationType rotationType) this.lineTileType = lineTileType; this.rotationType = rotationType; // Imposta sprite: switch (lineTileType) case LineTileType.Nub: sprite = new FSprite ("lineTileNub"); rompere; case LineTileType.Line: sprite = new FSprite ("lineTileLine"); rompere; case LineTileType.Corner: sprite = new FSprite ("lineTileCorner"); rompere; case LineTileType.Threeway: sprite = new FSprite ("lineTileThreeway"); rompere; case LineTileType.Cross: sprite = new FSprite ("lineTileCross"); rompere; default: lanciare una nuova FutileException ("tipo di linea di linea non valido");  AddChild (sprite); // Imposta rotazione sprite: switch (rotationType) case RotationType.Rotation0: sprite.rotation = 0; rompere; case RotationType.Rotation90: sprite.rotation = 90; rompere; case RotationType.Rotation180: sprite.rotation = 180; rompere; case RotationType.Rotation270: sprite.rotation = 270; rompere; default: lanciare una nuova FutileException ("tipo di rotazione non valido"); 

Ora, una delle parti più importanti. Assegniamo a ciascuna piastrella, in combinazione con la sua rotazione, una maschera di bit che è determinata da quale dei suoi lati è solido e quali sono aperti.

 // Imposta la maschera di bit eseguendo OR bit a bit con ciascun lato incluso nella forma. // Quindi, ad esempio, una mattonella con tutti e quattro i lati solidi (ad esempio la croce) sarà // 1 | 2 | 4 | 8 = 15, che è uguale a 0001 | 0010 | 0100 | 1000 = 1111 in binario. 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; 

Le nostre tessere sono configurate e siamo pronti per iniziare ad abbinarle!


Il gruppo di partite

I gruppi di fiammiferi sono proprio questo: gruppi di tessere che corrispondono (o no). Puoi iniziare su qualsiasi tessera in un gruppo di corrispondenza e raggiungere qualsiasi altra tessera attraverso le sue connessioni. Tutte le sue tessere sono collegate. Ciascuno dei diversi colori indica un diverso gruppo di corrispondenza. L'unico che è completato è quello blu al centro, non ha connessioni non valide.

La stessa classe del gruppo di fiammiferi è in realtà estremamente semplice. In pratica è solo una raccolta di tessere con poche funzioni di supporto. Ecco qui:

 Matchgroup di classe pubblica elenco pubblico piastrelle; public bool isClosed = true; pubblico MatchGroup () tiles = new List();  public void SetTileColor (Color color) foreach (Tile LineTile in tiles) tile.sprite.color = color;  public void Destroy () tiles.Clear (); 

Il gioco

Questa è di gran lunga la parte più complicata di questo processo. Dobbiamo analizzare l'intera scheda, suddividendola nei suoi singoli gruppi di partite, quindi determinare quali, se ve ne sono, sono completamente chiuse. Chiamerò questa classe BitmaskPuzzleGame, poiché è la classe principale che comprende la logica del gioco.

Prima di entrare nella sua implementazione, definiamo un paio di cose. Il primo è un semplice enum che le frecce saranno assegnate in base a quale direzione stanno affrontando:

 // Per aiutarci a determinare quale freccia è stata premuta: public enum Direction Up, Right, Down, Left

Il prossimo è un struct che verrà inviato da una freccia che viene premuta in modo da poter determinare dove si trova nella scacchiera e in quale direzione è rivolta:

 // Quando viene premuta una freccia, conterrà questi dati per capire cosa fare con la lavagna: struttura pubblica ArrowData direzione della direzione pubblica; indice pubblico int; ArrowData pubblico (direzione di direzione, indice int) this.direction = direction; this.index = index; 

Successivamente, all'interno della classe, definisci le variabili di istanza di cui abbiamo bisogno:

 // Contiene tutte le tessere della mappa: public LineTile [] [] tileMap; // Contiene tutti i gruppi di tessere connesse: Elenco pubblico matchGroups = new List(); // Quando una riga / colonna viene spostata, questa è impostata su true, in modo che HandleUpdate sappia aggiornare: private bool matchGroupsAreDirty = true; // Quante tessere sono più larghe: private int tileMapWidth; // Quante tessere il board è alto: private int tileMapHeight;

Ecco una funzione che prende una tessera e restituisce tutte le tessere circostanti (quelle sopra, sotto, a sinistra ea destra):

 // Metodo di supporto per ottenere tutte le tessere che sono sopra / sotto / destra / sinistra di una tessera specifica: Elenco privato GetTilesSurroundingTile (tile LineTile) List surroundingTiles = new List(); int xIndex = tile.tileIndex.xIndex; int yIndex = tile.tileIndex.yIndex; if (xIndex> 0) surroundingTiles.Add (tileMap [xIndex - 1] [yIndex]); se (xIndex < tileMapWidth - 1) surroundingTiles.Add(tileMap[xIndex + 1][yIndex]); if (yIndex > 0) surroundingTiles.Add (tileMap [xIndex] [yIndex - 1]); se (yIndex < tileMapHeight - 1) surroundingTiles.Add(tileMap[xIndex][yIndex + 1]); return surroundingTiles; 

Ora due metodi che restituiscono tutte le tessere in una colonna o riga in modo che possiamo spostarle:

 // Metodo di supporto per ottenere tutti i riquadri in una colonna specifica: private LineTile [] GetColumnTiles (int columnIndex) if (columnIndex < 0 || columnIndex >= tileMapWidth) lancia una nuova FutileException ("colonna non valida:" + columnIndex); LineTile [] columnTiles = new LineTile [tileMapHeight]; per (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) lancia una nuova FutileException ("colonna non valida:" + rowIndex); LineTile [] rowTiles = new LineTile [tileMapWidth]; per (int i = 0; i < tileMapWidth; i++) rowTiles[i] = tileMap[i][rowIndex]; return rowTiles; 

Ora due funzioni che effettivamente spostano una colonna o una fila di tessere in una direzione specifica. Quando una tessera si sposta da un lato, scorre verso l'altro lato. Ad esempio, un passaggio a destra su una riga di Nub, Croce, Linea darà come risultato una riga di Linea, Nub, Croce.

 // Sposta le tessere in una colonna in alto o in basso di una (con il wrapping). private void ShiftColumnInDirection (int columnIndex, Direction dir) LineTile [] currentColumnArrangement = GetColumnTiles (columnIndex); int nextIndex; // Sposta le tessere in modo che si trovino nei punti corretti dell'array tileMap. if (dir == Direction.Up) for (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; 

Quando facciamo clic su una freccia (ad esempio quando viene rilasciato il pulsante freccia), dobbiamo determinare quale riga o colonna spostare e in quale direzione.

 // Quando una freccia viene premuta e rilasciata, sposta una colonna su / giù o una riga a destra / a sinistra. public void ArrowButtonReleased (pulsante 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); 

I prossimi due metodi sono i più importanti del gioco. Il primo prende due tessere e determina il tipo di connessione che hanno. Basa la connessione sul primo input di tile nel metodo (chiamato baseTile). Questa è una distinzione importante. Il baseTile potrebbe avere un ValidWithOpenSide connessione con il otherTile, ma se li inserisci in ordine inverso, potrebbe tornare Non valido.

 // Esistono tre tipi di connessioni che possono avere due riquadri: // 1. ValidWithSolidMatch: ciò significa che le tessere vengono accuratamente abbinate ai lati solidi collegati. // 2. ValidWithOpenSide-questo significa che il baseTile ha un lato aperto che tocca l'altro riquadro, quindi non importa quale sia l'altro riquadro. // 3. Invalid: ciò significa che il lato solido del BaseTile è abbinato al lato aperto dell'altro riquadro, risultando in una mancata corrispondenza. private TileConnectionType TileConnectionTypeBetweenTiles (LineTile baseTile, LineTile otherTile) int baseTileBitmaskSide = baseTile.bitmask; // La maschera di bit per il lato baseType specifico che sta toccando l'altra tessera. int otherTileBitmaskSide = otherTile.bitmask; // La maschera di bit per il lato specifico otherTile che tocca la tessera di base. // A seconda del lato della tessera base, l'altra tessera è accesa, bit per bit e ogni lato insieme. con // la costante bit a bit per quel singolo lato. Se il risultato è 0, il lato è aperto. Altrimenti, // il lato è solido. 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) restituisce TileConnectionType.ValidWithOpenSide; // baseTasto laterale che tocca l'altroTile è aperto. else if (otherTileBitmaskSide! = 0) restituisce TileConnectionType.ValidWithSolidMatch; // lato baseTipo e lato altro lato sono solidi e abbinati. else return TileConnectionType.Invalid; // lato baseType è solido ma è aperto il lato otherTile. Mancata corrispondenza! 

Finalmente, UpdateMatches. Questo è il metodo più importante di tutti. Questo è quello che passa attraverso la scacchiera, analizza tutti i pezzi, determina quali connettono tra loro e quali gruppi di corrispondenza sono completamente chiusi. Tutto è spiegato nei commenti.

 // Passa attraverso la scacchiera e analizza tutte le tessere, cercando le corrispondenze: void privato UpdateMatches () // I gruppi di fiammiferi vengono aggiornati in modo che non siano più sporchi: matchGroupsAreDirty = false; // Poiché scorrere colonne e righe può rovinare tutto, dobbiamo liberarci dei vecchi gruppi di incontro e ricominciare da capo. // Tieni presente che probabilmente c'è un modo per utilizzare l'algoritmo in cui non dobbiamo eliminare tutte le partite e // ricominciare ogni volta (ad esempio, aggiornare le partite interrotte da un turno), ma può venire più tardi se // hai bisogno di migliorare le prestazioni. foreach (MatchGroup matchGroup in matchGroups) matchGroup.Destroy (); matchGroups.Clear (); // Inizieremo ad analizzare la scheda dal riquadro in basso a sinistra. La tessera base attuale sarà quella // di cui stiamo attualmente iniziando e creando gruppi di fiammiferi fuori da. LineTile currentBaseTile = tileMap [0] [0]; Elenco tileSurrounders; // Variabile che memorizzerà le tessere circostanti di varie tessere base. Elenco checkedTiles = new List(); // Conserveremo qui le tessere di base una volta che sono state analizzate, quindi non le rianalizzeremo. MatchGroup currentMatchGroup; // Il gruppo di partite che stiamo analizzando include il riquadro di base corrente. // Effettua il loop continuo attraverso la tavola, creando gruppi di fiammiferi fino a quando non ci sono più tessere da cui creare gruppi di fiammiferi. while (currentBaseTile! = null) // Crea un nuovo gruppo di fiammiferi, aggiungi la tessera di base corrente come prima tessera. currentMatchGroup = new MatchGroup (); currentMatchGroup.tiles.Add (currentBaseTile); // Passa in rassegna le tessere iniziando dalla tessera base attuale, analizza le loro connessioni, trova una nuova tessera base, // e riavvia il ciclo, e così via finché non trovi più connessioni possibili con le tessere nel gruppo di confronto bool stillWorkingOnMatchGroup = vero; while (stillWorkingOnMatchGroup) // Popolare l'elenco tileSurrounders con tutte le tessere che circondano il riquadro di base corrente: tileSurrounders = GetTilesSurroundingTile (currentBaseTile); // Iterate attraverso tutte le tessere circostanti e verificate se i loro lati solidi sono allineati ai lati solidi della piastrella di base: foreach (LineTile aroundTile in tileSurrounders) TileConnectionType connectionType = TileConnectionTypeBetweenTiles (currentBaseTile, surroundingTile); // Se c'è una corrispondenza solida, aggiungi il surrounder al gruppo di fiammiferi. // Se c'è una mancata corrispondenza, il matchgroup non è un perfetto gruppo di incontro "chiuso". // Se c'è una mancata corrispondenza a causa di un lato aperto della tessera base, ciò non ha importanza // poiché non c'è un lato solido tagliato (questo è chiamato TileConnectionType.ValidWithOpenSide). if (connectionType == TileConnectionType.ValidWithSolidMatch) currentMatchGroup.tiles.Add (surroundingTile); else if (TileConnectionTypeBetweenTiles (currentBaseTile, surroundingTile) == TileConnectionType.Invalid) currentMatchGroup.isClosed = false;  // Se la tessera base ha un lato chiuso / solido che tocca il bordo del tabellone, il gruppo di match non può essere chiuso. if (((currentBaseTile.bitmask & LineTile.kBitmaskTop)! = 0 && currentBaseTile.tileIndex.yIndex == tileMapHeight - 1) || ((currentBaseTile.bitmask & LineTile.kBitmaskRight)! = 0 && currentBaseTile.tileIndex.xIndex == tileMapWidth - 1) || ((currentBaseTile.bitmask & LineTile.kBitmaskBottom)! = 0 && currentBaseTile.tileIndex.yIndex == 0) || ((currentBaseTile.bitmask & LineTile.kBitmaskLeft)! = 0 && currentBaseTile.tileIndex.xIndex == 0)) currentMatchGroup.isClosed = false; // Aggiungi la nostra tessera di base a una matrice in modo da non ricontrollarla più tardi: if (! CheckedTiles.Contains (currentBaseTile)) checkedTiles.Add (currentBaseTile); // Trova una nuova tessera base che abbiamo aggiunto al gruppo di fiammiferi ma che non abbiamo ancora analizzato: 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;   

Tutto ciò che abbiamo lasciato è il HandleUpdate funzione! Ogni fotogramma, aggiorna i gruppi di fiammiferi se hanno bisogno di un aggiornamento (es. matchGroupsAreDirty == true), e impostare i loro colori.

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

Ecco come apparirà l'algoritmo se tutti i passaggi fossero animati:

E questo è tutto! Mentre parte del codice in questo è specifico di Futile, dovrebbe essere abbastanza chiaro come estenderlo a qualsiasi altra lingua o motore. E per ribadire, ci sono un sacco di cose non essenziali mancanti in questo post. Si prega di guardare il codice sorgente per vedere come tutto funziona insieme!