How to Make a Match 3 Game in Unity
Learn how to make a Match 3 game in this Unity tutorial! By Jeff Fisher.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make a Match 3 Game in Unity
30 mins
Matching
Matching can be broken down into a few key steps:
- Find 3 or more of the same sprites next to each other and consider it a match.
- Remove matching tiles.
- Shift tiles down to fill the empty space.
- Refill empty tiles along the top.
- Check for another match.
- 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?
- This method accepts a
Vector2
as a parameter, which will be the direction all raycasts will be fired in. - Create a new list of GameObjects to hold all matching tiles.
- Fire a ray from the tile towards the
castDir
direction. - 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.
- 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.
- Take a
Vector2
array of paths; these are the paths in which the tile will raycast. - Create a GameObject list to hold the matches.
- Iterate through the list of paths and add any matches to the
matchingTiles
list. - 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.
- Iterate through all matching tiles and remove their sprites by setting it
null
. - Set the
matchFound
flag totrue
.
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:
- Loop through and finds how many spaces it needs to shift downwards.
- Store the number of spaces in an integer named
nullCount
. - Loop again to begin the actual shifting.
- Pause for
shiftDelay
seconds. - Loop through every SpriteRenderer in the list of
renders
. - 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.