The Algorithms Behind Match-3 Games
Match-3 games are deceptively simple. To the player, it's just colorful candy falling on a screen. But to a developer, a game like Candy Crush or Royal Match is a complex orchestration of array manipulation, recursive search algorithms, and state management.
If you try to build a Match-3 game using Unity's physics engine (Rigidbodies and Colliders), you will fail. The physics will be unpredictable, the matching logic will be buggy, and performance will tank on mobile devices.
Professional Match-3 games are Deterministic. They don't rely on physics; they rely on grid math. In this guide, we will break down the core pillars of Match-3 architecture: The Data Structure, Match Detection, Gravity, and Deadlock Prevention.
1. The Model-View-Controller (MVC) Approach
The most important rule in puzzle game development is identifying the separation between data and visuals.
- The Model (Data): A 2D Array of integers in memory. This is the "Truth."
- The View (Visuals): The GameObjects, Sprites, and Animations that the player sees.
When a player swaps two gems, we don't swap the GameObjects first. We swap the numbers in the array. Then, the View updates to match the array.
The Grid Structure
We usually use a simple 2D array. Integers are faster and lighter than classes.
public class Board {
// 0 = Empty/Air
// 1 = Red, 2 = Blue, 3 = Green, 4 = Yellow, 5 = Purple
// 6 = Bomb, 7 = Rocket
public int width = 8;
public int height = 8;
public int[,] grid;
public void Init() {
grid = new int[width, height];
}
}
2. Detecting Matches (The Sweep)
There are two types of match detection: Targeted and Global.
A. Targeted Swap Check
When a player swaps Gem A at `(x, y)` with Gem B at `(x+1, y)`, we only need to check matches originating from those two specific coordinates. We check horizontally and vertically.
bool CheckMatchesAt(int x, int y) {
int currentType = grid[x, y];
if (currentType == 0) return false;
// Horizontal Check
if (x > 0 && x < width - 1) {
if (grid[x - 1, y] == currentType && grid[x + 1, y] == currentType)
return true;
}
// Vertical Check
if (y > 0 && y < height - 1) {
if (grid[x, y - 1] == currentType && grid[x, y + 1] == currentType)
return true;
}
return false;
}
B. Global Board Scan
After matches are cleared and new gems fall in (Gravity), we must scan the entire board for "Chain Reactions." This is usually done by iterating through every cell and checking for 3 consecutive IDs.
3. The "Toon Blast" Algorithm: Flood Fill
Some games (like Toon Blast or Pet Rescue Saga) don't use swapping. You tap a group of blocks to destroy them. This requires the Flood Fill algorithm (specifically, Connected Component Labeling).
This is a classic recursive search (Depth First Search).
// Returns a list of all connected matching blocks
List GetConnectedGroup(int startX, int startY) {
int targetColor = grid[startX, startY];
List matches = new List();
// We need to keep track of visited cells to prevent infinite loops
bool[,] visited = new bool[width, height];
// Recursive helper function
FloodFill(startX, startY, targetColor, visited, matches);
return matches;
}
void FloodFill(int x, int y, int color, bool[,] visited, List matches) {
// Boundary Checks
if (x < 0 || x >= width || y < 0 || y >= height) return;
if (visited[x, y]) return;
if (grid[x, y] != color) return;
visited[x, y] = true;
matches.Add(new Vector2Int(x, y));
// Search Neighbors (Up, Down, Left, Right)
FloodFill(x + 1, y, color, visited, matches);
FloodFill(x - 1, y, color, visited, matches);
FloodFill(x, y + 1, color, visited, matches);
FloodFill(x, y - 1, color, visited, matches);
}
Recursion Warning
Recursion is elegant but can cause a "Stack Overflow" on massive boards (e.g., 100x100). For extremely large grids, convert this to an Iterative approach using a Stack<Vector2Int> data structure instead of function calls.
4. Gravity Logic (Falling Gems)
Once matches are found, we set those grid cells to `0` (Empty). Now we must make the gems above them fall.
Do NOT use physics. Physics is expensive. Use a Column-based shift algorithm.
- Iterate through each column from bottom (`y = 0`) to top.
- If we find an empty cell (`0`), look above it for the first non-empty cell.
- Move that non-empty cell down to the empty slot.
- Repeat until all empty slots are at the top of the column.
- Spawn new random gems in the empty slots at the top.
5. Deadlock Detection (Is the game over?)
How does the game know to show the "No Moves Left" popup? It doesn't guess. It simulates.
Every time the board settles, the game runs a "Virtual Swap" simulation in the background. It brute-forces every possible move on the board.
6. Performance Optimization
Mobile devices have limits. Here is how to keep your Match-3 game running at 60 FPS.
Object Pooling
In a typical level, you might destroy 500 gems. If you use `Destroy(gameObject)` and `Instantiate(prefab)` 500 times, the Garbage Collector (GC) will cause lag spikes.
Instead, when a gem matches, disable it (`SetActive(false)`) and move it to a "Pool" list. When you need a new gem at the top, take one from the Pool, reset its position, and enable it.
Dirty Flags
Do not redraw the entire board every frame. Use a `bool isDirty` flag.
Only run your visual update logic if `isDirty == true`. The logic sets the flag to true whenever a swap happens or gravity finishes.
Conclusion
Match-3 games are a perfect example of how complex logic can be hidden behind simple interactions. The player sees exploding candy; the developer sees a beautifully optimized 2D array utilizing recursion and predictive algorithms.
At Boomie Studio, we use these exact algorithms for our titles like Hexa Jam. By mastering the grid, you master the genre.
Frequently Asked Questions (FAQ)
Q: Should I use a 1D array or 2D array?
A: A 1D array (`int[] grid`) is technically faster in memory due to cache locality, but a 2D array (`int[,]`) is much easier to read and debug. For a board size of 8x8 or 12x12, the performance difference is negligible. Stick to 2D for simplicity.
Q: How do I handle special matches (e.g., L-Shape or T-Shape bombs)?
A: When a match is detected (e.g., 5 gems), check the shape of the match coordinates. If the matched list contains both a horizontal set of 3 and a vertical set of 3 sharing a central gem, instantiate a Bomb instead of clearing them.