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
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
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).
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)
CheckVertical methods search for these patterns
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.
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.
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.
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.
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
Hello! Love your tutorial. Will the code work even if we only use MonoDevelop?
LikeLike
Yes, of course. In the end, Unity will compile and run the code, the editor is just a tool to help you write your scripts.
LikeLike
[…] is a method similar to one I used when I developed the match-3 game mechanism. It loads a file called “debugLevel” from the Resources folder. File contains integers (that […]
LikeLike
[…] to jump into the mediocre performance conclusion were two pretty simple 2D games I’ve built; the match-3 game and the puzzle one. Frame rate was 2-3 frames per second, making the performance totally […]
LikeLike
Thank you for such a wonderful tutorial. Seeing a really well made Match-3 really helped me think about the deeper issues of this kind of project.
LikeLiked by 1 person
Thank you for doing such a wonderful tutorial! Seeing such a well made match-3 example really helps me to think about the deeper issues of such a project.
LikeLiked by 2 people
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
LikeLike
You can do whatever you wish, good luck! Let me know if you find any bugs :)
LikeLike
Hi, thanks dgkanatsios
LikeLike
can u plz provide code…u give link but it’s didn’t work…:(
LikeLiked by 1 person
Hi, what link is not working? Links to my GitHub profile do work correctly: https://github.com/dgkanatsios/matchthreegame
LikeLike
thanku dgkanatsios:):) thanku so much..:):)
LikeLike
Can anyone use it for their own game?
LikeLike
Of course! A link to this blog post would be great, but not obligatory. Also, feel free to let me know about the game as I’d love to share and play it!
LikeLike
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.
LikeLike
Can you point me to the Disco Panda game? Can’t find it
LikeLike
it is like best fiends, that should be easier to find.
LikeLike
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?
LikeLike
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.
LikeLike
Thanks for the kind words :)
LikeLike
uhmmm sir dgk what version of unity did u use for making this project?
LikeLike
hello sir dgk would like to ask what ver. of Unity did you used for this project? Btw this looks cool!
LikeLike
It was created on 4.6, have tested it with 5.2. Thanks!
LikeLike
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
LikeLike
The question is how to do match 5, correct? Here is the part that I’m getting all of the candy matches in an array. You can easily check if this array has more than or equal to 5 items and do your stuff!
https://github.com/dgkanatsios/MatchThreeGame/blob/master/Assets/Scripts/ShapesManager.cs#L268
Hope that helps,
Dimitris
LikeLike
Hi Dimitris,
thanks for your advice,
if you could take a look at my repo and give some help that would be appreciated. I have the 5 match working, but it randomly bugs out with the following message:
NullReferenceException: Object reference not set to an instance of an object
ShapesArray.GetMatchesHorizontally (UnityEngine.GameObject go) (at Assets/Scripts/ShapesArray.cs:244)
ShapesArray.GetMatches (UnityEngine.GameObject go) (at Assets/Scripts/ShapesArray.cs:113)
ShapesArray.GetMatches (IEnumerable`1 gos) (at Assets/Scripts/ShapesArray.cs:100)
ShapesManager+c__Iterator1.MoveNext () (at Assets/Scripts/ShapesManager.cs:412)
Any help would be appreciated as i cant seem to wrap my head around it.
Cheers,
LikeLike
forgot the repositry link! https://github.com/Westerveld/starRush
LikeLike
Have you tried using Visual Studio tools for Unity to debug your code? It could be proven really useful in finding why this exception is thrown.
https://unity3d.com/learn/tutorials/topics/scripting/debugging-unity-games-visual-studio
LikeLike
I’ve tried using the debug method within visual studio, i know where the code breaks, but i can’t seem to find a work around. any ideas? (its within shapesarray line 227)
LikeLike
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.
LikeLike
Okay, will give that a go in a bit, thanks for your quick replies!
LikeLike
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.
LikeLike
Can you please issue a GitHub issue (if you can also submit a fix, that would be great!)?
LikeLike
How to add target score
LikeLike
When the player does a match and the game increases the score, you can compare it to a target one and act accordingly. Hope I understood your question correctly!
LikeLike
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
LikeLike
Single() throws this, probably because there are more than one elements on the list.
LikeLike
thanks for you response. i think i need to learn more, thanks anyway
LikeLike
Hi dimitris I’m completely new to game development so maybe my question is stupid but i wonder how i can make level animation. You know the animation between each level that we can see in many game for example a character progressing on a road every times you achieve a level http://www.gamasutra.com/db_area/images/blog/262755/Title.jpg
LikeLike
Hi, you could use the GoKit library (or any other animation library you like) to animate a GameObject from one position to another.
LikeLike
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
LikeLike
Hi, you can place an empty GameObject on the screen in the position you wish and add an image to it programmatically.
LikeLike
thank you for your response
LikeLike
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);
}
LikeLike
Are you starting an animation there? If yes, you could yield return new WaitForSeconds(animation_duration) there before removing the shapes.
LikeLike
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.
LikeLike
Have you had any luck? All magic happens in this function: https://github.com/dgkanatsios/MatchThreeGame/blob/master/Assets/Scripts/Constants.cs#L9 I should have made it smaller, to be honest, but anyway you can see what happens when a bonus is to be added.
LikeLike
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.
LikeLike
Hi, have you tried changing the values on Constants.cs file? https://github.com/dgkanatsios/MatchThreeGame/blob/master/Assets/Scripts/Constants.cs#L9
LikeLike
Yes but when I did all of the 5 scenes GridSize I created by following your tutorial will be equal. I just want it to be different for example
Level1 8×8
Level2 9×8
Level3 9×9
Level2 8×10
Level3 10×10
LikeLike
Ah OK. In this case, I would have different variables for each level.
LikeLike
Don’t mind the level names
LikeLike
Can you please give me a sample sir?
LikeLike
Well, the most simple way, if you are sure you will have a certain amount of levels is load them into an array. Hence, for 3 levels you could do
public static readonly int[] Rows = {4,5,6}; //level 1: 4 rows, level 2: 5 rows etc.
public static readonly int[] Columns = {8,9,10}; //level 1: 8 rows, level 2: 9 rows etc.
And then, in https://github.com/dgkanatsios/MatchThreeGame/blob/master/Assets/Scripts/ShapesManager.cs#L109
you could write Constants.Rows[0] for level 1, Constants.Rows[1] for level 2 etc.
Hope this helps!
LikeLike
Will that work in a multiple scenes?
LikeLike
well, what you have to do is select the proper value in the array depending on the scene you are!
LikeLike
Can you help me sir on how to do that? That will help me a lot sir.
LikeLike
public static readonly int[] Rows = {4,5,6}; //level 1: 4 rows, level 2: 5 rows etc.
public static readonly int[] Columns = {8,9,10}; //level 1: 8 rows, level 2: 9 rows etc.
And then, in https://github.com/dgkanatsios/MatchThreeGame/blob/master/Assets/Scripts/ShapesManager.cs#L109
you could write Constants.Rows[0] for level 1, Constants.Rows[1] for level 2 etc.
I tried this method but it gives me a lot of error in ShapesArray.cs, DebugUtilities.cs, and in Utilities.cs
LikeLike
Yup, you need to make some changes in the code, depending on how you have built your level/scene flow.
LikeLike
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!
LikeLike
Thanks for the kind worlds. Feel free to use the code, check the license on the GitHub repository: https://github.com/dgkanatsios/MatchThreeGame/blob/master/License.md
LikeLike
Mr. Dgkanatsios, thanks for your speed reply. (just to double check, you wrote “feel free to SUE the code”, you guess you meant “feel free to USE the code”, right?), so for being so careful!
you have a great day!
Cheers!
LikeLike
Haha, correct, fixed now :)
LikeLike
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.
LikeLike
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
LikeLike
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.
LikeLike
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!
LikeLike
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!
LikeLike
Check here for the GetMatchesHorizontally method https://github.com/dgkanatsios/MatchThreeGame/blob/master/Assets/Scripts/ShapesArray.cs#L177
You’ll also see the GetMatchesVertically method nearby. thanks!
LikeLike
Thank you so much for your pointers! it works perfectly. :)
LikeLike
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.
LikeLike
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!
LikeLike
thanks for this wonderful tutorial!
guys how to add levels??
LikeLike
Thanks! You could replicate the one that is demonstrated here
LikeLike
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
LikeLike
positionTo is implemented in GoKit https://github.com/dgkanatsios/MatchThreeGame/blob/586054556ef8fb093d50faab18c30d71842daafc/Assets/Plugins/GoKit/extensions/GoKitTweenExtensions.cs
LikeLike
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?
LikeLike
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
LikeLike
[…] Building a match-3 game (like Candy Crush) in Unity […]
LikeLike
@dgkanatsios: can you tell me how much time you’ve invested to create this code?
LikeLike
Not sure, since it’s been a couple of years since I wrote it. I would guess two weeks part-time, tops
LikeLike
what if I want to get matches diagonally?
LikeLike
You could create another method that does exactly that. It would be similar to the GetMatchesHorizontally and GetMatchesVertically methods
LikeLike
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!
LikeLike
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.
LikeLike
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.
LikeLike
[…] taken from this tutorial https://dgkanatsios.com/2015/02/25/building-a-match-3-game-in-unity-3/ Script: AlteredCandyInfo.cs, Constants.cs, DebugUtilities.cs, Enums.cs, MatchesInfo.cs, Shape.cs, […]
LikeLike
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? :)
LikeLike
Terrific tutorial. I’ve forked the repo and updated the project to run in Unity 2019.4. https://github.com/tacman/MatchThreeGame/ I can make a PR to your repo if you’d like these changes.
I also published the game at https://tacman1123.itch.io/match-three, the link to the demo at this beginning of this tutorial is broken.
LikeLike
wow, thank you! Yes, I’d love a PR, also edit the blog post to point to your website!
LikeLike
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!
LikeLike
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.
LikeLike