(Part 3) Create Candy Crush Clone: Identify matches + exercise

Create a match 3 game in Unity

Hello my friend, welcome to the third part of this tutorial on how to Create Candy Crush Clone!

Here you can find the links to other parts:

In this tutorial, we are going to see how to identify the match within the grid! It’s not difficult at all, so lets’ start with the tutorial.

Create Candy Crush Clone: Identify matches

Once two cells have been exchanged, if a sequence of three or more cells has been formed, this must be eliminated, the upper squares must fall down and at the top of the columns, it will be necessary to generate new cells to fill in the empty spaces. If no sequences are detected following the exchange, the action is cancelled and the grid restored to its original state.
The solution we are going to implement will check for each cell whether the upper (in the same column) or subsequent (in the same row) cells will have the same sprite. In the event that three or more cells have the same sprite, they are registered in a list of cells to be deleted, and at the end of the method their sprite is changed to null.

Create Candy Crush Clone
Create Candy Crush Clone

To represent empty space, therefore, we set the SpriteRenderer‘s sprite field to null, and when we move the cells we will simply move the sprite field values, not the SpriteRenderer or the Tile components.

Let’s look at the code, open the GridManager.cs file. First, let’s create a function that helps us get the SpriteRenderer of a cell (which I remember, is the component that deals with drawing a sprite on the screen, that is the icon of our cell), given the row and column numbers:

SpriteRenderer GetSpriteRendererAt(int column, int row)
{
    if (column < 0 || column >= GridDimension
         || row < 0 || row >= GridDimension)
        return null;
    GameObject tile = Grid[column, row];
    SpriteRenderer renderer = tile.GetComponent<SpriteRenderer>();
    return renderer;
}

GetSpriteRendererAt is really similar to GetSpriteAt, the only difference is that the renderer is returned instead of the sprite, so there is nothing new. The renderer that we will obtain is the object on which we will have to act to change the image of the cell.

The function that deals with identifying matches is more interesting:

bool CheckMatches()
{
	HashSet matchedTiles = new HashSet(); // 1
	for (int row = 0; row < GridDimension; row++)
	{
		for (int column = 0; column < GridDimension; column++) // 2
		{
			SpriteRenderer current = GetSpriteRendererAt(column, row); // 3

			List horizontalMatches = FindColumnMatchForTile(column, row, current.sprite); // 4
			if (horizontalMatches.Count >= 2)
			{
				matchedTiles.UnionWith(horizontalMatches);
				matchedTiles.Add(current); // 5
			}

			List verticalMatches = FindRowMatchForTile(column, row, current.sprite); // 6
			if (verticalMatches.Count >= 2)
			{
				matchedTiles.UnionWith(verticalMatches);
				matchedTiles.Add(current);
			}
		}
	}

	foreach (SpriteRenderer renderer in matchedTiles) // 7
	{
		renderer.sprite = null;
	}
	return matchedTiles.Count > 0; // 8
}

CheckMatches has no input parameters, instead, it returns a Boolean that is positive if matches are detected. But let’s see how it works:

  1. A hash set is a container very similar to a list, but among other things, it has the particularity of allowing you to search for elements very quickly and does not allow duplicate elements. Within this data structure, we will store the SpriteRenderers of the cells involved in a match, so that they can be emptied at the end of the function.
  2. We analyze cells in the same order as usual: starting from the bottom right, row by row upwards.
  3. Using the function we wrote earlier, let’s get the renderer of the current cell.
  4. At this point, let’s get the list of consecutive cells equal to the current one in the same column. The logic is implemented in a function that we will see later, but for now, we just need to know that it returns a list of renderers who are involved in the match.
  5. In case the match list is two or greater, we have identified a match! Why two? Because the third cell is the current one, in fact in the SpriteRenderer list that we declared at the beginning of the function we will add the current cell and the list that we have just found.
  6. The same thing is done with the same consecutive cells in the same row.
  7. Finally, let’s make all the renderers involved in the match equal to null, effectively eliminating these cells.
  8. In the event that the list of cells involved in a match is greater than 0, it means that at least one match has taken place (we could have written even greater than or equal to three, it would have been fine anyway).

So let’s see the two functions FindRowMatchForTile and FindColumnMatchForTile. The simplest implementation starts from the current cell and scrolls the entire row or column counting the same cells, stopping as soon as it finds a different one. All the identical cells are saved in a list which will the return value of the function.

List FindColumnMatchForTile(int col, int row, Sprite sprite)
    {
        List result = new List();
        for (int i = col + 1; i < GridDimension; i++)
        {
            SpriteRenderer nextColumn = GetSpriteRendererAt(i, row);
            if (nextColumn.sprite != sprite)
            {
                break;
            }
            result.Add(nextColumn);
        }
        return result;
    }

And, for rows:

List FindRowMatchForTile(int col, int row, Sprite sprite)
    {
        List result = new List();
        for (int i = row + 1; i < GridDimension; i++)
        {
            SpriteRenderer nextRow = GetSpriteRendererAt(col, i);
            if (nextRow.sprite != sprite)
            {
                break;
            }
            result.Add(nextRow);
        }
        return result;
    }

The functions are very similar and do exactly what we have just said, to decide if two cells are equal their sprite is compared, and the sprite renderer is stored in the list.

Now that we have written our function, we just have to use it! So let’s move into the SwapTiles function and add these lines to the end of the function:

Sprite temp = renderer1.sprite;
renderer1.sprite = renderer2.sprite;
renderer2.sprite = temp;

bool changesOccurs = CheckMatches();
if(!changesOccurs)
{
	temp = renderer1.sprite;
	renderer1.sprite = renderer2.sprite;
	renderer2.sprite = temp;
}

We simply call up the function, and if there are no matches, the cells are exchanged again to bring the grid back to the original situation.

Switching to Unity and pressing play, our cells will finally disappear if we form rows of 3 or more identical cells! If, on the other hand, our move does not lead to any result, the move is cancelled.

The next task will be to drop the cells and generate new ones so that there are never empty spaces.

Drop cells and generate new ones

The last step is to tidy up the grid so that it is ready for the player’s next move, i.e. scrolling down the cells above empty spaces and filling them.
Our algorithm will be very simple: for each empty cell look at the cells at the top and move them at the bottom and filling the space that will form at the top with a sprite chosen at random from those available.

void FillHoles()
{
    for (int column = 0; column < GridDimension; column++)
    {
        for (int row = 0; row < GridDimension; row++) // 1
        {
            while (GetSpriteRendererAt(column, row).sprite == null) // 2
            {
                for (int filler = row; filler < GridDimension - 1; filler++) // 3
                {
                    SpriteRenderer current = GetSpriteRendererAt(column, filler); // 4
                    SpriteRenderer next = GetSpriteRendererAt(column, filler + 1);
                    current.sprite = next.sprite;
                }
                SpriteRenderer last = GetSpriteRendererAt(column, GridDimension - 1);
                last.sprite = Sprites[Random.Range(0, Sprites.Count)]; // 5
            }
        }
    }
}
  1. Cycle through our grill as always
  2. Let’s see if the current cell is empty, in which case we start moving the upper cells. If it is not empty, we continue. This cycle assure that the cells are moved down one step a time, until the current cell is not null anymore.
  3. At this point, a new cycle will effectively move the cells down of one position
  4. To move cells down, we simply assign to the lower cell the value of the above cell. The above cell will be setted in the next step.
  5. Finally, we choose a random sprite and assign it to the top cell.

BONUS: Optimization exercise

Within the while loop of the code of the previous paragraph, the GetSpriteRendererAt function is called which uses the GetComponent<T> function which is a very heavy function. Is there any way to optimize this function, so that GetSpriteRendererAt is used less times? The answer is yes, if you want to compare your solution with mine, visit the GitHub page of this project and look through the commits for the one entitled “optimization exercise solution”.

Lastly we have to use the function we just wrote. Let’s add the else branch inside the SwapTiles function of the GridManager:

void FillHoles()
if(!changesOccurs)
{
    temp = renderer1.sprite;
    renderer1.sprite = renderer2.sprite;
    renderer2.sprite = temp;
}
else
{
    do
    {
        FillHoles();
    } while (CheckMatches());
}

Here too we use a cycle because it is possible that moving and adding new tiles has created new matches, in this way we ensure that all of them are managed.

At this point the difficult part is over: we have created the basic gameplay of a match-three puzzle game!

But before you feel satisfied, you should continue reading this guide where I will give you some tips on how to complete the game and add some effects that improve the user experience. Come on, the worst is over, but there are still things to do! You can find how to create the GUI for this game in the next part of this article. Stay tuned to know when the new part is published!

More from Andrea Peretti

(Part 3) Create Candy Crush Clone: Identify matches + exercise

Hello my friend, welcome to the third part of this tutorial on...
Read More

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *