DCSIMG
Building a match-3 game in Unity - Scenes From A Developer Memory - Site Root - StudentGuru

Building a match-3 game in Unity

Updated blog post can be found at https://dgkanatsios.com/2015/02/25/building-a-match-3-game-in-unity-3/ 

Please there for updates

This tutorial is meant for educational purposes only to showcase how to build certain types of games. Please respect the copyrights/trademarks of others!

If you are in a hurry, you can try the game here and find the source code here.

Match three games are pretty famous these days. From the original Bejeweled to Candy Crush Saga and even Evolve: Hunters Quest, many games are based on the match 3 mechanism while giving specialized bonuses to the user. Such an example is giving a special item if she matches more than three items. If the user creates a match that includes this bonus, then the whole row or column is destroyed.

In this blog post, we’ll try to dissect in what is needed to build such a game, using Unity 3D, Visual Studio and the C# programming language. Game code was written in Visual Studio (check the free Community edition here). For debugging purposes, don't forget to check the Visual Studio tools for Unity here.

Let’s start with a screenshot of the game running in the Unity Editor

image

Only external assets we’re using are some candy graphics (Public Domain, found on OpenGameArt here) and a very cool sound (found on FreeSound here) to build our game. User can drag (in an attempt to swap) one candy either horizontally or vertically. When the swap happens, the game checks for a match. As soon as a vertical or horizontal match of three (or more!) is encountered, the matched candies disappear. Remaining candies collapse, new candies get created to replace them which collapse, too (imagine gravity acting upon them). The game checks if another match of three is encountered (without any user intervention). If this happens, the matched ones disappear again, remaining candies collapse, new candies fall and so on and so forth. This goes on until no match of three exist and user intervention is required for the game to go on. If the user does not touch the screen for a while, potential matches (candies that if one of them gets swapped will form a match of three) start animating, to give the user a small hint in order to continue the game.

The described game flow can be visualized in the below diagram

image

Such a game can have many types of bonuses. For the sake of this blog post, we have implemented only one. This is created if the user’s drag/swap has a match of four (or more) as an immediate result (i.e. it is not created in matches of four that occur in the subsequent loop of collapses/creations). These bonus candy have a certain color (matching the one found in the normal game candy). If the user later does a match that contains a bonus, then the whole row or column is removed (depending on whether the match was horizontally or vertically oriented).

image

Our game never ends; user can swap and destroys candy (while having her score increased) forever. In production games, user progresses through levels by achieving a certain score or by other means, e.g. by destroying an amount of special bonus candy. The game has only one scene, which we’ll describe. This scene has two buttons, one to restart the level and one to load a predefined level (quite useful for debugging!).

Let’s dive into the code! As in our previous blog posts, we’ll see the code file by file.

Enums

Our game contains two enumerations. The BonusType contains info about the bonus that a shape/candy can carry. It has been defined with the Flags attribute to allow multiple values in the enumeration (check here for a nice article). The class BonusTypeUtilities contains only one method, to determine whether an enumeration variable contains the specified bonus type. Finally, the GameState enum contains the three states of our game.

- None: initial state (idle)

- SelectionStarted: when the user has started dragging

- Animating: when the game is animating (showing animations, collapsing, creating new candies etc.)

image

Constants

Contains some useful constant and self-explainable variables for our game, regarding animation durations, score, rows and columns for the array that will contain our candy and more.

image

 

Shape

The Shape class will be used to hold details for each individual candy. Each candy GameObject on the screen will have a Shape component, so each Shape instance is a MonoBehaviour. It contains info about potential bonus(es), the Type of the Shape (in our case, the candy color) the Column and Row that the candy is placed, a constructor that initializes the bonus enumeration and a method that compares the current Shape with another one, by comparing its Type. We use a Row and Column since we’ll have a two dimensional array host our candies.

image

Since Shape is a MonoBehaviour that is attached to our prefabs (as we’ll see later), we cannot use a constructor to initialize it. So, we’ve implemented an Assign method that sets the basic properties of the Shape. The SwapColumnRow method swaps the row and the column properties of two shape instances.

image

SoundManager

The SoundManager is an easily extendable class that contains AudioClip and relevant AudioSource for the crincle sound. Plus, there is a public method to play this sound.

If one wants to add more sounds, she could easily do that by adding an AudioClip, and AudioSource, add that during Awake and create another “PlayCrincle” method, just like the code below.

image

Utilities

This static class contains some static helper methods.

The AnimatePotentialMatches coroutine takes a list of GameObjects and modifies their opacity (from 1.0 to 0.3 and then to 1.0) using a constant time delay. This is to animate the potential matches that are given as a hint to the user.

image

The AreVerticalOrHorizontalNeighbors method returns true if the two shapes that are passed as parameters are next to each other, either vertically or horizontally.

image

The GetPotentialMatches method tries to find and return a list of possible matches for the game to animate, as a hint to the user. It loops in all candy, calls six different methods that search for potential matches and gathers their results. When we have more than 3 results (different sets of matches), we return a random one of them. However, if we search half the array and we have less than or equal to two matches, we return a random one. This, because we don’t want the algorithm to search more, since by running on a mobile device i) we’ll have a performance penalty which in turn ii) will lead to a battery drain.

image

The code for the six “search” methods won’t be fully listed (it was longer than I originally thought), we’ll just include the comments next to the code that visualize what kind of patterns the methods are searching for.

CheckHorizontal methods search for these patterns (imagine this like a 5x5 array, those elements marked with * are random shapes whereas the ones marked with & are of the same color)

imageimageimageimageimageimage

CheckVertical methods search for these patterns

image image image imageimage  image

AlteredCandyInfo

This class contains information about candy that  are about to be moved after a collapse/new candy creation event. It contains

- a private list with all the candy to be moved

- a property that returns the Distinct (i.e. unique) result of the above list. This is necessary in case the internal list contains the same shape twice

- a method to add a new candy to the private list

- a constructor that initializes the private list

image

MatchesInfo

The MatchesInfo class contains useful information about the candies that were matches (either a match of three or more). It looks a lot like the before mentioned AlteredCandyInfo class (we could possible use some inheritance here) with the addition of the BonusType information for the entire match.

image

image

ShapesArray

As we previously described, we’ll be using a two dimensional array to store our candy shapes. One option would be to create an instance of the array and then do operations on it. However, a much better option is to encapsulate this array (along with some useful operations and variables) in a class. This is the purpose of the ShapesArray class.

Initially, we can see that a two dimensional array is declared. Its dimensions correspond to values taken from the Constants class. There is also an indexer that returns the specific GameObject via requested column/row.

image

The Swap method has the responsibility to swap two GameObjects. It starts by creating a backup of them, in case there is no match and they need to get back to their original positions. Then, it swaps their position in the array and, finally, it calls the SwapColumnRow static method in the Shape class, to swap the individual properties of the two Shape components.

image

The UndoSwap method will undo the swap by simply calling the Swap method on the backup GameObjects.

image

The ShapesArray class contains two methods for matches checking. One of them does a horizontal check whereas the other does a vertical one. They both accept a GameObject as a parameter and will check either the row or the column in which this GameObject belongs to. Also, this GameObject is always added to the list of matches. However, if they find less than three matches, they return an empty list.

image

image

The GetEntireRow and GetEntireColumn methods return the collection of GameObjects that belong in a specific row or column. They are used when a match contains a bonus candy.

image

The ContainsDestroyRowColumnBonus method checks if a collection of matches contains a bonus candy with type “DestroyRowColumn”. This, in order to have the entire row/column removed later.

image

The GetMatches method has two overloads. The first one takes a single GameObject as a parameter. It sequentially

- checks for horizontal matches

- if there are any bonuses there, it will retrieve the entire row. It will also add the DestroyWholeRowColumn bonus flag to the matchesInfo.BonusesContained property if it does not already exist.

- adds the horizontal matches to the MatchesInfo instance

- repeats the same 3 steps while checking vertically

image

The other overload of the GetMatches method gets a collection of GameObjects as a parameter. For each one, it will use the previously described overload to check for matches.

image

The Remove method removes (sets as null) an item from the array. It will be called once for each match encountered.

image

The Collapse method will collapse the remaining candies in the specified columns, after the matched candies removal. Basically, it searches for null items. If it finds any, it will move the nearest top candy to the null item position. It will continue to do so until all null items are stacked on the top positions of the column. Moreover, it will calculate the max distance a candy will have to be moved (this will assist in calculating the animation duration). All the required information is passed into an AlteredCandyInfo class, which is returned to the caller.

image

The GetEmptyItemsOnColumn method gets a specified column as a parameter. It will return the Shape details (more specifically, the positions) via the ShapeInfo class in this column which are empty (null).

image

The ShapeInfo class contains details about row and column for a shape.

image

 

ShapesManager

The ShapesManager class is the main class of our game. It handles the array creation, score keeping and the candy GameObjects’ creation and destruction.

It is attached to the ShapesManager GameObject. We pass some prefabs and GameObjects  via the Editor to the ShapesManager public fields. Specifically, the CandyPrefabs array contains our candy GameObjects, the explosion prefabs contains some animated GameObjects that will run when any candy is destroyed and the BonusPrefabs array contains 5 candy GameObjects, with each one having a corresponding color with our normal candy (for correct matching). The DebugText and ScoreText fields contain UI Text GameObject references, whereas the ShowDebugInfo boolean variable allows the game to show some debug information, on developer’s request.

image

Let’s see the code! In the beginning, there are some private members’ declarations, along with the public ones that we previously described. Candy size is also specified, along with the first candy (the one at [0,0]) position in the scene (called BottomRight). We also declare two IEnumerator variables, which will hold references to coroutines instantiated throughout this class, to make their termination easier.

image

The Awake method enables or disables a UI Text GameObject. This GameObject, if enabled, shows some debug info during the game.

The Start method calls 3 methods to initialize our game.

image

The InitializeTypesOnPrefabShapesAndBonuses method does two things

- sets the Type of each prefab Shape component with the name of the GameObject (e.g. bean_blue)

- sets the Type of each prefab Bonus Shape component with the name of the corresponding prefab (e.g. the swirl_blue bonus candy will get bean_blue as a type). This, in order to be precisely matched (the blue bonus matches the blue candy etc.).

image

InitializeCandyAndSpawnPositions

The InitializeCandyAndSpawnPositions method is based on some other methods and functions.

The score related methods are listed below, featuring a simple initialization and UI updates.

image

The GetRandomCandy method returns a random candy prefab from the candy prefabs collection.

image

The InstantiateAndPlaceNewCandy method creates a new candy GameObject (prefab instantiation) at the specified row and column and at the specified position. It uses the Assign method of the Shape component to give some initial values to it and it places it into the candy array.

image

The SetupSpawnPositions method gives initial values to the spawn positions. Those are the positions that new candy will be created to replace the ones that were removed because of a match of three or four. After their creation at the designated positions, they’ll be animated to the positions they’ll cover (the null/empty positions in the array).

image

The DestroyAllCandy method calls the GameObject.Destroy method on all candy in the array, in order to remove them from our scene.

image

The InitializeCandyAndSpawnPositions

- initializes the score variables

- destroys all elements in the array

- reinitializes the array and the spawn positions for the new candy

- loops through all the array elements and creates new candy taking caution *not* to initially create any matches of three. It’s up to the user to do that, via her swaps!

image

The FixSortingLayer method is used during a user swap, to make sure that the candy that was dragged will appear on top of the other one, for better visual results.

image

Hint related methods

As we previously described, if a user does not touch the screen for a specified amount of time, hints will appear on the screen, showing potential matches if she swaps the proper candy shapes. Let’s take a look at these methods.

The CheckPotentialMatches coroutine uses the GetPotentialMatches method in the Utilities class. If there are any matches, it will animate them using the AnimatePotentialMatches (again in the Utilities class). Moreover, a reference to the coroutine for the animation is saved, in order for it to be possibly stopped at a later time via the StopCoroutine method.

image

The ResetOpacityOnPotentialMatches sets the opacity to default (1.0f) at the candy that were animated, as potential matches.

image

The StartCheckForPotentialMatches method stops the check if it’s already running and starts the CheckPotentialMatches coroutine, storing a reference to it so it can be stopped at a later time.

image

The StopCheckForPotentialMatches will attempt to stop both the AnimatePotentialMatches and the CheckPotentialMatches coroutines (via the use of the StopCoroutine method). Plus, it will reset the opacity on the items that were previously animated.

image

Matching, collapsing and creating new candy

Let’s dive into the hardest part of the ShapesManager. We’ll see the Update method and the rest of the code that handles the core logic of our game.

The GetRandomExplosion method returns a random explosion prefab.

image

The GetBonusFromType method will return the bonus prefab that corresponds to a normal candy type. For example, if the parameter type is a blue candy, it will return the blue bonus prefab.

image

The RemoveFromScene method creates a new explosion, sets it to be destroyed after a specified amount of seconds and destroys the candy which is passed as a parameter. This method makes for a nice “disappear with a bang” effect!

image

The MoveAndAnimate method utilizes the awesome GoKit animation library to animate a collection of GameObjects to their new position. It is used to animate any candy that were collapsed and new candy that was created to replace the empty positions on the array (that was left from the matched candy, which was eventually removed).

image

The CreateNewCandyInSpecificColumns takes the columns that have missing candy (null values) as a parameter. For each column

- it gets the empty items’ info (row + column)

- for each such empty item

- a new random candy is created

- its shape component is assigned with the necessary values

- max distance is calculated (to assist in the animation duration calculation)

- its info is added to a AlteredCandyInfo collection, to be returned and eventually animated to their new location in the scene

image

The CreateBonus method

- creates a new bonus (copied from the prefab) based on the candy type given as parameter

- assigns the new GameObject to its proper position in the array

- sets necessary variables via the Assign method

- adds the DestroyWholeRowColumn bonus type to the Bonus property

image

The Update method is split into two parts, each one handling a different state.

In the none/initial/idle state, game checks if the user has touched a candy. If this happens, the game transitions to the SelectionStarted page.

image

In the SelectionStarted page

- we get a reference of the second GameObject (the second part of the swap operation)

- we stop the check for potential matches

- if user dragged diagonally or very quickly (skipped a GameObject), state changes to idle/none

- else, we transition to the animating state, fix the sorting layer of the two GameObjects and initialize the FindMatchesAndCollapse coroutine, to detect potential matches as a result of the swap and proceed accordingly

image

The FindMatchesAndCollapse method is a big one, we’ll split it into smaller parts to property dissect it.

At the beginning, the method swaps and moves the two candies. Eventually, it gets the matches (matched candies) around the two candies. If they are less than three, then the swap is undone. Otherwise, we hold a boolean variable to indicate that a bonus will be created if

- we have more than four matches

- the matches from both candies do not already contain a bonus

image

Afterwards, if the addBonus variable is equal to true, we get a reference to the GameObject that is part of the match of four. We create a temporary Shape (hitGoCache) to store the necessary details (type, row, column) of this GameObject.

image

If the total matches are more than three, a while loop starts. There, the score is increased and the matches are removed from the array and destroyed from the scene. If we have to add a bonus candy, we create a bonus GameObject. The addBonus boolean is set to false, so that the bonus can be added only in the first run of the while loop. After that, we get the indices of the columns that have null/empty items (have had matches destroyed).

image

We continue by collapsing the candy in these columns, creating new candy in them and calculating the max distance needed for animations. These animations are executed and then, we again check for new matches (after candies have collapsed and new candies have been created). We continue the while loop, doing the same stuff.

Eventually, in a subsequent run of the while loop, the matches encountered are less than three. We exit the loop, transition to the none/idle state and run the method that checks for potential matches (as hint for the user).

image

Debugging

During the development of the game, there was the need to test specific scenarios. E.g. can we test multiple collapses at the same time? Can we easily get a match of five to see the algorithm’s behavior? As you saw, the algorithm is pretty random so we couldn’t easily test such scenarios. This is the reason we developed a way to load custom levels. Take a look at the level.txt file, found in the Resources folder.

The pipe character (|) is used to separate the items in the same row, the new line character (\n) acts as a row separator and blanks are ignored (trimmed). The candies that are created correspond to the defined color. If there is a “_B” at the end of the color, then the respective bonus candy is created.

image

In the DebugUtilities file there is a static method to load this file into a two dimensional string array.

image

In the ShapesManager file, the GetSpecificCandyOrBonusForPremadeLevel loads the specific candy (or bonus candy) from a specified string.

image

The InitializeCandyAndSpawnPositionsFromPremadeLevel method uses the FillShapesArrayFromResourcesData utility method to fill the two dimensional string array. For each string there, the GetSpecificCandyOrBonusForPremadeLevel method is called, which in turn returns a new GameObject candy which is instantiated into our game scene.

image

We saw that there are two buttons on our scene. The “Restart” button calls the IntializeCandyAndSpawnPositions method whereas the “Premade level” calls the InitializeCandyAndSpawnPositionsFromPremadeLevel method.

image

Moreover, since we use Visual Studio for our development, we saved invaluable time though the use of Visual Studio Tools for Unity that allow for easy integration of Unity and Visual Studio plus setting breakpoints and debugging. Highly recommended!

The end

Game is ready for all platforms, including mouse and touch input. Here is a screenshot of the game running in Windows Phone 8.1 emulator (512 MB devices are supported!). The “Premade level” button needs, of course, removal for production use.

image

Thanks for reading this! Hope it’s helpful for your next game. As always, you can try the game here and find the source code here on GitHub.

If you are new to Unity, check out a cool intro video series here. For instructions on how to deploy your existing game onto Windows Store/Phone, check out the Microsoft Virtual Academy video here: http://www.microsoftvirtualacademy.com/training-courses/porting-unity-games-to-windows-store-and-windows-phone

  • Anonymous
    Anonymous

    I have bug in class DebugUtilities ,in line  string[] items = lines[row].Split('|');

    Message Error : Array index is out of range

  • Anonymous
    Anonymous

    Very good. But there is a bug in ShapesManager.cs line 260 : "UnityEngine.Transform don't have any definition for "positionTo"

  • Anonymous
    Anonymous

    muy bueno, voy a probar

  • Anonymous
    Anonymous

    This really help me out thankyou :)

  • Anonymous
    Anonymous

    Hi! Awesome tutorial, so thank you!

    Only problem I have with, is in the ShapesArray with the "public IEnumerable GetEmptyItemsOnColumn(int column)".

    It has a line "emptyItems.Add(new Shape() { Row = row, Column = column });".

    Unity (5.0.0f4) says this: "You are trying to create a MonoBehaviour using the 'new' keyword.  This is not allowed.  MonoBehaviours can only be added using AddComponent()".

    I'm not exactly sure how to do that corectly :)

  • Hi, I have fixed that in the updated version, please check!

  • Anonymous
    Anonymous

    Hi,

    this is awesome. Your approach helped me a lot! One question though, is there any way to say what object was destroyed? I tried print (item.name); in RemoveFromScene (GameObject item) BUT it of course return all object what was destroyed(I see bean_purple(Clone) three times). What I want to know is name of the "group" which was destroyed (want just one "print" on three match). I hope you understand me:) Thanks a lot.

  • Anonymous
    Anonymous

    playing most famous match 3 games when you have a block the pieces scroll in diagonal, how I can implement this?

  • Anonymous
    Anonymous

    Hi! I would like to know how I could make different colored beans do something different, for example blue beans give more points than others.

  • Anonymous
    Anonymous

    hi i was wondering when generating new candy, is it completely random in the candy crush saga game? if so, was there any method they use to prevent a total-no-match scenario? coz random gen means you dont hav 100% control over the gen at all, which means there is always a chance that a total-no-match scenario could occur, no matter how slim the chance might be. thx

  • Anonymous
    Anonymous

    Great tutorial!

    But there is a small bug in the void InitializeCandyAndSpawnPositionsFromPremadeLevel() which flips the premadelevel.

                 1                                        3

                 2  is the text file ->     2  is the "output"

                 3                                        1

    But  it can be easily fixed by altering:

    InstantiateAndPlaceNewCandy(row,column, newCandy);

    to

    InstantiateAndPlaceNewCandy(Constants.Rows - row,column, newCandy);

  • Anonymous
    Anonymous

    edit: InstantiateAndPlaceNewCandy(Constants.Rows -1 - row,column, newCandy);

  • Anonymous
    Anonymous

    The game Crashes immediately I launched it On Android.Any solution.