Building a match-3 game (like Candy Crush) in Unity

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_5062F746.png

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_659DD063

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_14F1D69F

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.)

[Flags]
public enum BonusType
{
    None,
    DestroyWholeRowColumn
}

public static class BonusTypeUtilities
{

    public static bool ContainsDestroyWholeRowColumn(BonusType bt)
    {
        return (bt & BonusType.DestroyWholeRowColumn)
            == BonusType.DestroyWholeRowColumn;
    }
}

public enum GameState
{
    None,
    SelectionStarted,
    Animating
}

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.

    public static class Constants
    {
        public static readonly int Rows = 12;
        public static readonly int Columns = 8;
        public static readonly float AnimationDuration =  0.2f;

        public static readonly float MoveAnimationMinDuration = 0.05f;

        public static readonly float ExplosionDuration = 0.3f;

        public static readonly float WaitBeforePotentialMatchesCheck = 2f;
        public static readonly float OpacityAnimationFrameDelay = 0.05f;

        public static readonly int MinimumMatches = 3;
        public static readonly int MinimumMatchesForBonus = 4;

        public static readonly int Match3Score = 60;
        public static readonly int SubsequentMatchScore = 1000;
    }

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.

public class Shape : MonoBehaviour
{
    public BonusType Bonus { get; set; }
    public int Column { get; set; }
    public int Row { get; set; }

    public string Type { get; set; }

    public Shape()
    {
        Bonus = BonusType.None;
    }

    public bool IsSameType(Shape otherShape)
    {
        if (otherShape == null || !(otherShape is Shape))
            throw new ArgumentException("otherShape");

        return string.Compare(this.Type, (otherShape as Shape).Type) == 0;
    }

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.

    public void Assign(string type, int row, int column)
    {

        if (string.IsNullOrEmpty(type))
            throw new ArgumentException("type");

        Column = column;
        Row = row;
        Type = type;
    }

    public static void SwapColumnRow(Shape a, Shape b)
    {
        int temp = a.Row;
        a.Row = b.Row;
        b.Row = temp;

        temp = a.Column;
        a.Column = b.Column;
        b.Column = temp;
    }
}

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.

public class SoundManager : MonoBehaviour {

    //crincle sound found here: http://freesound.org/people/volivieri/sounds/37171/

    public AudioClip crincleAudioClip;
    AudioSource crincle;

    void Awake()
    {
        crincle = AddAudio(crincleAudioClip);
    }

    AudioSource AddAudio( AudioClip audioClip)
    {
        AudioSource audioSource = this.gameObject.AddComponent<AudioSource>();
        audioSource.playOnAwake = false;
        audioSource.clip = audioClip;
        return audioSource;
    }

    public void PlayCrincle()
    {
        crincle.Play();
    }
}

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.

    public static IEnumerator AnimatePotentialMatches(IEnumerable<GameObject> potentialMatches)
    {
        for (float i = 1f; i >= 0.3f; i -= 0.1f)
        {
            foreach (var item in potentialMatches)
            {
                Color c = item.GetComponent<SpriteRenderer>().color;
                c.a = i;
                item.GetComponent<SpriteRenderer>().color = c;
            }
            yield return new WaitForSeconds(Constants.OpacityAnimationFrameDelay);
        }
        for (float i = 0.3f; i <= 1f; i += 0.1f)
        {
            foreach (var item in potentialMatches)
            {
                Color c = item.GetComponent<SpriteRenderer>().color;
                c.a = i;
                item.GetComponent<SpriteRenderer>().color = c;
            }
            yield return new WaitForSeconds(Constants.OpacityAnimationFrameDelay);
        }
    }

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

    public static bool AreVerticalOrHorizontalNeighbors(Shape s1, Shape s2)
    {
        return (s1.Column == s2.Column ||
                        s1.Row == s2.Row)
                        && Mathf.Abs(s1.Column - s2.Column) <= 1
                        && Mathf.Abs(s1.Row - s2.Row) <= 1;
    }

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.

    public static IEnumerable<GameObject> GetPotentialMatches(ShapesArray shapes)
    {
        //list that will contain all the matches we find
        List<List<GameObject>> matches = new List<List<GameObject>>();

        for (int row = 0; row < Constants.Rows; row++)
        {
            for (int column = 0; column < Constants.Columns; column++)
            {
                  var matches1 = CheckHorizontal1(row, column, shapes);
                  var matches2 = CheckHorizontal2(row, column, shapes);
                  var matches3 = CheckHorizontal3(row, column, shapes);
                  var matches4 = CheckVertical1(row, column, shapes);
                  var matches5 = CheckVertical2(row, column, shapes);
                  var matches6 = CheckVertical3(row, column, shapes);

                  if (matches1 != null) matches.Add(matches1);
                  if (matches2 != null) matches.Add(matches2);
                  if (matches3 != null) matches.Add(matches3);
                  if (matches4 != null) matches.Add(matches4);
                  if (matches5 != null) matches.Add(matches5);
                  if (matches6 != null) matches.Add(matches6);

                  //if we have >= 3 matches, return a random one
                  if (matches.Count >= 3)
                                    return matches[UnityEngine.Random.Range(0, matches.Count - 1)];

                  //if we are in the middle of the calculations/loops
                  //and we have less than 3 matches, return a random one
                  if(row >= Constants.Rows / 2 && matches.Count > 0 && matches.Count <=2)
                                    return matches[UnityEngine.Random.Range(0, matches.Count - 1)];
            }
        }
        return null;
    }

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 5×5 array, those elements marked with * are random shapes whereas the ones marked with & are of the same color)

123456

CheckVertical methods search for these patterns

789101112

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

public class AlteredCandyInfo
{
    private List<GameObject> newCandy { get; set; }
    public int MaxDistance { get; set; }

    public IEnumerable<GameObject> AlteredCandy
    {
        get
        {
            return newCandy.Distinct();
        }
    }

    public void AddCandy(GameObject go)
    {
        if (!newCandy.Contains(go))
            newCandy.Add(go);
    }

    public AlteredCandyInfo()
    {
        newCandy = new List<GameObject>();
    }
}

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.

public class MatchesInfo
{
    private List<GameObject> matchedCandies;

    public IEnumerable<GameObject> MatchedCandy
    {
        get
        {
            return matchedCandies.Distinct();
        }
    }

    public void AddObject(GameObject go)
    {
        if (!matchedCandies.Contains(go))
            matchedCandies.Add(go);
    }

    public void AddObjectRange(IEnumerable<GameObject> gos)
    {
        foreach (var item in gos)
        {
            AddObject(item);
        }
    }

    public MatchesInfo()
    {
        matchedCandies = new List<GameObject>();
        BonusesContained = BonusType.None;
    }

    public BonusType BonusesContained { get; set; }
}

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.

public class ShapesArray
{

    private GameObject[,] shapes = new GameObject[Constants.Rows, Constants.Columns];

    public GameObject this[int row, int column]
    {
        get
        {
            try
            {
                return shapes[row, column];
            }
            catch (Exception ex)
            {

                throw ex;
            }
        }
        set
        {
            shapes[row, column] = value;
        }
    }

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.

    public void Swap(GameObject g1, GameObject g2)
    {
        //hold a backup in case no match is produced
        backupG1 = g1;
        backupG2 = g2;

        var g1Shape = g1.GetComponent<Shape>();
        var g2Shape = g2.GetComponent<Shape>();

        //get array indexes
        int g1Row = g1Shape.Row;
        int g1Column = g1Shape.Column;
        int g2Row = g2Shape.Row;
        int g2Column = g2Shape.Column;

        //swap them in the array
        var temp = shapes[g1Row, g1Column];
        shapes[g1Row, g1Column] = shapes[g2Row, g2Column];
        shapes[g2Row, g2Column] = temp;

        //swap their respective properties
        Shape.SwapColumnRow(g1Shape, g2Shape);

    }

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

    public void UndoSwap()
    {
        if (backupG1 == null || backupG2 == null)
            throw new Exception("Backup is null");

        Swap(backupG1, backupG2);
    }

    private GameObject backupG1;
    private GameObject backupG2;

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.

  private IEnumerable<GameObject> GetMatchesHorizontally(GameObject go)
    {
        List<GameObject> matches = new List<GameObject>();
        matches.Add(go);
        var shape = go.GetComponent<Shape>();
        //check left
        if (shape.Column != 0)
            for (int column = shape.Column - 1; column >= 0; column--)
            {
                if (shapes[shape.Row, column].GetComponent<Shape>().IsSameType(shape))
                {
                    matches.Add(shapes[shape.Row, column]);
                }
                else
                    break;
            }

        //check right
        if (shape.Column != Constants.Columns - 1)
            for (int column = shape.Column + 1; column < Constants.Columns; column++)
            {
                if (shapes[shape.Row, column].GetComponent<Shape>().IsSameType(shape))
                {
                    matches.Add(shapes[shape.Row, column]);
                }
                else
                    break;
            }

        //we want more than three matches
        if (matches.Count < Constants.MinimumMatches)
            matches.Clear();

        return matches.Distinct();
    }

    private IEnumerable<GameObject> GetMatchesVertically(GameObject go)
    {
        List<GameObject> matches = new List<GameObject>();
        matches.Add(go);
        var shape = go.GetComponent<Shape>();
        //check bottom
        if (shape.Row != 0)
            for (int row = shape.Row - 1; row >= 0; row--)
            {
                if (shapes[row, shape.Column] != null &&
                    shapes[row, shape.Column].GetComponent<Shape>().IsSameType(shape))
                {
                    matches.Add(shapes[row, shape.Column]);
                }
                else
                    break;
            }

        //check top
        if (shape.Row != Constants.Rows - 1)
            for (int row = shape.Row + 1; row < Constants.Rows; row++)
            {
                if (shapes[row, shape.Column] != null &&
                    shapes[row, shape.Column].GetComponent<Shape>().IsSameType(shape))
                {
                    matches.Add(shapes[row, shape.Column]);
                }
                else
                    break;
            }

        if (matches.Count < Constants.MinimumMatches)
            matches.Clear();

        return matches.Distinct();
    }

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.

    private IEnumerable<GameObject> GetEntireRow(GameObject go)
    {
        List<GameObject> matches = new List<GameObject>();
        int row = go.GetComponent<Shape>().Row;
        for (int column = 0; column < Constants.Columns; column++)
        {
            matches.Add(shapes[row, column]);
        }
        return matches;
    }

    private IEnumerable<GameObject> GetEntireColumn(GameObject go)
    {
        List<GameObject> matches = new List<GameObject>();
        int column = go.GetComponent<Shape>().Column;
        for (int row = 0; row < Constants.Rows; row++)
        {
            matches.Add(shapes[row, column]);
        }
        return matches;
    }

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.

    private bool ContainsDestroyRowColumnBonus(IEnumerable<GameObject> matches)
    {
        if (matches.Count() >= Constants.MinimumMatches)
        {
            foreach (var go in matches)
            {
                if (BonusTypeUtilities.ContainsDestroyWholeRowColumn
                    (go.GetComponent<Shape>().Bonus))
                    return true;
            }
        }

        return false;
    }

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

    public MatchesInfo GetMatches(GameObject go)
    {
        MatchesInfo matchesInfo = new MatchesInfo();

        var horizontalMatches = GetMatchesHorizontally(go);
        if (ContainsDestroyRowColumnBonus(horizontalMatches))
        {
            horizontalMatches = GetEntireRow(go);
            if (!BonusTypeUtilities.ContainsDestroyWholeRowColumn(matchesInfo.BonusesContained))
                matchesInfo.BonusesContained |= BonusType.DestroyWholeRowColumn;
        }
        matchesInfo.AddObjectRange(horizontalMatches);

        var verticalMatches = GetMatchesVertically(go);
        if (ContainsDestroyRowColumnBonus(verticalMatches))
        {
            verticalMatches = GetEntireColumn(go);
            if (!BonusTypeUtilities.ContainsDestroyWholeRowColumn(matchesInfo.BonusesContained))
                matchesInfo.BonusesContained |= BonusType.DestroyWholeRowColumn;
        }
        matchesInfo.AddObjectRange(verticalMatches);

        return matchesInfo;
    }

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.

    public IEnumerable<GameObject> GetMatches(IEnumerable<GameObject> gos)
    {
        List<GameObject> matches = new List<GameObject>();
        foreach (var go in gos)
        {
            matches.AddRange(GetMatches(go).MatchedCandy);
        }
        return matches.Distinct();
    }

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

    public void Remove(GameObject item)
    {
        shapes[item.GetComponent<Shape>().Row, item.GetComponent<Shape>().Column] = null;
    }

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.

 public AlteredCandyInfo Collapse(IEnumerable<int> columns)
    {
        AlteredCandyInfo collapseInfo = new AlteredCandyInfo();

        ///search in every column
        foreach (var column in columns)
        {
            //begin from bottom row
            for (int row = 0; row < Constants.Rows - 1; row++)
            {
                //if you find a null item
                if (shapes[row, column] == null)
                {
                    //start searching for the first non-null
                    for (int row2 = row + 1; row2 < Constants.Rows; row2++)                     {                         //if you find one, bring it down (i.e. replace it with the null you found)                         if (shapes[row2, column] != null)                         {                             shapes[row, column] = shapes[row2, column];                             shapes[row2, column] = null;                             //calculate the biggest distance                             if (row2 - row > collapseInfo.MaxDistance)
                                collapseInfo.MaxDistance = row2 - row;

                            //assign new row and column (name does not change)
                            shapes[row, column].GetComponent<Shape>().Row = row;
                            shapes[row, column].GetComponent<Shape>().Column = column;

                            collapseInfo.AddCandy(shapes[row, column]);
                            break;
                        }
                    }
                }
            }
        }

        return collapseInfo;
    }

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).

 public IEnumerable<ShapeInfo> GetEmptyItemsOnColumn(int column)
    {
        List<ShapeInfo> emptyItems = new List<ShapeInfo>();
        for (int row = 0; row < Constants.Rows; row++)
        {
            if (shapes[row, column] == null)
                emptyItems.Add(new ShapeInfo() { Row = row, Column = column });
        }
        return emptyItems;
    }

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

public class ShapeInfo
{
      public int Column { get; set; }
      public int Row { get; set; }
}

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_1F55FD39

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.

public class ShapesManager : MonoBehaviour
{
    public Text DebugText, ScoreText;
    public bool ShowDebugInfo = false;
    //candy graphics taken from http://opengameart.org/content/candy-pack-1

    public ShapesArray shapes;

    private int score;

    public readonly Vector2 BottomRight = new Vector2(-2.37f, -4.27f);
    public readonly Vector2 CandySize = new Vector2(0.7f, 0.7f);

    private GameState state = GameState.None;
    private GameObject hitGo = null;
    private Vector2[] SpawnPositions;
    public GameObject[] CandyPrefabs;
    public GameObject[] ExplosionPrefabs;
    public GameObject[] BonusPrefabs;

    private IEnumerator CheckPotentialMatchesCoroutine;
    private IEnumerator AnimatePotentialMatchesCoroutine;

    IEnumerable<GameObject> potentialMatches;

    public SoundManager soundManager;

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.

    void Awake()
    {
        DebugText.enabled = ShowDebugInfo;
    }

    // Use this for initialization
    void Start()
    {
        InitializeTypesOnPrefabShapesAndBonuses();

        InitializeCandyAndSpawnPositions();

        StartCheckForPotentialMatches();
    }

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.).

    private void InitializeTypesOnPrefabShapesAndBonuses()
    {
        //just assign the name of the prefab
        foreach (var item in CandyPrefabs)
        {
            item.GetComponent<Shape>().Type = item.name;

        }

        //assign the name of the respective "normal" candy as the type of the Bonus
        foreach (var item in BonusPrefabs)
        {
            item.GetComponent<Shape>().Type = CandyPrefabs.
                Where(x => x.GetComponent<Shape>().Type.Contains(item.name.Split('_')[1].Trim())).Single().name;
        }
    }

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.

    private void InitializeVariables()
    {
        score = 0;
        ShowScore();
    }

    private void IncreaseScore(int amount)
    {
        score += amount;
        ShowScore();
    }

    private void ShowScore()
    {
        ScoreText.text = "Score: " + score.ToString();
    }

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

  private GameObject GetRandomCandy()
    {
        return CandyPrefabs[Random.Range(0, CandyPrefabs.Length)];
    }

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.

    private void InstantiateAndPlaceNewCandy(int row, int column, GameObject newCandy)
    {
        GameObject go = Instantiate(newCandy,
            BottomRight + new Vector2(column * CandySize.x, row * CandySize.y), Quaternion.identity)
            as GameObject;

        //assign the specific properties
        go.GetComponent<Shape>().Assign(newCandy.GetComponent<Shape>().Type, row, column);
        shapes[row, column] = go;
    }

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).

 private void SetupSpawnPositions()
    {
        //create the spawn positions for the new shapes (will pop from the 'ceiling')
        for (int column = 0; column < Constants.Columns; column++)
        {
            SpawnPositions[column] = BottomRight
                + new Vector2(column * CandySize.x, Constants.Rows * CandySize.y);
        }
    }

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

    private void DestroyAllCandy()
    {
        for (int row = 0; row < Constants.Rows; row++)
        {
            for (int column = 0; column < Constants.Columns; column++)
            {
                Destroy(shapes[row, column]);
            }
        }
    }

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!

  public void InitializeCandyAndSpawnPositions()
    {
        InitializeVariables();

        if (shapes != null)
            DestroyAllCandy();

        shapes = new ShapesArray();
        SpawnPositions = new Vector2[Constants.Columns];

        for (int row = 0; row < Constants.Rows; row++)
        {
            for (int column = 0; column < Constants.Columns; column++)
            {

                GameObject newCandy = GetRandomCandy();

                //check if two previous horizontal are of the same type
                while (column >= 2 && shapes[row, column - 1].GetComponent<Shape>()
                    .IsSameType(newCandy.GetComponent<Shape>())
                    && shapes[row, column - 2].GetComponent<Shape>().IsSameType(newCandy.GetComponent<Shape>()))
                {
                    newCandy = GetRandomCandy();
                }

                //check if two previous vertical are of the same type
                while (row >= 2 && shapes[row - 1, column].GetComponent<Shape>()
                    .IsSameType(newCandy.GetComponent<Shape>())
                    && shapes[row - 2, column].GetComponent<Shape>().IsSameType(newCandy.GetComponent<Shape>()))
                {
                    newCandy = GetRandomCandy();
                }

                InstantiateAndPlaceNewCandy(row, column, newCandy);

            }
        }

        SetupSpawnPositions();
    }

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.

    private void FixSortingLayer(GameObject hitGo, GameObject hitGo2)
    {
        SpriteRenderer sp1 = hitGo.GetComponent<SpriteRenderer>();
        SpriteRenderer sp2 = hitGo2.GetComponent<SpriteRenderer>();
        if (sp1.sortingOrder <= sp2.sortingOrder)
        {
            sp1.sortingOrder = 1;
            sp2.sortingOrder = 0;
        }
    }

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.

  private IEnumerator CheckPotentialMatches()
    {
        yield return new WaitForSeconds(Constants.WaitBeforePotentialMatchesCheck);
        potentialMatches = Utilities.GetPotentialMatches(shapes);
        if (potentialMatches != null)
        {
            while (true)
            {

                AnimatePotentialMatchesCoroutine = Utilities.AnimatePotentialMatches(potentialMatches);
                StartCoroutine(AnimatePotentialMatchesCoroutine);
                yield return new WaitForSeconds(Constants.WaitBeforePotentialMatchesCheck);
            }
        }
    }

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

    private void ResetOpacityOnPotentialMatches()
    {
        if (potentialMatches != null)
            foreach (var item in potentialMatches)
            {
                if (item == null) break;

                Color c = item.GetComponent<SpriteRenderer>().color;
                c.a = 1.0f;
                item.GetComponent<SpriteRenderer>().color = c;
            }
    }

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.

    private void StartCheckForPotentialMatches()
    {
        StopCheckForPotentialMatches();
        //get a reference to stop it later
        CheckPotentialMatchesCoroutine = CheckPotentialMatches();
        StartCoroutine(CheckPotentialMatchesCoroutine);
    }

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.

    private void StopCheckForPotentialMatches()
    {
        if (AnimatePotentialMatchesCoroutine != null)
            StopCoroutine(AnimatePotentialMatchesCoroutine);
        if (CheckPotentialMatchesCoroutine != null)
            StopCoroutine(CheckPotentialMatchesCoroutine);
        ResetOpacityOnPotentialMatches();
    }

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.

    private GameObject GetRandomExplosion()
    {
        return ExplosionPrefabs[Random.Range(0, ExplosionPrefabs.Length)];
    }

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.

    private GameObject GetBonusFromType(string type)
    {
        string color = type.Split('_')[1].Trim();
        foreach (var item in BonusPrefabs)
        {
            if (item.GetComponent<Shape>().Type.Contains(color))
                return item;
        }
        throw new System.Exception("Wrong type");
    }

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!

    private void RemoveFromScene(GameObject item)
    {
        GameObject explosion = GetRandomExplosion();
        var newExplosion = Instantiate(explosion, item.transform.position, Quaternion.identity) as GameObject;
        Destroy(newExplosion, Constants.ExplosionDuration);
        Destroy(item);
    }

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).

    private void MoveAndAnimate(IEnumerable<GameObject> movedGameObjects, int distance)
    {
        foreach (var item in movedGameObjects)
        {
            item.transform.positionTo(Constants.MoveAnimationMinDuration * distance, BottomRight +
                new Vector2(item.GetComponent<Shape>().Column * CandySize.x, item.GetComponent<Shape>().Row * CandySize.y));
        }
    }

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

   private AlteredCandyInfo CreateNewCandyInSpecificColumns(IEnumerable<int> columnsWithMissingCandy)
    {
        AlteredCandyInfo newCandyInfo = new AlteredCandyInfo();

        //find how many null values the column has
        foreach (int column in columnsWithMissingCandy)
        {
            var emptyItems = shapes.GetEmptyItemsOnColumn(column);
            foreach (var item in emptyItems)
            {
                var go = GetRandomCandy();
                GameObject newCandy = Instantiate(go, SpawnPositions[column], Quaternion.identity)
                    as GameObject;

                newCandy.GetComponent<Shape>().Assign(go.GetComponent<Shape>().Type, item.Row, item.Column);

                if (Constants.Rows - item.Row > newCandyInfo.MaxDistance)
                    newCandyInfo.MaxDistance = Constants.Rows - item.Row;

                shapes[item.Row, item.Column] = newCandy;
                newCandyInfo.AddCandy(newCandy);
            }
        }
        return newCandyInfo;
    }

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

    private void CreateBonus(Shape hitGoCache)
    {
        GameObject Bonus = Instantiate(GetBonusFromType(hitGoCache.Type), BottomRight
            + new Vector2(hitGoCache.Column * CandySize.x,
                hitGoCache.Row * CandySize.y), Quaternion.identity)
            as GameObject;
        shapes[hitGoCache.Row, hitGoCache.Column] = Bonus;
        var BonusShape = Bonus.GetComponent<Shape>();
        //will have the same type as the "normal" candy
        BonusShape.Assign(hitGoCache.Type, hitGoCache.Row, hitGoCache.Column);
        //add the proper Bonus type
        BonusShape.Bonus |= BonusType.DestroyWholeRowColumn;
    }

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.

 void Update()
    {
        if (ShowDebugInfo)
            DebugText.text = DebugUtilities.GetArrayContents(shapes);

        if (state == GameState.None)
        {
            //user has clicked or touched
            if (Input.GetMouseButtonDown(0))
            {
                //get the hit position
                var hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
                if (hit.collider != null) //we have a hit!!!
                {
                    hitGo = hit.collider.gameObject;
                    state = GameState.SelectionStarted;
                }

            }
        }

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

        else if (state == GameState.SelectionStarted)
        {
            //user dragged
            if (Input.GetMouseButton(0))
            {

                var hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
                //we have a hit
                if (hit.collider != null && hitGo != hit.collider.gameObject)
                {

                    //user did a hit, no need to show him hints
                    StopCheckForPotentialMatches();

                    //if the two shapes are diagonally aligned (different row and column), just return
                    if (!Utilities.AreVerticalOrHorizontalNeighbors(hitGo.GetComponent<Shape>(),
                        hit.collider.gameObject.GetComponent<Shape>()))
                    {
                        state = GameState.None;
                    }
                    else
                    {
                        state = GameState.Animating;
                        FixSortingLayer(hitGo, hit.collider.gameObject);
                        StartCoroutine(FindMatchesAndCollapse(hit));
                    }
                }
            }
        }
    }

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

 private IEnumerator FindMatchesAndCollapse(RaycastHit2D hit2)
    {
        //get the second item that was part of the swipe
        var hitGo2 = hit2.collider.gameObject;
        shapes.Swap(hitGo, hitGo2);

        //move the swapped ones
        hitGo.transform.positionTo(Constants.AnimationDuration, hitGo2.transform.position);
        hitGo2.transform.positionTo(Constants.AnimationDuration, hitGo.transform.position);
        yield return new WaitForSeconds(Constants.AnimationDuration);

        //get the matches via the helper methods
        var hitGomatchesInfo = shapes.GetMatches(hitGo);
        var hitGo2matchesInfo = shapes.GetMatches(hitGo2);

        var totalMatches = hitGomatchesInfo.MatchedCandy
            .Union(hitGo2matchesInfo.MatchedCandy).Distinct();

        //if user's swap didn't create at least a 3-match, undo their swap
        if (totalMatches.Count() < Constants.MinimumMatches)
        {
            hitGo.transform.positionTo(Constants.AnimationDuration, hitGo2.transform.position);
            hitGo2.transform.positionTo(Constants.AnimationDuration, hitGo.transform.position);
            yield return new WaitForSeconds(Constants.AnimationDuration);

            shapes.UndoSwap();
        }

        //if more than 3 matches and no Bonus is contained in the line, we will award a new Bonus
        bool addBonus = totalMatches.Count() >= Constants.MinimumMatchesForBonus &&
            !BonusTypeUtilities.ContainsDestroyWholeRowColumn(hitGomatchesInfo.BonusesContained) &&
            !BonusTypeUtilities.ContainsDestroyWholeRowColumn(hitGo2matchesInfo.BonusesContained);

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.

        Shape hitGoCache = null;
        if (addBonus)
        {
            //get the game object that was of the same type
            var sameTypeGo = hitGomatchesInfo.MatchedCandy.Count() > 0 ? hitGo : hitGo2;
            hitGoCache = sameTypeGo.GetComponent<Shape>();
        }

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).

  int timesRun = 1;
        while (totalMatches.Count() >= Constants.MinimumMatches)
        {
            //increase score
            IncreaseScore((totalMatches.Count() - 2) * Constants.Match3Score);

            if (timesRun >= 2)
                IncreaseScore(Constants.SubsequentMatchScore);

            soundManager.PlayCrincle();

            foreach (var item in totalMatches)
            {
                shapes.Remove(item);
                RemoveFromScene(item);
            }

            //check and instantiate Bonus if needed
            if (addBonus)
                CreateBonus(hitGoCache);

            addBonus = false;

            //get the columns that we had a collapse
            var columns = totalMatches.Select(go => go.GetComponent<Shape>().Column).Distinct();

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).

  //the order the 2 methods below get called is important!!!
            //collapse the ones gone
            var collapsedCandyInfo = shapes.Collapse(columns);
            //create new ones
            var newCandyInfo = CreateNewCandyInSpecificColumns(columns);

            int maxDistance = Mathf.Max(collapsedCandyInfo.MaxDistance, newCandyInfo.MaxDistance);

            MoveAndAnimate(newCandyInfo.AlteredCandy, maxDistance);
            MoveAndAnimate(collapsedCandyInfo.AlteredCandy, maxDistance);

            //will wait for both of the above animations
            yield return new WaitForSeconds(Constants.MoveAnimationMinDuration * maxDistance);

            //search if there are matches with the new/collapsed items
            totalMatches = shapes.GetMatches(collapsedCandyInfo.AlteredCandy).
                Union(shapes.GetMatches(newCandyInfo.AlteredCandy)).Distinct();

            timesRun++;
        }

        state = GameState.None;
        StartCheckForPotentialMatches();
    }

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_2D1178B7.png

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

    public static string[,] FillShapesArrayFromResourcesData()
    {
        string[,] shapes = new string[Constants.Rows, Constants.Columns];

        TextAsset txt = Resources.Load("level") as TextAsset;
        string level = txt.text;

        string[] lines = level.Split(new string[] { System.Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
        for (int row = Constants.Rows - 1; row >= 0; row--)
        {
            string[] items = lines[row].Split('|');
            for (int column = 0; column < Constants.Columns; column++)
            {
                shapes[row, column] = items[column];
            }
        }
        return shapes;

    }

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

    private GameObject GetSpecificCandyOrBonusForPremadeLevel(string info)
    {
        var tokens = info.Split('_');

        if (tokens.Count() == 1)
        {
            foreach (var item in CandyPrefabs)
            {
                if (item.GetComponent<Shape>().Type.Contains(tokens[0].Trim()))
                    return item;
            }

        }
        else if (tokens.Count() == 2 && tokens[1].Trim() == "B")
        {
            foreach (var item in BonusPrefabs)
            {
                if (item.name.Contains(tokens[0].Trim()))
                    return item;
            }
        }

        throw new System.Exception("Wrong type, check your premade level");
    }

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.

    public void InitializeCandyAndSpawnPositionsFromPremadeLevel()
    {
        InitializeVariables();

        var premadeLevel = DebugUtilities.FillShapesArrayFromResourcesData();

        if (shapes != null)
            DestroyAllCandy();

        shapes = new ShapesArray();
        SpawnPositions = new Vector2[Constants.Columns];

        for (int row = 0; row < Constants.Rows; row++)
        {
            for (int column = 0; column < Constants.Columns; column++)
            {

                GameObject newCandy = null;

                newCandy = GetSpecificCandyOrBonusForPremadeLevel(premadeLevel[row, column]);

                InstantiateAndPlaceNewCandy(row, column, newCandy);

            }
        }

        SetupSpawnPositions();
    }

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_5F99F78B.png

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_1A812A55.png

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

92 thoughts on “Building a match-3 game (like Candy Crush) in Unity

  1. Hi, great tutorial! I want to know if I can use this source code as a base to create my own match 3 game, I plan to create more functions to the game. I ask that because I want to sell the source code to other developers? Do you give me permission to do that?

    Regards

    Like

  2. Would you be able to do a tutorial like this one on a match 3 like in Disco Panda? I can’t seem to find a tutorial with that mechanic.

    Like

      • So, the mechanism in Best Fiends is as follows, from what I can tell. User starts dragging her finger from one gem to one nearby (vertically, horizontally or diagonally). You push the initial get type to a stack. On the subsequent gems, if they are the same type as the original one, you continue to add them to the stack. Upon the end of the drag, you can check if this stack contains more than 3 items. If this is the case, you remove them from the game board and new gems fall from the top (as in my match three game tutorial).
        Is this clear enough for you?

        Like

  3. Match 3 games are incredibly complex to learn , I might just buy a match 3 game kit on the unity store. This project is great however for learning programing in C#. Thanks.

    Like

  4. Hi there, i was wondering how would you go about adding another special candy in? i’ve taken your project and adjusted it to include a new star that the user gets for matching 5 (it can be used with any type to remove all of that type), but my problem is that at random times the game will break.

    any help as to how you would do the match 5 would be appreciated

    Like

  5. This line, right?
    if (shapes [shape.Row, column].GetComponent ().IsSameType (shape)) {
    well, hard to say what is null there without debugging. I would suggest you insert a breakpoint and find the exact variable which is null, this would help you understand what is going on.

    Like

  6. There is a bug in InitializeCandyAndSpawnPositions method. When checking the two previous vertical are of the same type, you may generate a type which is the same as the two previous horizontal type.

    Like

  7. hai dgkanatsios, i have a problem when i try to change the prefabs, it gives me an error like this :
    InvalidOperationException: Operation is not valid due to the current state of the object
    System.Linq.Enumerable.Single[GameObject] (IEnumerable`1 source, System.Func`2 predicate, Fallback fallback)
    System.Linq.Enumerable.Single[GameObject] (IEnumerable`1 source)
    ShapesManager.InitializeTypesOnPrefabShapesAndBonuses () (at Assets/Scripts/ShapesManager.cs:67)
    ShapesManager.Start () (at Assets/Scripts/ShapesManager.cs:45)

    im sorry but i still learning c# and newbie to unity. Any help would be appreciated, thank you

    Like

      • Hi, can u tell, how I can add all prefabs to canvas? I want change background image and etc but prefabs haven’t parents for this. I tried create canvas and used setParent(); in insilization but it’s not working.Thanks for your answer

        Like

  8. Hi, great project for learning. I’m newbie. I want to add one more lightning effect not just random explosion when we has bonus which kill whole row or column. How can i do that? took me a week but still can’t figure it out. Something like:

    if (bonus in totalmatches) {
    do animation at that position();
    shapes.remove(item);
    removefromscene(item);
    }

    Like

  9. Well, it’s not just something like that, I know how to do animation or call an yield. I mean a real code actually work in your project. How do i call an If like that? How do i figure out when bonus kill a column to start animation from bottom or start from left when it kill a row? I played many match 3 games on store they have a lot of effects, make their games more exciting. Thanks for replying, i think i will do it myself, try & learn more.

    Like

  10. Hi just want to know how to make the gridsize declared because I want to make a new level of it with different grid sizes. I don’t know how to make another set cause the Gridsize is constant.

    Like

  11. Mr. Dgkanatsios

    I am recently learning how to make match3 game in Unity for my first commercial game app. Your tutorial is very easy to understand, and I found that you code base is really good for me to start coding and build new rule on top. I also understand that there are a lot of developers was ask you the same question about if it is fine to use your match3 source code for development. However, i just want to inquire you direct, to let me use and modify your source code freely without charges and legal issue.

    Of course, It would be very much my pleasure to info you when the game is released and have you the enjoy it.

    Thank you very much!

    Like

  12. wow. so cool, I did not know that you can actually fix the previous message?! that’s very cool. (I am not a tech person!!! sorry!!!). I fully understood! You have a great day, Mr. Dgkanatsios.

    Like

  13. Thanks for the Game i made a Flag Destroy with Match 3 i also added Admob advertisement its working fine for me but after 5 to 10 Min its disappear what will be the problem i am still finding

    Like

  14. Hi Mr. Gkanatsios, about the Shape class, you said;
    “It contains … a constructor that initializes the bonus enumeration …”
    “Since Shape is a MonoBehaviour that is attached to our prefabs (as we’ll see later), we cannot use a constructor to initialize it.”

    Could you please clarify;
    – Why did you write a constructor, if Shape is a MonoBehaviour and cannot be used. What is its purpose if it’s not used.
    – Exactly when & where in your code the Shape() constructor is used/called, if it has a purpose.

    Thanks for your tutorial and answers.

    Like

    • Hi, yup, we’re not calling the Shape constructor anywhere. This is (probably) called internally by Unity, so the one line it contains is executed at that time. More proper way would have been to either implement this line at Start() or Awake(), or just initialize the field in the class (public BonusType Bonus = BonusType.None). Thanks!

      Like

  15. Mr. Dgkanatsios
    I was trying to put a new gameplay logic into the system, that is eventually base on many match patterns. But first, I want to keep it simple and step by step. That’s of course, it is something I should figure it out by myself, but I was wondering if there is way to find out the matches is a vertical or horizontal match? I was looking into the MatchesInfo class, but it seems it does not doing any bookkeeping on directional information. I think it is a right class to extend the functionalities. If you have any good advice or pointers, it would be very much appreciated! thank you sir!

    Like

  16. Hello dgkanatsios,
    Thanks for the tutorial, it was good that you even provided the code.
    I want to know is there a possibility to add obstacles that do not move and the candy or shapes move around the obstacle. like how we see in many match 3 games. If yes can you guide me how to add them to the project.

    Like

    • Hi and thanks for the kind words. So, what you ask can be done (of course) but it can be a little tricky. There are two basic parts that obstacle-like functionality should be included.
      1. At the candy comparison. When you compare candies either vertically or horizontally, you should stop when you encounter an obstacle.
      2. At the candy drop, when the user makes a successful match. Candy should *not* drop vertically when an obstacle is directly below them.
      Let me know if you have any more questions!

      Like

  17. Hello dgkanatsios, I have a quick question concerning the Shapes Manager. When I import the code as a new asset in Unity 5.6.1 and open it in visual studio I encounter 5 errors. All five are errors are CS1061 errors and state “error CS1061: Type `UnityEngine.Transform’ does not contain a definition for `positionTo’ and no extension method `positionTo’ of type `UnityEngine.Transform’ could be found. Are you missing an assembly reference?” I was wondering what the problem is, and how I can fix it in order for the game to work.
    -Thanks

    Like

  18. Hello and thanks for the match3 game code.
    My problem is when there are no matches left on board (possible whit a 4×4 or 5×5 game board), a new random board is not created and the game becomes stuck.
    Can you show a workaround for this?

    Like

    • Thanks for the comment. Yeah, I tried to make the tutorial as simple as possible (even though it became much bigger than I originally imagined!). Anyway, what I would suggest you could do is use the Utilities.GetPotentialMatches method. This method returns null if it cannot find any matches at all. So you could use this method to check for null, and, if this is the case, take the user to a next level or restart the level or show a “congrats” message or whatever you like. Hope this helps, Dimitris

      Like

  19. Hello, thanks for the tutorial!

    I was wondering how I would go about referencing specific colors of the candy to increase a score for each. For example clearing blue candies would increase the score for “blue”, and pink candies increase “pink”. How can I “check” for the color and increase the score from there? Thank you!

    Like

    • Thanks for the comment. We’re already checking the Color via the “IsSameType” method on the “Shape” class. That’s where we’re comparing candies to see if they are of the same color.
      To solve your problem, you could have an array (or better, a Dictionary) of scores where each entry in the Dictionary would hold the score for the specified color.

      Like

      • Thanks for the solution!

        I’m now trying to create a new bonus that clears all candies of the same color. I’m following the same steps of the previous bonus but I’m unsure of how to create an IEnumerable like GetEntireColumn, albeit for getting all the candy of the color with which the bonus is swapped.

        Like

  20. Even though I’m no hardcore coder your efficient code and detailed description is, as far as I have seen, the best example for making unique match 3 games. After trying out several other match 3 tutorials/codes this is still the most streamlined. Although it has taken me nearly a week to get to grips with how your routines talk to each other, I have managed to (finally) get a reshuffle routine up and running as well as 2 different bonus styles (adjacent and same color) . I just have a couple of questions though, f#1 is probably easy but I’m a noob with gokit, i use scaleTo (in place of your color tinting for hint tiles, but how to make sure all game objects no longer tween in case I need to restart a puzzleboard? GoTween does capture the error with a yellow warning icon but any easier way of detecting which gameobjects are scaling with goTween? #2 has me pulling out my hair for the last week, how on earth do we make it so that new incoming tiles will also create bonus tiles for match 4/5 instead of just destroying them? Appreciate any help and curious to know what are you doing these days? :)

    Like

  21. Hello Sir. This is an amazing and extensive tutorial, covering many different concepts that are required for development of Match-3 Games.

    Keeping in mind, that I am not a hardcore coder, I have been trying to extend your project, with an intention to add more candy elements and power-ups, and Diagonal Match.
    I have made some attempts to figure out how I could add Diagonal Matching mechanic, but am struggling to figure out how to do that. It would be a great help, if you could give some guidance on how to approach development of such mechanic.

    I tried to break down the requirement and create a potential list of places where the new diagonal mechanic should be added, in order for it to work exactly as horizontal and vertical match.

    With reference to the order in which the description of various components has been provided, I attempted to jot down rough estimates which would be required to be added to different scripts.
    If you can infer the flow and let me know whether I am approaching in a correct direction, then it would be a great help!

    1) In Utilities.cs script, there’s a method “AreVerticalOrHorizontalNeighbors()”, where I think, we would have to add a condition for checking diagonal elements.
    2) In Utilities.cs script, there’s IEnumerable “GetPotentialMatches”, where I think, we would have to add different variants for diagonal checking.
    /* example *\
    * * * * *
    * * * * *
    * * & * *
    * & * * *
    & * * * *
    /* example *\

    /* example *\
    * * * * *
    * * * & *
    * * & * *
    * & * * *
    * * * * *
    /* example *\

    /* example *\
    & * * * *
    * & * * *
    * * & * *
    * * * * *
    * * * * *
    /* example *\ etc.

    3) In ShapesArray.cs script, there are methods for horizontal and vertical checking. Similarly, I guess we would have to add a method here as well, for diagonal checking. Then, there’s also “MatchInfo GetMatches()” method, where I suppose we would have to add the logic for diagonal match.
    Then, would we have to make any changes to “AlteredCandyInfo()” method?

    4) In ShapesManager.cs script, there’s “InitializeCandyAndSpawnPositions()” method, where we would again have to add condition for diagonal match.

    That’s the rough Idea I got, but I am having issues with regards to actual implementation of the core logic responsible for getting the diagonal match, work exactly as horizontal and vertical matches. How can I do that?

    I cannot appreciate enough, about your development of this blog post, the Match3 project, and any form of help/advice that you might provide in future.

    Kind Regards!

    Like

  22. Hello Sir.
    This is an amazing tutorial. Thank you very much sharing your knowledge.

    I have been trying to extend this project, where I intend to add more bonus candies. Is there any possible way on how to do that?
    I have been trying to figure out where exactly does the core logic for bonus reside, but having a hard time in figuring it out. Also, confused about how to structure any other bonus functionality, that mimics the flow of bonus candy that you have already implemented.

    Any help/advice with regards to that, would be much appreciated.

    Like

Leave a comment