How to Make a Match 3 Game in Unity

Learn how to make a Match 3 game in this Unity tutorial! By Jeff Fisher.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Matching

Matching can be broken down into a few key steps:

  1. Find 3 or more of the same sprites next to each other and consider it a match.
  2. Remove matching tiles.
  3. Shift tiles down to fill the empty space.
  4. Refill empty tiles along the top.
  5. Check for another match.
  6. Repeat until no more matches are found.

Open up Tile.cs and add the following method below the GetAllAdjacentTiles method:

private List<GameObject> FindMatch(Vector2 castDir) { // 1
    List<GameObject> matchingTiles = new List<GameObject>(); // 2
    RaycastHit2D hit = Physics2D.Raycast(transform.position, castDir); // 3
    while (hit.collider != null && hit.collider.GetComponent<SpriteRenderer>().sprite == render.sprite) { // 4
        matchingTiles.Add(hit.collider.gameObject);
        hit = Physics2D.Raycast(hit.collider.transform.position, castDir);
    }
    return matchingTiles; // 5
}

So what's going on here?

  1. This method accepts a Vector2 as a parameter, which will be the direction all raycasts will be fired in.
  2. Create a new list of GameObjects to hold all matching tiles.
  3. Fire a ray from the tile towards the castDir direction.
  4. Keep firing new raycasts until either your raycast hits nothing, or the tiles sprite differs from the returned object sprite. If both conditions are met, you consider it a match and add it to your list.
  5. Return the list of matching sprites.

Keep up the momentum, and add the following boolean to the top of the file, right above the Awake method:

private bool matchFound = false;

When a match is found, this variable will be set to true.
Now add the following method below the FindMatch method:

private void ClearMatch(Vector2[] paths) // 1
{
    List<GameObject> matchingTiles = new List<GameObject>(); // 2
    for (int i = 0; i < paths.Length; i++) // 3
    {
        matchingTiles.AddRange(FindMatch(paths[i]));
    }
    if (matchingTiles.Count >= 2) // 4
    {
        for (int i = 0; i < matchingTiles.Count; i++) // 5
        {
            matchingTiles[i].GetComponent<SpriteRenderer>().sprite = null;
        }
        matchFound = true; // 6
    }
}

This method finds all the matching tiles along the given paths, and then clears the matches respectively.

  1. Take a Vector2 array of paths; these are the paths in which the tile will raycast.
  2. Create a GameObject list to hold the matches.
  3. Iterate through the list of paths and add any matches to the matchingTiles list.
  4. Continue if a match with 2 or more tiles was found. You might wonder why 2 matching tiles is enough here, that’s because the third match is your initial tile.
  5. Iterate through all matching tiles and remove their sprites by setting it null.
  6. Set the matchFound flag to true.

Now that you’ve found a match, you need to clear the tiles. Add the following method below the ClearMatch method:

public void ClearAllMatches() {
    if (render.sprite == null)
        return;

    ClearMatch(new Vector2[2] { Vector2.left, Vector2.right });
    ClearMatch(new Vector2[2] { Vector2.up, Vector2.down });
    if (matchFound) {
        render.sprite = null;
        matchFound = false;
        SFXManager.instance.PlaySFX(Clip.Clear);
    }
}

This will start the domino method. It calls ClearMatch for both the vertical and horizontal matches. ClearMatch will call FindMatch for each direction, left and right, or up and down.

If you find a match, either horizontally or vertically, then you set the current sprite to null, reset matchFound to false, and play the “matching” sound effect.

For all this to work, you need to call ClearAllMatches() whenever you make a swap.

In the OnMouseDown method, and add the following line just before the previousSelected.Deselect(); line:

previousSelected.ClearAllMatches();

Now add the following code directly after the previousSelected.Deselect(); line:

ClearAllMatches();

You need to call ClearAllMatches on previousSelected as well as the current tile because there's a chance both could have a match.

Save this script and return to the editor. Press the play button and test out the match mechanic, if you line up 3 tiles of the same type now, they'll disappear.

To fill in the empty space, you'll need to shift and re-fill the board.

Shifting and Re-filling Tiles

Before you can shift the tiles, you need to find the empty ones.
Open up BoardManager.cs and add the following coroutine below the CreateBoard method:

public IEnumerator FindNullTiles() {
    for (int x = 0; x < xSize; x++) {
        for (int y = 0; y < ySize; y++) {
            if (tiles[x, y].GetComponent<SpriteRenderer>().sprite == null) {
                yield return StartCoroutine(ShiftTilesDown(x, y));
                break;
            }
        }
    }
}

Note: After you've added this coroutine, you'll get an error about ShiftTilesDown not exisiting. You can safely ignore that error as you'll be adding that coroutine next!

This coroutine will loop through the entire board in search of tile pieces with null sprites. When it does find an empty tile, it will start another coroutine ShiftTilesDown to handle the actual shifting.

Add the following coroutine below the previous one:

private IEnumerator ShiftTilesDown(int x, int yStart, float shiftDelay = .03f) {
    IsShifting = true;
    List<SpriteRenderer>  renders = new List<SpriteRenderer>();
    int nullCount = 0;

    for (int y = yStart; y < ySize; y++) {  // 1
        SpriteRenderer render = tiles[x, y].GetComponent<SpriteRenderer>();
        if (render.sprite == null) { // 2
            nullCount++;
        }
        renders.Add(render);
    }

    for (int i = 0; i < nullCount; i++) { // 3
        yield return new WaitForSeconds(shiftDelay);// 4
        for (int k = 0; k < renders.Count - 1; k++) { // 5
            renders[k].sprite = renders[k + 1].sprite;
            renders[k + 1].sprite = null; // 6
        }
    }
    IsShifting = false;
}

ShiftTilesDown works by taking in an X position, Y position, and a delay. X and Y are used to determine which tiles to shift. You want the tiles to move down, so the X will remain constant, while Y will change.

The coroutine does the following:

  1. Loop through and finds how many spaces it needs to shift downwards.
  2. Store the number of spaces in an integer named nullCount.
  3. Loop again to begin the actual shifting.
  4. Pause for shiftDelay seconds.
  5. Loop through every SpriteRenderer in the list of renders.
  6. Swap each sprite with the one above it, until the end is reached and the last sprite is set to null

Now you need to stop and start the FindNullTiles coroutine whenever a match is found.

Save the BoardManager script and open up Tile.cs. Add the following lines to the ClearAllMatches() method, right above SFXManager.instance.PlaySFX(Clip.Clear);:

StopCoroutine(BoardManager.instance.FindNullTiles());
StartCoroutine(BoardManager.instance.FindNullTiles());

This will stop the FindNullTiles coroutine and start it again from the start.

Save this script and return to the edior. Play the game again and make some matches, you'll notice that the board runs out of tiles as you get matches. To make a never-ending board, you need to re-fill it as it clears.

Open BoardManager.cs and add the following method below ShiftTilesDown:

private Sprite GetNewSprite(int x, int y) {
    List<Sprite> possibleCharacters = new List<Sprite>();
    possibleCharacters.AddRange(characters);

    if (x > 0) {
        possibleCharacters.Remove(tiles[x - 1, y].GetComponent<SpriteRenderer>().sprite);
    }
    if (x < xSize - 1) {
        possibleCharacters.Remove(tiles[x + 1, y].GetComponent<SpriteRenderer>().sprite);
    }
    if (y > 0) {
        possibleCharacters.Remove(tiles[x, y - 1].GetComponent<SpriteRenderer>().sprite);
    }

    return possibleCharacters[Random.Range(0, possibleCharacters.Count)];
}

This snippet creates a list of possible characters the sprite could be filled with. It then uses a series of if statements to make sure you don't go out of bounds. Then, inside the if statements, you remove possible duplicates that could cause an accidental match when choosing a new sprite. Finally, you return a random sprite from the possible sprite list.

In the coroutine ShiftTilesDown, replace:

renders[k + 1].sprite = null;

with:

renders[k + 1].sprite = GetNewSprite(x, ySize - 1);

This will make sure the board is always filled.

When a match is made and the pieces shift there's a chance another match could be formed. Theoretically, this could go on forever, so you need to keep checking until the board has found all possible matches.

MatchFlowChart