how to make a puzzle game in godot

Making a Puzzle Game in Godot


Learning how to make a puzzle game in Godot starts with an empty editor and a grid sketched on paper. Store the board as data first, draw it with a TileMapLayer second, then add input and match logic on top.

Quick answer: Create a Godot project with a board scene, keep every piece in a dictionary keyed by cell, and treat the tiles on screen as a picture of that data. Convert clicks and taps to cell coordinates, find runs of matching pieces, clear them, refill, and repeat until the board is stable. Export once the loop works.

This walkthrough builds a small Match-3 style board as part of our wider guide to indie puzzle game development. The same data structure also suits sliding-block and merge puzzles.

Setting up the project and scenes

Godot Engine is a cross-platform 2D and 3D game engine with a unified interface, and its official editor binaries and export templates are published on the Godot website, as the engine repository README explains. Download the editor for your operating system and create a new Godot project. Install the export templates now only if you want to test on a device soon; they are not needed to build the board.

Stock photo: the phone’s screen is not visible, so the image does not show a game running on it.

Godot builds everything from nodes grouped into scenes, so give the game a few small scenes rather than one big one:

  • Level scene: a Node2D root, which matches the approach in a Godot forum thread on puzzle setup where a level is simply a new scene with a Node2D at its root.
  • Board script: attach a game script to that root. The same thread suggests a script that places things into the scene, which is where your grid data will live.
  • Board display: add a TileMapLayer as a child of the root. This is the Godot 4 node for drawing a grid of tiles.
  • Interface: build menus and score panels as a separate UI scene. The forum example uses a split container anchored to full rect for this.

If you are weighing another engine, how to make a puzzle game in unity covers the same task there. Confirm node and class names against the class reference, which is also accessible from the Godot editor.

Planning the mechanic on paper

Decide on the grid, the piece types and one legal move before you write code. A matching rule that fits in a sentence is easier to implement than a vague idea.

Crumpled blue-lined sheets beside a spiral notebook: discarded rule ideas are normal. Change one rule at a time so you can tell which change broke the board.
  1. Draw the grid and mark a single swap or slide.
  2. Write down what counts as a match and what happens to the cells it empties.
  3. List what the player can select, because that decides your input code.
A pen cup sits behind the open notebook. Sketching the grid and one legal move by hand settles the rules before any scene exists.

If you are unsure which rule is fun, test it cheaply first. Our guide on how to prototype a puzzle game covers that stage.

Storing the board as data

The board state belongs in a script, not in the tiles. A dictionary keyed by Vector2i cell, with the piece kind as the value, makes empty cells easy to represent because a missing key means an empty cell. It also avoids the index arithmetic that nested arrays need at the edges.

A generic code illustration; the Godot examples below are written in GDScript, with the board stored in a dictionary rather than an image or scene.

Filling the grid

Attach this to the level root. The sizes and tile-source values are exported variables, so you set them in the Inspector.

extends Node2D
@export var grid_size: Vector2i
@export var kind_count: int
@export var min_run: int
@export var tile_source: int
@export var atlas_row: int
@onready var layer: TileMapLayer = $TileMapLayer
var cells: Dictionary = {}

func fill_board():
    for y in grid_size.y:
        for x in grid_size.x:
            var pos := Vector2i(x, y)
            cells[pos] = randi() % kind_count
            layer.set_cell(pos, tile_source, Vector2i(cells[pos], atlas_row))

Random filling can create matches before the player moves. After filling, run the match check below and re-roll any cell it flags until the board starts clean.

Drawing with TileMapLayer

Each kind maps to one tile in your tileset, and every change to cells is followed by a matching change on the layer. Never read gameplay state back from the layer. A paid course on Godot puzzle grids teaches the same split, with separate tile-scene and grid-setup lessons before any game rules.

Reading mouse and touch input

Mouse clicks and screen touches arrive as different event classes, InputEventMouseButton and InputEventScreenTouch. Handle both in one function that converts the event position to a cell, so a desktop build and a phone build share all later logic.

signal cell_picked(cell)

func _unhandled_input(event):
    if not (event is InputEventMouseButton or event is InputEventScreenTouch):
        return
    if not event.pressed:
        return
    var cell: Vector2i = layer.local_to_map(make_input_local(event).position)
    if cells.has(cell):
        cell_picked.emit(cell)

This assumes the TileMapLayer sits at the root node’s origin. If you offset it, convert through the layer’s own transform instead. Emitting a signal keeps the input code ignorant of the rules. The forum example works the same way: a button script triggers a signal on the game script, and the game script decides what happens.

Finding and clearing matches

Detecting runs

A match is a run of identical pieces at least as long as min_run. Scan from each cell to the right and downward, then collect every cell in a qualifying run.

func run_from(start: Vector2i, step: Vector2i):
    var run: Array = [start]
    var next := start + step
    while cells.has(next) and cells[next] == cells[start]:
        run.append(next)
        next += step
    return run

func find_matches():
    var found := {}
    for pos in cells:
        for step in [Vector2i.RIGHT, Vector2i.DOWN]:
            var run: Array = run_from(pos, step)
            if run.size() >= min_run:
                for p in run:
                   found[p] = true
    return found.keys()

Collecting results in a dictionary removes duplicates when a run is found from several starting cells, and it handles crossing runs correctly because shared cells are stored once.

Clearing and refilling

Clear in this order so data and display never disagree:

  1. Erase each matched key from cells and call erase_cell on the layer.
  2. Walk each column from the bottom up and move pieces down into empty cells, updating both data and tiles.
  3. Fill the empty cells at the top with new random kinds.
  4. Run find_matches again and repeat until it returns nothing. This loop produces chain reactions.

Disable input while this loop runs, and add a short tween between steps if you want the player to see the cascade. Sliding and merging puzzles reuse the same dictionary. The Zenva course lists tile-sliding mechanics and merging tiles with math-based logic, and runs 3h 45m at intermediate level.

Fixing a misbehaving board

Work from the simplest check upward, and stop as soon as the data and the drawing agree.

  1. Print the dictionary. If cells looks right but the screen does not, the fault is in your set_cell calls or tileset mapping, not in the rules.
  2. Test one direction. Call run_from on a cell you know is part of a run. Wrong lengths point to the step vector or the equality test.
  3. Log the picked cell. If clicks land on the wrong cell, the cause is the coordinate conversion, usually an offset layer or a scaled viewport.
  4. Stop tuning. When a full cascade ends with a stable, correct board, move on to export. Further polish is better judged on a real device.

Exporting to desktop and mobile

Godot exports with one click to Linux, macOS, Windows, Android, iOS and web-based platforms, with console support also listed, according to the official engine README. Install the matching export templates from the Godot website, then add a preset for each target in the Export dialog. Each platform has its own requirements, so follow what the dialog reports for your target.

Test desktop first, because it is the fastest loop. Then test on a real phone, since touch input and screen size are the likeliest sources of surprises. A tap that misses its cell on a handset usually traces back to the conversion step in the input function.

Frequently asked questions

Is Godot a good engine for a small puzzle game?

Yes. Godot is free and open source under the MIT license, with no royalties, and it is supported by the not-for-profit Godot Foundation. A course vendor also describes it as free, lightweight and beginner-friendly, which suits a board game built from tiles and a single script.

Where can I check the exact names of TileMapLayer and input event classes?

The class reference is accessible from inside the Godot editor, so you can look up TileMapLayer, InputEventMouseButton and InputEventScreenTouch while you code.

Are there official examples I can study?

Yes. The Godot team maintains official demo projects in their own GitHub repository, and they are also available through the Asset Store.

Can the same grid code support a sliding-block puzzle?

Yes. The dictionary of cells stays the same. Only the move rule changes, from swapping pieces and finding runs to shifting tiles along a row or column and merging equal neighbours.