In this tutorial, we will create a match 3 puzzle game. Match 3 is a puzzle game in which players move multiple colorful pieces evenly arranged along a grid on the board to eliminate three or more pieces of the same color in a row. This genre is particularly popular among mobile game players because it is easy to operate and enjoyable.

Candy Crush, Toon Blast, and Royal Match are just a few examples of popular games. Puzzle & Dragons and LINE Tsum Tsum are also based on Match 3, although the controls are slightly different. In this tutorial, we will create a puzzle like Candy Crush, in which the pieces are moved only one square at a time to match colors. If you want to make a game like “LINE tsum tsum”, check another tutorial, “Connecting matching colors puzzle game in Godot ”.

The final project file for this tutorial is located at GitHub repository . If you download the .zip file and import the “project.godot” file in the “End” folder with the Godot Engine, you can check the project directly.


Environment
This tutorial was created in the following environment

Godot version: 3.4.4
Computer OS version: macOS 11.6.5

Memo:
Please also use the following articles to help you start creating your game.
Downloading Godot
Project Manager of Godot



Creating a new project

Start Godot Engine and create a new project. You can name your project as you like. If you can’t think of one, let’s call it “Match3 Start”.


Updating project settings

When the editor appears, let’s go ahead and update project settings for the entire project.

First, set the display size for the game. In this case, we will assume a smartphone screen in portrait orientation and set the aspect ratio to 16 : 9.

  1. Open the “Project” menu > “Project Settings”.
  2. In the “General” tab, search for “window” and select “Display” > “Window” in the sidebar.
  3. In the “Size” section, change the values of the following items
    • Width: 630
    • Height: 1120
    • Test Width: 315
    • Test Height: 560
      project settings - Display - Window - Size
  4. In the “Stretch” section, change the values of the following items
    • Mode: 2d
    • Aspect: keep
      project settings - Display - Window - Stretch

With the “Project Settings” window open as it is, configures the settings to substitute the mouse for the phone’s touch operation in the debug panel.

  1. Search for “mouse” in the “General” tab and select “Input Devices” > “Pointing” in the sidebar.
  2. Check “On” for “Emulate Touch From Mouse”.
    Input Devices - Pointing - Emulate Touch From Mouse

In addition, with the “Project Settings” window open, add an action to the input map corresponding to a smartphone touch operation.

  1. Switch to the “Input Map” tab and add “touch” to the action.
  2. Add a left mouse click to the “touch” action.
    Inputmap - action - tap

Downloading and importing assets

Next, let’s download assets from KENNEY’s site and use them. This time, we will use an asset pack called “Physics Assets ”. I’m going to use the cute alien face images in this asset pack as textures for the pieces on the game board. I can’t help but be thankful for this wonderful free resource.

After downloading, delete the images in the “/physicspack/PNG/Aliens” folder, leaving only the one with the file name “~_round.png” and drag the “Aliens” folder into the file system dock of the editor to import it into your project.



Creating a Grid scene

First, let’s create a “Grid” scene as the board on which the pieces are placed in a match 3 puzzle game.

  1. Select “Scene” menu > “New Scene”.
  2. Select “2D Scene” in “Generate Root Node”.
  3. When the root node of the “Node2D” class is generated, rename it to “Grid”.
  4. Save the scene. Create a folder and save the scene with the file path “res://Grid/Grid.tscn”.

Adding a node to the Grid scene

Let’s add another child node of class “Node2D” to the “Grid” root node and rename it to “PiecesContainer”. This node is used to organize the pieces placed on the board. During the game, when instances of the pieces are created by the script, they will all be added as children of this “PiecesContainer” node.

The scene tree dock should now look like the following.
scene tree dock

Note that there is no need to edit the properties of the “Grid” scene node.

Translated with www.DeepL.com/Translator (free version)



Creating a Piece scene

Next, create a “Piece” scene for the pieces to be placed on the board. However, this “Piece” scene is only a blueprint, and the actual pieces of each color to be used in the game will be prepared later by inheriting this “Piece” scene.

  1. Select “Scene” menu > “New Scene”.
  2. Select “2D Scene” in the “Create Root Node” section.
  3. When the root node of “Node2D” class is generated, rename it to “Piece”.
  4. Save the scene. Create a folder and save the scene with the file path “res://Pieces/Piece.tscn”.

Adding a child node to the Piece node

Let’s add a child node of class “Sprite” to the “Piece” root node. The scene tree dock should now look like the following.
Piece scene tree


Editing the properties of the Sprite node

Let’s edit a few properties of the “Sprite” node. As mentioned above, the “Piece” is only a blueprint, so we will leave the “Texture” property of the “Sprite” node as it is without applying any resources to it in this scene. In the inherited scene, apply an image that matches the color of each piece.

Change the value of the “Offset” property to “(x: 35, y: -35)”. In the scene of each piece of color that inherits this scene, we will apply the KENNEY image imported earlier to the “Texture” property. Since the size of the image is 70 px in height and width, we shifted the center of the image to the upper right and set the lower left corner of the image to (x: 0, y: 0).
Piece scene - Sprite - offset

The grid of the board on which the pieces are placed is set to count from left to right on the x-axis and from bottom to top on the y-axis, and the size of one grid on the board is set to 70 px to match the size of the texture. If the lower left corner of the piece’s texture image is aligned with (x: 0, y: 0), then when the “Piece” root node is positioned (x: 0, y: 0) to the grid, the “Sprite” image will be placed exactly along the grid.
Diagram - Sprite offset ajusted to grid


Attaching and editing a script to the Piece node

Let’s attach a new script to the “Piece” root node. Create a script file with the file path “res://Pieces/Piece.tscn”.

Edit the script as follows.

extends Node2D

# Property to set the color of the piece as string data
export (String) var color
# Property to indicate matched (3 or more of the same color in a row)
var matched = false
# Reference to a Sprite node
onready var sprite = $Sprite

# Method to move a piece
# Move the Piece instance to the position passed in the target argument
func move(target):
	position = target

# Method called when a match is found (3 or more of the same color in a row)
# Set the matched property to true and make the color translucent
func make_matched():
	matched = true
	sprite.modulate = Color(1,1,1,.5)

This completes the editing of the “Piece.gd” script.



Creating a scene for each color that inherits from the Piece scene

Now that the “Piece” scene, which will serve as a template, is complete, let’s create a scene that inherits the “Piece” scene in the number of colors of the piece. There are five Piece colors: beige, blue, green, pink, and yellow. First, let’s proceed with the steps using the “beige” drop as an example.

  1. Select “Scene” menu > “New Inherited Scene”.
  2. Select “Piece.tscn” as the source scene.
  3. After the inherited scene is generated, rename the root node to “PieceBeige”.
    *The name of this root node should match the color of each drop.
  4. Save the scene once. Save the file path as “res://Pieces/PieceBeige.tscn”.
  5. With the root node “PieceBeige” selected in the scene tree dock, set the value of the “Color” property of “Script Variables” to “beige” in the inspector.
    PieceBeige - Color property
  6. Select the “Sprite” node in the scene tree dock and apply the previously imported resource “res://Aliens/alienBeige_round.png” to the “Texture” property (you can drag it from the file system dock).
    Sprite - Texture Region
  7. On the 2D workspace it should now look like the following screenshot.
    Sprite - Texture Region
  8. The “PieceBeige” scene is now complete. Follow the same procedure to create scenes for the remaining four colored pieces. Please refer to the following for details on the different parts of each scene.
  • Blue Piece
    • Root node name: PieceBlue
    • Color property: blue
    • Sprite > Texture property: res://Aliens/alienBlue_round.png
    • File path when saving the scene: res://Pieces/PieceBlue.tscn
  • Green Piece
    • Root node name: PieceGreen
    • Color property: green
    • Sprite > Texture property: res://Aliens/alienGreen_round.png
    • File path when saving the scene: res://Pieces/PieceGreen.tscn
  • Pink Piece
    • Root node name: PiecePink
    • Color property: green
    • Sprite > Texture property: res://Aliens/alienPink_round.png
    • File path when saving the scene: res://Pieces/PiecePink.tscn
  • Yellow Piece
    • Root node name: PieceYellow
    • Color property: yellow
    • Sprite > Texture property: res://Aliens/alienYellow_round.png
    • File path when saving the scene: res://Pieces/PieceYellow.tscn

When the inherited scenes of all 5 colored pieces have been created, the work is complete.



Controlling the Grid scene with scripts

Now that we have created a scene for each color piece, we can now program and control the game. The amount of code is a bit large this time, so let’s do our best.

After switching to the “Grid.tscn” scene, attach a new script to the “Grid” root node. The file path should be “res://Grid/Grid.gd”.

In the comments in the script, “finger touched” or “finger released” should be replaced with “left mouse button pressed” or “left mouse button released” on the Godot debug panel.

Also, please note that “match” is defined as three or more of the same color.

Now, once the script editor is open, let’s define the necessary properties.

### Grid.gd ###

extends Node2D

# An array with scene files for each color piece as elements
const pieces_scn = [
	preload("res://Pieces/PieceBeige.tscn"),
	preload("res://Pieces/PieceBlue.tscn"),
	preload("res://Pieces/PieceGreen.tscn"),
	preload("res://Pieces/PiecePink.tscn"),
	preload("res://Pieces/PieceYellow.tscn")
]

# Number of grids in x-axis direction
var width: = 7
# Number of grids in y-axis direction
var height: = 10
# Grid start position (pixels) in x-axis direction
var x_start: = 70
# Grid start position (pixels) in y-axis direction
var y_start: = 910
# Size of one grid (should be the same as Texture of Sprite in Piece)
var grid_size: = 70

# A two-dimensional array (initially empty) that manages all the pieces of the board as elements and their grid coordinates
var all_pieces = [].

# Position where finger touches the screen
var touched_pos = Vector2()
# The position where the finger leaves the screen
var released_pos = Vector2()

# State where finger is touching the screen, touched: true / away: false
var is_touching = false
# State of automatic processing of matching, processing: true / stopped: false
var is_waiting = false

# Referencing the PiecesContainer node
onready var pieces_container = $PiecesContainer

Then let’s add the following method. Note that the two-dimensional array that appears in the following code is an array that stores arrays as elements.

In the case of the two-dimensional array used in this script, the first level of the array contains as many empty arrays as the number of grids in the x-axis direction of the board, and the second level of the array contains as many elements as the number of vertical grids in each array. By storing piece objects as elements, it is possible to manage where each piece is located on the board (how many grids along the x-axis and how many grids along the y-axis).

### Grid.gd ###

# Function called when the scene is loaded
func _ready():
    # Method to randomize the output result of a function that generates a random number each time
	randomize()
    # Make all_pieces a 2d array that makes up the grid of the board
	all_pieces = make_2d_array() # define after this
    # Spawn the pieces and place them on each grid
	spawn_pieces() # define after this

# Method to create a 2d array that makes up the grid of the board
func make_2d_array():
	# Prepare an array named array for output
    var array = [].
    # Fill the prepared array with the number of empty arrays for the number of grids along the x-axis
	for i in width:
		array.append([])
        # Add a value of null for the number of grids in the y-axis to each array
		for j in height:
			array[i].append(null)
    # return the two-dimensional array
	return array

# Method to spawn pieces and place them on each grid
func spawn_pieces():
    # Loop for the number of grids along the x-axis
	for i in width:
        # Loop over the number of grids in the y-axis
		for j in height:
			# If no piece exists in the corresponding grid on the 2D array of all pieces
            # (All null at the start of the game)
            if all_pieces[i][j] == null:
                # Randomly select one from the scenes of each color and instantiate it
				var index = floor(rand_range(0, pieces_scn.size()))
				var piece = pieces_scn[index].instance()
                # If it matches, delete the piece instance and start over
				while match_at(i, j, piece.color): # define after this
					piece.queue_free()
					index = floor(rand_range(0, pieces_scn.size()))
					piece = pieces_scn[index].instance()
                # Make a piece instance a child of a PiecesContainer node
				pieces_container.add_child(piece)
                # Place the piece instance at the position converted from grid to pixels
				piece.position = grid_to_pixel(i, j) # define after this
                # Update the 2D array of all pieces
				all_pieces[i][j] = piece

Let’s define the match_at and grid_to_pixel methods in the above code.

### Grid.gd ###

# Method to check if 3 or more pieces of the same color are lined up at the specified grid position.
# column is the grid position on the x-axis, row is the grid position on the y-axis, and color is the color of the piece
func match_at(column, row, color):
    # If the x-axis value of the specified grid coordinate is 3 or greater
	if column >= 2:
        # If there is a piece to the left of the specified grid coordinate and one more to the left
		if all_pieces[column-1][row] ! = null \
		and all_pieces[column-2][row] ! = null:
            # If the color of those pieces is the same as the color of the given piece
			if all_pieces[column-1][row].color == color \
			and all_pieces[column-2][row].color == color:
                # Return true
				return true
    # If the y-axis value of the specified grid coordinate is 3 or greater
	if row >= 2:
        # If there are pieces below the specified grid coordinates and one more below
		if all_pieces[column][row-1] ! = null \frz
		and all_pieces[column][row-2] ! = null:
            # If the color of those pieces is the same as the color of the given piece
			if all_pieces[column][row-1].color == color \
			and all_pieces[column][row-2].color == color:
                # return true
				return true

# Method to convert grid position to pixel position
func grid_to_pixel(column, row):
    # Define a Vector2 variable pixel_pos for pixel position output
	var pixel_pos = Vector2()
    # Pixel x-coordinate = grid start position along x-axis + grid size * grid x-coordinate
	pixel_pos.x = x_start + grid_size * column
    # Pixel y-coordinate = grid start position along y-axis - grid size * grid y-coordinate
	pixel_pos.y = y_start - grid_size * row
    # Return pixel position
	return pixel_pos

Now the pieces of each color should be randomly arranged on the board at the start of the game. Let’s run the project once to check it out. If you are running the project for the first time, select “Grid.tscn” as the main scene when the dialog for selecting the main scene appears.
run project - distribute pieces on the grid board


Since we have just defined the grid_to_pixel method, we should also define the pixel_to_grid method to be used later. As the name suggests, this method is the opposite of grid_to_pixel defined earlier, and converts pixel positions to grid positions.

### Grid.gd ###

# Method to convert pixel position to grid position
func pixel_to_grid(pixel_x, pixel_y) -> Vector2:
	var grid_pos = Vector2()
	grid_pos.x = floor((pixel_x - x_start) / grid_size)
	grid_pos.y = floor((pixel_y - y_start) / -grid_size)
	return grid_pos

In addition, we define one more method is_in_grid to be used later. This method determines whether the position passed as an argument is within the board grid and returns true or false.

### Grid.gd ###

# Method that returns whether the given position is in the grid or not.
func is_in_grid(grid_position: Vector2):
	if grid_position.x >= 0 and grid_position.x < width \
	and grid_position.y >= 0 and grid_position.y < height
        # Return true if within the board grid
		return true
	else:
        # Return false if outside of the grid
		return false

Here, we will write a program to handle the player’s input for the game.

### Grid.gd ###

# Function called every frame in the main loop of the game
func _process(_delta):
    # If not in the process of matching
	if not is_waiting:
        # Process the player's input
		touch_input() # define after this

# Method to process player input
func touch_input():
    # If a finger touches the screen
	if Input.is_action_just_pressed("touch"):
        # Convert finger position from pixels to grid
		var start_pos = get_global_mouse_position()
		var start_grid = pixel_to_grid(start_pos.x, start_pos.y)
        # If the finger position is within the board grid
		if is_in_grid(start_grid):
            # Save the position where you touched your finger to the screen
			touched_pos = start_grid
            # Make the state with the finger touching the screen
			is_touching = true
	# If finger leaves the screen
    if Input.is_action_just_released("touch"):
        # Convert finger position from pixels to grid
		var end_pos = get_global_mouse_position()
		var end_grid = pixel_to_grid(end_pos.x, end_pos.y)
        # If finger position is within the board grid...
        # and the state is with the finger touching the screen
		if Is_in_grid(end_grid) and is_touching:
            # Save as released position information
			released_pos = end_grid
            # Call a method that handles the movement of the piece at the touched and released positions
			touch_and_release() # define after this
        # State is finger off the screen
		is_touching = false

In the above code, the touch_and_release method is called to acquire the position of the finger touching the screen and the position of the finger leaving the screen within the grid of the board, and to use this information to process the movement of the pieces.

Let’s define this method and a helper method called swap_pieces that is called further in the method. A helper method is simply a method that is called within a method and serves to keep the parent method simple.

### Grid.gd ###

# Method that handles the movement of the piece using the position of the touched finger and the position of the released finger
func touch_and_release():
    # Calculate the difference between the position where the finger touched and the position where the finger released
	var difference = released_pos - touched_pos
    # If the absolute value of the difference along the x-axis is greater than the absolute value of the difference along the y-axis
	if abs(difference.x) > abs(difference.y):
        # If difference along x-axis is greater than 0
		if difference.x > 0:
            # Call helper method to swap the piece at the touched position with the adjacent piece to the right
			swap_pieces(touched_pos, Vector2.RIGHT) # define after this
        # If the difference along the x-axis is less than 0
		elif difference.x < 0:
            # Call helper method to swap the piece at the touched position with the adjacent piece to the left
			swap_pieces(touched_pos, Vector2.LEFT) # defined after this
    # If the absolute difference along the x-axis is less than the absolute difference along the y-axis
	elif abs(difference.x) < abs(difference.y):
        # If the difference along y-axis is greater than 0
		if difference.y > 0:
            # Call helper method to swap the piece at the touched position with the piece adjacent below
			swap_pieces(touched_pos, Vector2.DOWN) # define after this
        # If the difference along the y-axis is less than 0
		elif difference.y < 0:
            # Call helper method to swap the piece at the touched position with the one adjacent above it
			swap_pieces(touched_pos, Vector2.UP) # defined after this

# Helper method to swap pieces
func swap_pieces(pos, dir):
    # Get the piece at the touched position from the 2D array of all pieces
	var touched_piece = all_pieces[pos.x][pos.y].
    # Get the piece adjacent to the piece in the direction of finger release from the 2D array of all pieces
	var target_piece = all_pieces[pos.x + dir.x][pos.y + dir.y]
    # If both pieces exist in the 2D array of all pieces
	if touched_piece ! = null and target_piece ! = null:
        # Overwrite the piece at the touched position in the 2D array of all pieces with the piece adjacent to the one you removed your finger from
		all_pieces[pos.x][pos.y] = target_piece
        # Overwrite the piece adjacent to the finger release position with the piece at the finger release position from the two-dimensional array of all pieces
		all_pieces[pos.x + dir.x][pos.y + dir.y] = touched_piece
        # Move the piece instance at the touched position on the board by 1 grid toward the one you removed your finger from
		touched_piece.move(grid_to_pixel(pos.x + dir.x, pos.y + dir.y))
        # move the piece instance adjacent to the one you release your finger on the board to the position touched by your finger
		target_piece.move(grid_to_pixel(pos.x, pos.y))
        # Set auto-processing state to "processing in progress" since processing of the matched piece starts here
		is_waiting = true

With the above code, we have implemented a process whereby a piece dragged by the player is replaced by an adjacent piece.

Let’s run the project to see if the input operation works correctly.
run project - swap pieces


From here, the process that should be executed automatically after the pieces are replaced is implemented. The general flow is as follows:

  1. Change the automatic processing state during processing.
  2. Check if there is at least one matched piece, and if so, loop through the following process.
    1. Check all pieces and flag matched pieces.
    2. Delete the flagged piece.
    3. Fill the empty space by moving a piece from the same column.
    4. After the pieces are packed down, a new piece is created in the empty space at the end.
  3. When there are no more matched pieces, the automatic processing state is stopped.

Let’s code the general flow of the above.

First, let’s add a line is_waiting = true at the end of the swap_pieces method that swaps the pieces defined earlier. This changes the automatic processing state to “processing”.

### Grid.gd ###

func swap_pieces(pos, dir):
	var touched_piece = all_pieces[pos.x][pos.y]
	var target_piece = all_pieces[pos.x + dir.x][pos.y + dir.y]
	if touched_piece ! = null and target_piece ! = null:
		all_pieces[pos.x][pos.y] = target_piece
		all_pieces[pos.x + dir.x][pos.y + dir.y] = touched_piece
		touched_piece.move(grid_to_pixel(pos.x + dir.x, pos.y + dir.y))
		target_piece.move(grid_to_pixel(pos.x, pos.y))

        # Add the following
        # Set the auto-processing state to "processing" since this is where the automatic processing of the matched piece starts
		is_waiting = true

Let’s write the if syntax “if the state is in the process of executing automatic processing” in the touch_input method, and then add the processing we want to execute automatically in the block. The position for adding is just after the finger operation is finished. After the process is complete, let’s add a line is_waiting = false to set the automatic processing state to “stopped”.

### Grid.gd ###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# Omit
	if Input.is_action_just_released("touch"):
		# Omit

        # Add from here
        # If Input.is_action_just_pressed("touch"): # omit
		if is_waiting:
            # Check if there is at least one pair of matched pieces > loop as long as there are
			while check_matches(): # define after this
                pass
            # If no matched pieces are found and processing is complete, set autoprocessing state to waiting
			is_waiting = false

A while loop is used to repeat the necessary process if there is at least one pair of matching pieces.


First, define check_matches(), which is also the loop condition of the while loop, as follows.

### Grid.gd ###

# Method to check if there is at least one matched piece and return the result
func check_matches() -> bool:
    # Loop over the x-axis grid of the board
	for i in width:
        # Loop over the y-axis grid of the board
		for j in height:
            # if piece exists at that grid coordinate
			if all_pieces[i][j] ! = null:
                # return true if the piece matches at that grid coordinate, and method also terminates
				if match_at(i, j, all_pieces[i][j].color):
					return true
    # Check all pieces and return false if none of them match
	return false

Then, let’s define the find_matches method to perform the “first process in the while loop”, which is to check all the pieces and flag the matched ones, as follows. By “flagging” here, I mean changing the value of the matched property of a piece instance to true.

### Grid.gd ###

# Find matching pieces and flag method
func find_matches():
    # Loop over the number of x-axis grids on the board
	for i in width:
        # Loop over the number of y-axis grids on the board
		for j in height:
            # If a piece exists at the coordinates of that grid
			if all_pieces[i][j] ! = null:
                # Define the current color as the color of that piece
				var current_color = all_pieces[i][j].color
                # If its x-axis coordinates are less than the number of x-axis grids - 2
				if i < width - 2:
                    # If there are pieces to the right of that piece and further to its right
					if all_pieces[i+1][j] ! = null }
					and all_pieces[i+2][j] ! = null:
                        # If the color of those pieces is the same as the current color
						if all_pieces[i+1][j].color == current_color \
						and all_pieces[i+2][j].color == current_color:
                            # If the piece is not flagged
							if not all_pieces[i][j].matched:
                                # Set the matched property of the piece to true to flag it
                                # Make the piece's texture color translucent at the same time
								all_pieces[i][j].make_matched()
                            # If the piece's right neighbor is not flagged
							if not all_pieces[i+1][j].matched:
								all_pieces[i+1][j].make_matched()
                            # If the piece's two neighbors to the right are not flagged
							if not all_pieces[i+2][j].matched:
								all_pieces[i+2][j].make_matched()
				# If the piece's y-coordinate is less than the number of grids along the y-axis - 2
                if j < height - 2:
                    # If there are pieces above and further to its above that piece
					if all_pieces[i][j+1] ! = null \f}
					and all_pieces[i][j+2] ! = null:
                        # If the color of those pieces is the same as the current color
						if all_pieces[i][j+1].color == current_color \
						and all_pieces[i][j+2].color == current_color:
                            # If the piece is not flagged
							if not all_pieces[i][j].matched:
                                # Set the matched property of the piece to true to flag it
                                # Make the piece's texture color translucent at the same time
								all_pieces[i][j].make_matched()
                            # If the piece above it is not flagged
							if not all_pieces[i][j+1].matched:
								all_pieces[i][j+1].make_matched()
                            # If the piece two above it is not flagged
							if not all_pieces[i][j+2].matched:
								all_pieces[i][j+2].make_matched()

We need to call this find_matches method in the while loop in the touch_input method, so let’s update it as follows.

### Grid.gd ###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# Omit
	if Input.is_action_just_released("touch"):
		# Omit

		if is_waiting:
            # Check if there is at least one pair of matched pieces > loop as long as there are
			while check_matches():
                # Find matched pieces and flag them
				find_matches()
                # Wait 0.3 seconds to make the process visually clear
				yield(get_tree().create_timer(0.3), "timeout")
			is_waiting = false

Now the matched property of each matched piece instance should be true and the color of the piece should be translucent.

Let’s run the project and check it out.
run project - flag on matched pieces


Next, I would like you to define a delete_matches method that executes the second process “delete flagged pieces” in the while loop as follows.

### Grid.gd ###

# Method to delete matched pieces
func delete_matches():
    # Loop over the number of x-axis grids on the board
	for i in width:
        # Loop over the number of y-axis grids of the board
		for j in height:
            # If a piece exists at that grid coordinate
			if all_pieces[i][j] ! = null:
                 # If a piece at that grid coordinate is flagged
				if all_pieces[i][j].matched:
                    # Delete the piece at that grid coordinate
					all_pieces[i][j].queue_free()
                    # Empty the element with that grid coordinate from the 2D array of all_pieces[i][j].queue_free()
					all_pieces[i][j] = null

Let’s add this delete_matches method to the while loop of the touch_input method.

### Grid.gd ###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# Omit
	if Input.is_action_just_released("touch"):
		# Omit

		if is_waiting:
            # Check if there is at least one pair of matched pieces > loop as long as there are
			while check_matches():
                # Find matched pieces and flag them
				find_matches()
                # Wait 0.3 seconds to make the process visually clear
				yield(get_tree().create_timer(0.3), "timeout")
                # Delete flagged pieces
				delete_matches()
                # Wait 0.3 seconds
				yield(get_tree().create_timer(0.3), "timeout")
            # If no matched pieces are found and processing is complete, invalidate the in-process state
			is_waiting = false

Now the matched pieces should become translucent and then be deleted.

Let’s run the project and check it out.
run project - delete matched pieces


Next, I would like you to update the collapse_columns method that executes the third process in the while loop, “Move and pack pieces from the grid above the same column into the space vacated by the deletion”, as follows.

### Grid.gd ###

# Method to collapse a column by moving the piece above it in a space where no piece exists
func collapse_columns():
    # Loop over the number of x-axis grids in the board
	for i in width:
        # Loop over the number of y-axis grids in the board
		for j in height:
            # If no piece exists at that grid coordinate (null)
			if all_pieces[i][j] == null:
                # Loop from one row above the y-coordinate of that grid to the top row
				for k in range(j + 1, height):
                    # If a piece exists on the grid one above
					if all_pieces[i][k] ! = null:
                        # Move the piece on the grid above to an empty grid below
						all_pieces[i][k].move(grid_to_pixel(i, j))
                        # Put one piece above into the current grid coordinates of the 2D array of all_pieces
						all_pieces[i][j] = all_pieces[i][k].
                        # Empty the grid coordinate one above in the 2D array of all_pieces
						all_pieces[i][k] = null
                        # Exit the loop
						break

Let’s call this collapse_columns method inside the while loop of the touch_input method.

### Grid.gd ###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# Omit
	if Input.is_action_just_released("touch"):
		# Omit

		if is_waiting:
            # Check if there is at least one pair of matched pieces > loop as long as there are
			while check_matches():
                # Find matched pieces and flag them
				find_matches()
                # Wait 0.3 seconds to make the process visually clear
				yield(get_tree().create_timer(0.3), "timeout")
                # Delete flagged pieces
				delete_matches()
                # Wait 0.3 seconds
				yield(get_tree().create_timer(0.3), "timeout")
                # Move and collapse pieces on the same column into empty space
				collapse_columns()
                # Wait 0.3 seconds
				yield(get_tree().create_timer(0.3), "timeout")
            # If no matched pieces are found and processing is complete, invalidate the in-process state
			is_waiting = false

Now we have an algorithm that fills the empty space with the piece above it after the piece has been deleted. The while loop should also continuously delete the matching piece after it has been stuffed into the empty space.

Let’s run the project and check it out.
run project - collapse columns


Finally, if the spawn_pieces method is called after the pieces are packed down, new pieces will be placed in the empty space and the board will be filled. This method is already defined and called in the _ready method to place the pieces on the board at the beginning of the game.

Now let’s update the touch_input method as follows.

### Grid.gd ###

func touch_input():
	if Input.is_action_just_pressed("touch"):
		# Omit
	if Input.is_action_just_released("touch"):
		# Omit

		if is_waiting:
            # Check if there is at least one pair of matched pieces > loop as long as there are
			while check_matches():
                # Find matched pieces and flag them
				find_matches()
                # Wait 0.3 seconds to make the process visually clear
				yield(get_tree().create_timer(0.3), "timeout")
                # Delete flagged pieces
				delete_matches()
                # Wait 0.3 seconds
				yield(get_tree().create_timer(0.3), "timeout")
                # Move and collapse pieces on the same column into empty space
				collapse_columns()
                # Wait 0.3 seconds
				yield(get_tree().create_timer(0.3), "timeout")
                # Spawn and place a new piece in an empty space
				spawn_pieces()
                # Wait 0.3 seconds
				yield(get_tree().create_timer(0.3), "timeout")
            # If no matched pieces are found and processing is complete, invalidate the in-process state
			is_waiting = false

This completes the coding of the automatic processing part. This is the end of this tutorial. Finally, let’s run the project to see how it works.
run project - whole check



Sample Game

We have prepared a sample game that is a brushed-up version of the project created in this tutorial. Note that the GIF images below are played at 3x speed, so they are actually a bit calmer.

sample game


The project file is located in the GitHub repository . Please download the .zip file from there and import the “project.godot” file in the “Sample” folder and import it into Godot Engine.



Conclusion

This time, we created a match 3 puzzle game. This is a perfect game genre for mobile games with simple controls that can be enjoyed over and over again.

Let me summarize the key points when creating a simple match 3 puzzle game like this one.

  • There are only two minimum scenes required: the board and the pieces.
  • Create a blueprint piece scene and then inherit it to create a piece scene for each color.
  • Use a two-dimensional array to manage the pieces to be placed on the board grid.
  • When replacing a piece, it is necessary to both replace the position of the piece on the screen and replace the elements of the two-dimensional array.
  • The following are the key points of the script.
    • The process of the player moving the pieces
      1. Get the position where the finger touches the screen and the position where the finger leaves the screen.
      2. Determine if the two positions are within the grid (valid operation).
      3. Determine the direction in which the pieces are swapped based on the difference between the two positions.
    • Automatic processing (loop) when a match is made.
      1. Checks if there is at least one pair of matching pieces (loop condition).
      2. Flag the matched pieces.
      3. Delete flagged pieces (loop condition).
      4. Fill the space vacated by the removed piece with the piece above it.
      5. Create a new piece in the space vacated by the filling.