So far, this series has covered the basics of setting up a Match-3 game, and implementing the initial gameplay elements such as block-swapping. In this tutorial, we are going to build on all of that, and start detecting when the player has made a match.
Final Game Demo
Here is a demo of the game we’re working towards throughout this series:
1. Detecting a Match
For now, we are only going to implement a basic version of the matching system, focusing on finding when matches exist and destroying matched blocks. In later articles we will continue developing and advancing the system.
Tip: You should read into how a recursive function works, if you don’t already know; essentially, it is a function which calls itself. Recursive functions can work similarly to loops, but since they can also take in and return variables, they have many more uses than loops do.
As with the previous tutorial, I first want to discuss how the system will work, and then attempt to build it.
- The match system will iterate through each instance of the
Block
object. - For each
Block
, it will pass the color and position of the block it’s looking at into a recursive function, which will look at the horizontal or vertical neighbor, and determine whether they are the same color. - If a match is found, it will call the function again with the position and color of the new block, rather than the original one.
- This will continue until it finds no match. At that point, it will check how many matches it found.
- If it found three or more matches, it marks all the blocks it just looked at as
Matched
with theIsMatched
instance variable we made in one of the previous tutorials; otherwise, it does nothing. - Finally, once all of the blocks have been checked, the function will destroy every block that’s marked as a match.
First, we need an event which can iterate through each Block
. The way I built the system, it actually iterates through the blocks twice: once to check for vertical matches, and once to check for horizontal matches. Depending on which check it is doing, it will use a different function to actually look for the match.
The very first thing we need to do is make a Global Variable to keep track of how many matching blocks we’ve found in any given iteration:
Global Variable Name: "NumMatchesFound" Type = Number Value = 0
Now, let’s make the Event that will iterate through the blocks:
Event: Function > On function Name: "FindMatches" Sub-Event: System > For Each Object: Block Action: System > Set value NumMatchesFound = 1 Action: Function > Call function Name: "CheckMatchesX" Parameter 0: Block.X Parameter 1: Block.Y Parameter 2: Block.Color Sub-Event: System > Compare Variable NumMatchesFound >= 3 Action: Block > Set Boolean IsMatched = True Sub-Event: System > For Each Object: Block Action: System > Set value NumMatchesFound = 1 Action: Function > Call function Name: "CheckMatchesY" Parameter 0: Block.X Parameter 1: Block.Y Parameter 2: Block.Color Sub-Event: System > Compare Variable NumMatchesFound >= 3 Action: Block > Set Boolean IsMatched = True Sub-Event: Block > Is Boolean instance variable set System > Wait Second = 0.1 Block > Destroy
Your code should look like this:
In this event, we iterate through every block and sending them into CheckMatchesX
or CheckMatchesY
, the functions which will check to see if the neighboring Block is a match.
To send the block into the function, we pass the functions three different parameters:
- Parameter 0 is the Block’s X position
- Parameter 1 is the Block’s Y position
- Parameter 2 is the color.
After each Block is sent into one of the functions and the function finishes running, it checks NumMatchesFound
to see if it found three or more matching Blocks, and then labels the Blocks as Matched
if it did.
Finally, every Block that is marked as being Matched
gets destroyed after .1 seconds passes. This wait
statement is there to allow the game to switch the images for the Blocks to the image that indicates they are matched, and to give the player a moment to notice this change.
(While you could remove the wait
statement without negatively impacting the gameplay, it makes the matching easier for the player to understand, and slows down the game just enough so that the player can easily keep track of what is going on.)
2. The Two Check Functions
Next we need to make the CheckMatchesX
and CheckMatchesY
functions. These functions will work similarly to the iterators above, in that there will be one version for checking horizontal matches, CheckMatchesX
, and one for vertical matches, CheckMatchesY
.
Horizontal Checks
First, let’s build the horizontal check function:
Event: Function > On function Name: "CheckMatchesX" Sub-Event: Condition: Block > Compare X X = Function.Param(0) + (Block.Width+2) Condition: Block > Compare Y Y = Function.Param(1) Condition: Block > Compare instance variable Color = Function.Param(2) Action: System > Add to Variable = NumBlocks Value = 1 Action: Function > Call function Name: "CheckMatchesX" Parameter 0: Function.Param(0) + (Block.Width+2) Parameter 1: Function.Param(1) Parameter 2: Function.Param(2) Sub-Event: System > Compare Variable NumMatchesFound >= 3 Action: Block > Set Boolean IsMatched = True
Your code should look like this:
So, what is this function doing?
- First, it tests to see whether a neighboring block even exists to the left of the block we passed in.
- Once the function confirms there is a Block in the neighboring location, it checks whether it is the same color as the Block we passed in.
- If it is, it increases
NumMatchesFound
by one, and passes the newly found Block into the function just like it did for the original. - This continues until it finds a Block which isn’t the same color as the original. At that point it checks to see if it found enough matching blocks to create a group, and labels the blocks as matches if it did.
Vertical Checks
Now, let’s make another version of this function which will do the same thing for vertical matches. This is going to be our CheckMatchesY
function. You can either copy the original function and make all the appropriate changes, or just build it again from scratch; in either case, here is how your function should look when it is finished:
Event: Function > On function Name: "CheckMatchesY" Sub-Event: Condition: Block > Compare X X = Function.Param(0) Condition: Block > Compare Y Y = Function.Param(1) + (Block.Width+2) Condition: Block > Compare instance variable Color = Function.Param(2) Action: System > Add to Variable = NumBlocks Value = 1 Action: Function > Call function Name: "CheckMatchesY" Parameter 0: Function.Param(0) Parameter 1: Function.Param(1) + (Block.Width+2) Parameter 2: Function.Param(2) Sub-Event: System > Compare Variable NumMatchesFound >= 3 Action: Block > Set Boolean IsMatched = True
Your code should look like this:
3. Actually Looking for Checks
Finally, we need to actually call the FindMatches
function. Go to the SwapBlocks
function and add a new sub-event to the end of the function:
Event: Function > Sub-Event: Action: Function > Call function Name: "FindMatches"
You’ll notice that this sub-event doesn’t actually have any conditions. If you’ve never made a sub-event like this before, just make a sub-event with any condition at all, since it requires you to give a condition when making a sub-event, and then delete the condition, but leave the sub-event. This way, you make sure the sub-event always run.
Your SwapBlocks
event should now look like this:
If you run the game at this point, you will see that the blocks get destroyed when matches occur. You’ll also notice though that any matches that are there when the game begins don’t disappear until you make a swap of some kind. This is because we never call the FindMatches
function after we create the grid of blocks.
The reason we haven’t added this code is because in the final version there will be another function which prevents matches from being auto-generated like this, so there is really no reason to worry about this problem at all. (But feel free to call the FindMatches
function earlier, if you like.)
4. Consolidating the Checks
At this point, we have a pretty strong matching system, but the problem is that our code is redundant. Currently, we have two different functions that check to see if there is a matching neighbor, and the only difference between them is that one checks vertically, and the other checks horizontally.
Since the free version of Construct 2 limits how many Events we can have, this is definitely a waste. To solve this, we are going to make a new version of the function that can do both checks.
If you look at the function, you will see the only difference between the two versions is that one adds Block.Width + 2
to the x-position of the Block, and the other adds it to the y-position of the Bock. So, the obstacle we have to get past to make this a single function, is giving the function a way to add Block.Width + 2
to only X
, or only Y
, without using an If
statement or multiple functions, since those require more Events to be executed.
My solution to this is not very complex, but it will be easier to understand if we can see it come together, so we will implement it, and I will explain how it works once we can see it all in action.
- Delete the
CheckMatchesY
event. - Rename the
CheckMatchesX
event to, simply,CheckMatches
. - In the function call for
CheckMatchesX
under theFindMatches
event:- Modify the function call to be for
CheckMatches
instead ofCheckMatchesX
. - Add
Parameter 3
.- Value =
1
.
- Value =
- Add
Parameter 4.
- Value =
0
.
- Value =
- Modify the function call to be for
- In the function call for
CheckMatchesY
under theFindMatches
event:- Modify the function call to be for
CheckMatches
instead ofCheckMatchesY
. - Add
Parameter 3
.- Value =
0
.
- Value =
- Add
Parameter 4
.- Value =
1
.
- Value =
- Modify the function call to be for
As I will explain soon, these added parameters will tell CheckMatches
whether it is doing a horizontal check or a vertical check. When we send in 1
for Parameter 3
, and 0
for Parameter 4
, it is a horizontal check, and when we send in 0
for Parameter 3
, and 1
for Parameter 4
, it is a vertical check.
Now, go back to the CheckMatches
function, and modify the conditions and actions to look like this:
Event: Function > On function Name: "CheckMatches" Sub-Event: Condition: Block > Compare X X = Function.Param(0) + ((Block.Width+2)*Function.Param(3)) Condition: Block > Compare Y Y = Function.Param(1) + ((Block.Width+2)*Function.Param(4)) Condition: Block > Compare instance variable Color = Function.Param(2) Action: Block > Set Boolean IsMatched = True Action: Function > Call function Name: "CheckMatches" Parameter 0: Function.Param(0) + ((Block.Width+2)*Function.Param(3)) Parameter 1: Function.Param(1) + ((Block.Width+2)*Function.Param(4)) Parameter 2: Function.Param(2) Parameter 3: Function.Param(3) Parameter 4: Function.Param(4) Sub-Event: System > Compare Variable NumMatchesFound >= 3 Action: Block > Set Boolean IsMatched = True
This is what your FindMatches
and CheckMatches
code should now look like:
How does this work?
So, what is this new version of the function actually doing?
Well, whenever you call CheckMatches
you are now sending two more parameters, and rather than adding Block.Width + 2
to either the x- or the y-position, it is adding (Block.Width + 2) * Function.Param(3)
to the x-position, and (Block.Width + 2) * Function.Param(4)
to the y-position.
Since one of those two parameters will always be 1
, and the other will always be 0
, this means that either the x- or the y-position will be modified – never both!
For instance, if we pass in 1
for Parameter 3
, and 0
for Parameter 4
, then it adds (Block.Width + 2) * 1
, which is simply Block.Width + 2
, to the x-position, and (Block.Width + 2) * 0
, which is 0
, to the y-position.
Here is a quick example to show what I mean and how it calculates the position of the block where it will check for the match. Let’s say that in this example the original Block is at (200, 200)
, and the Blocks have a width of 40
. So, if we want to get the position of the neighboring vertical Block, the formulas would work out like this:
X = 200 + ((Block.Width + 2)*0) = 200 + (40 + 2)*0 = 200 + 0 = 200
Y = 200 + ((Block.Width + 2)*1) = 200 + (40 + 2)*1 = 200 + 42 = 242
If we wanted to get the position of the neighboring horizontal Block, the formulas would work out like this:
X = 200 + ((Block.Width + 2)*1) = 200 + (40 + 2)*1 = 200 + 42 = 242
Y = 200 + ((Block.Width + 2)*0) = 200 + (40 + 2)*0 = 200 + 0 = 200
If you run the game now, you should see the match system still works the way it originally did, but from our perspective, it is actually a better system.
Conclusion
At this point our match detection function is still incomplete, but we’ve done a lot in this tutorial already and I think it’s important to let all of this sink in before we add anything else. With that in mind, I am going to end this article here. Check out the demo in its current form.
In the next article we will be adding a points system,we will improve the matching system, and we will add “gravity” so that the Blocks will fall when Blocks below them are eliminated.
If you want to get a head start on the next article, take some time to consider how you would detect when there is an empty space below a Block. Try looking at the Block > Is Overlapping at Offset
function for inspiration!