Voltar para o Blog
Quest Log

Como Fazer o Jogo 2048 na Godot 4: Tutorial Completo em GDScript

Tabuleiro do jogo 2048 feito na Godot 4 com peças numéricas coloridas em uma grade 4x4

Tutorial completo: como fazer o jogo 2048 na Godot 4 com GDScript tipado. Grade 4x4, algoritmo de fusão, spawn de peças, game over e animação com Tween.

Aprender como fazer o jogo 2048 é um dos melhores exercícios de lógica que existem para quem está começando na Godot 4. O jogo inteiro cabe em uma matriz 4x4 de inteiros: não tem física, não tem colisão, não tem inimigo com IA. Tudo se resume a deslizar números, fundir pares iguais e somar pontos. Neste tutorial você vai montar o 2048 completo em GDScript tipado, do tabuleiro ao game over, com animação de sobra no final.

Por que o 2048 é um projeto de estudo perfeito

O 2048 pertence à mesma família de projetos didáticos que jogos de grade em geral. Se você já leu nosso guia sobre como fazer um jogo de tabuleiro digital, vai reconhecer o padrão: o estado do jogo vive em uma matriz no código, e a tela é apenas um espelho desse estado.

Isso traz três vantagens enormes para quem está aprendendo:

  1. Lógica pura: o coração do jogo é um único algoritmo (deslizar e fundir uma linha). Você escreve, testa e entende cada linha.
  2. Zero física: nada de RigidBody, Area2D ou colisão. Menos conceitos novos ao mesmo tempo, mais foco em programação.
  3. Muito espaço para polimento: depois que a lógica funciona, dá para adicionar animação com Tween, cores por valor, efeito de "pop" na fusão, som e placar de recorde. É o tipo de projeto que fica bonito no portfólio.

Quem já fez como fazer Tetris na Godot vai notar que a estrutura é parecida, mas o 2048 é ainda mais enxuto: não existe queda automática nem timer, o tabuleiro só muda quando o jogador aperta uma tecla.

Montando a cena na Godot 4

A cena é minimalista. Crie um novo projeto 2D e monte esta árvore:

  • Main (Node2D): nó raiz, onde vai o script principal
    • Tiles (Node2D): container das peças, que serão criadas por código
    • CanvasLayer (CanvasLayer)
      • ScoreLabel (Label): mostra a pontuação
      • GameOverLabel (Label): mensagem de fim de jogo, comece com visible = false

Você poderia usar um GridContainer para as peças, mas posicionar manualmente com Node2D dá controle total sobre a animação depois. Cada peça será um Panel com um Label dentro, criados inteiramente via script.

Anexe um script ao nó Main. Vamos começar pelas constantes, pelas variáveis e pela inicialização do tabuleiro como matriz 4x4:

extends Node2D

const GRID_SIZE: int = 4
const CELL_SIZE: int = 120
const CELL_GAP: int = 12

var grid: Array[Array] = []
var score: int = 0

@onready var tiles_root: Node2D = $Tiles
@onready var score_label: Label = $CanvasLayer/ScoreLabel
@onready var game_over_label: Label = $CanvasLayer/GameOverLabel


func _ready() -> void:
    for y: int in GRID_SIZE:
        var row: Array[int] = []
        for x: int in GRID_SIZE:
            row.append(0)
        grid.append(row)
    spawn_tile()
    spawn_tile()
    update_board()

Repare na tipagem estática em tudo: Array[Array], Array[int], -> void. Além de evitar bugs bobos, o editor da Godot passa a autocompletar melhor. Se essa sintaxe ainda é novidade para você, o guia GDScript do zero cobre tipagem em detalhe.

O valor 0 representa célula vazia. O tabuleiro começa zerado e recebe duas peças iniciais, exatamente como no 2048 original.

Como fazer o jogo 2048 deslizar e fundir peças

Aqui está o coração do jogo. O truque é resolver o problema para uma única linha, sempre para a esquerda, e depois reaproveitar essa função para as quatro direções.

O algoritmo tem três passos:

  1. Comprimir: remover os zeros, empurrando os números para o início.
  2. Fundir: percorrer da esquerda para a direita e somar vizinhos iguais, uma única vez por movimento. Isso garante que [2, 2, 2, 2] vire [4, 4] e não [8].
  3. Comprimir de novo: preencher com zeros até completar 4 posições.
func slide_row(row: Array[int]) -> Array[int]:
    var compact: Array[int] = []
    for value: int in row:
        if value != 0:
            compact.append(value)

    var merged: Array[int] = []
    var i: int = 0
    while i < compact.size():
        if i + 1 < compact.size() and compact[i] == compact[i + 1]:
            var new_value: int = compact[i] * 2
            merged.append(new_value)
            score += new_value
            i += 2
        else:
            merged.append(compact[i])
            i += 1

    while merged.size() < GRID_SIZE:
        merged.append(0)
    return merged

O i += 2 depois de uma fusão é o detalhe que impede fusão dupla: a peça recém-criada nunca é considerada de novo no mesmo movimento. A pontuação segue a regra clássica: cada fusão soma o valor da peça resultante ao placar.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Input das 4 direções sem duplicar código

Em vez de escrever quatro versões do algoritmo, extraímos e devolvemos cada "linha lógica" na orientação certa. Para a esquerda, a linha é lida na ordem natural. Para a direita, invertida. Para cima e para baixo, lemos colunas.

func _process(_delta: float) -> void:
    if Input.is_action_just_pressed("ui_left"):
        make_move(Vector2i.LEFT)
    elif Input.is_action_just_pressed("ui_right"):
        make_move(Vector2i.RIGHT)
    elif Input.is_action_just_pressed("ui_up"):
        make_move(Vector2i.UP)
    elif Input.is_action_just_pressed("ui_down"):
        make_move(Vector2i.DOWN)


func get_line(index: int, dir: Vector2i) -> Array[int]:
    var line: Array[int] = []
    for j: int in GRID_SIZE:
        if dir == Vector2i.LEFT:
            line.append(grid[index][j])
        elif dir == Vector2i.RIGHT:
            line.append(grid[index][GRID_SIZE - 1 - j])
        elif dir == Vector2i.UP:
            line.append(grid[j][index])
        else:
            line.append(grid[GRID_SIZE - 1 - j][index])
    return line


func set_line(index: int, dir: Vector2i, line: Array[int]) -> void:
    for j: int in GRID_SIZE:
        if dir == Vector2i.LEFT:
            grid[index][j] = line[j]
        elif dir == Vector2i.RIGHT:
            grid[index][GRID_SIZE - 1 - j] = line[j]
        elif dir == Vector2i.UP:
            grid[j][index] = line[j]
        else:
            grid[GRID_SIZE - 1 - j][index] = line[j]


func make_move(dir: Vector2i) -> void:
    var moved: bool = false
    for i: int in GRID_SIZE:
        var line: Array[int] = get_line(i, dir)
        var new_line: Array[int] = slide_row(line)
        if new_line != line:
            moved = true
            set_line(i, dir, new_line)
    if moved:
        spawn_tile()
        update_board()
        if not has_moves():
            game_over_label.visible = true

Note o detalhe importante em make_move: só nasce peça nova se alguma coisa mudou no tabuleiro. Apertar para a esquerda quando tudo já está encostado à esquerda não pode gastar um turno. A comparação new_line != line resolve isso, já que a Godot 4 compara arrays elemento por elemento.

Usamos as ações padrão ui_left, ui_right, ui_up e ui_down, que já vêm mapeadas para as setas do teclado. Se quiser suportar WASD, adicione essas teclas às ações no Input Map do projeto.

Spawn de peças e detecção de game over

Depois de cada movimento válido, uma peça nova aparece em uma célula vazia aleatória: 90 por cento de chance de ser um 2, 10 por cento de ser um 4. E o jogo só acaba quando não há célula vazia e nenhuma fusão possível, porque um tabuleiro cheio ainda pode ter vizinhos iguais.

func spawn_tile() -> void:
    var empty_cells: Array[Vector2i] = []
    for y: int in GRID_SIZE:
        for x: int in GRID_SIZE:
            if grid[y][x] == 0:
                empty_cells.append(Vector2i(x, y))
    if empty_cells.is_empty():
        return
    var cell: Vector2i = empty_cells.pick_random()
    grid[cell.y][cell.x] = 2 if randf() < 0.9 else 4


func has_moves() -> bool:
    for y: int in GRID_SIZE:
        for x: int in GRID_SIZE:
            if grid[y][x] == 0:
                return true
            if x + 1 < GRID_SIZE and grid[y][x] == grid[y][x + 1]:
                return true
            if y + 1 < GRID_SIZE and grid[y][x] == grid[y + 1][x]:
                return true
    return false

A função has_moves percorre o tabuleiro uma única vez e verifica três condições: existe célula vazia, existe par igual na horizontal, existe par igual na vertical. Se nenhuma for verdadeira, é game over. Para detectar vitória, basta checar se algum valor da matriz chegou a 2048 dentro do mesmo loop.

Desenhando o tabuleiro e animando com Tween

Falta espelhar a matriz na tela. A abordagem mais simples para um tutorial é reconstruir as peças a cada movimento: apagar os filhos de Tiles e criar um Panel com Label para cada célula ocupada. Aproveitamos para dar o "juice" com create_tween(): cada peça nasce pequena e cresce com um leve overshoot.

func update_board() -> void:
    for child: Node in tiles_root.get_children():
        child.queue_free()

    for y: int in GRID_SIZE:
        for x: int in GRID_SIZE:
            var value: int = grid[y][x]
            if value == 0:
                continue

            var tile: Panel = Panel.new()
            tile.size = Vector2(CELL_SIZE, CELL_SIZE)
            tile.position = Vector2(
                x * (CELL_SIZE + CELL_GAP),
                y * (CELL_SIZE + CELL_GAP)
            )
            tile.pivot_offset = Vector2(CELL_SIZE, CELL_SIZE) / 2.0

            var label: Label = Label.new()
            label.text = str(value)
            label.set_anchors_preset(Control.PRESET_FULL_RECT)
            label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
            label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
            tile.add_child(label)
            tiles_root.add_child(tile)

            tile.scale = Vector2(0.6, 0.6)
            var tween: Tween = create_tween()
            tween.tween_property(tile, "scale", Vector2.ONE, 0.12)
            tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)

    score_label.text = "Pontos: %d" % score

O pivot_offset no centro da peça faz a escala crescer a partir do meio, e a transição TRANS_BACK com EASE_OUT cria aquele efeito de "pop" que passa um pouco do tamanho final antes de assentar. É um detalhe de meia dúzia de linhas que muda completamente a sensação do jogo.

Dicas de polimento para deixar o 2048 com cara de jogo

Com a lógica pronta, o projeto vira um playground de polimento. Algumas ideias em ordem de impacto:

  • Cores por valor: crie um Dictionary mapeando cada potência de 2 para uma cor e aplique em tile.self_modulate. O contraste entre 2, 64 e 1024 é parte da identidade visual do 2048.
  • Escala extra na fusão: quando duas peças se fundem, anime a peça resultante de 1.0 para 1.15 e de volta para 1.0 com dois tween_property encadeados. Fusão precisa ser sentida.
  • Deslizamento real: em vez de reconstruir o tabuleiro, mantenha referência aos Panels e anime position com Tween até a célula de destino. Dá mais trabalho, mas o resultado fica idêntico ao original.
  • Recorde salvo: guarde a melhor pontuação com FileAccess ou ConfigFile e mostre ao lado do placar.
  • Suporte a swipe: para exportar para celular, detecte gestos em _input comparando a posição de toque inicial e final.

O 2048 é pequeno o bastante para terminar em um fim de semana e rico o bastante para ensinar o padrão mais importante do desenvolvimento de jogos de grade: separar estado (a matriz) de apresentação (os nós na tela). Domine isso aqui e você estará pronto para qualquer puzzle game. Bom código e boa jogatina.

Perguntas frequentes

Preciso saber programar bem para fazer o 2048 na Godot?

Não. O 2048 usa apenas matrizes, loops e condicionais. Se você já sabe o básico de GDScript, consegue seguir o tutorial. É justamente um ótimo projeto para consolidar lógica de programação.

O 2048 precisa de física ou colisão na Godot?

Não precisa de nada disso. Todo o jogo é resolvido com uma matriz 4x4 de inteiros no código. A parte visual apenas desenha o estado da matriz na tela, o que simplifica muito o projeto.

Como funciona a fusão de peças no 2048?

Ao mover em uma direção, cada linha é comprimida removendo os zeros, depois vizinhos iguais são somados uma única vez por movimento, e a linha é comprimida de novo. Isso garante que 2, 2, 2, 2 vire 4, 4 e não 8.

Quando o jogo 2048 termina?

O jogo termina em derrota quando não há célula vazia no tabuleiro e nenhum par de vizinhos iguais na horizontal ou vertical. A vitória clássica acontece quando alguma peça atinge o valor 2048.

Qual porcentagem de chance de nascer um 4 em vez de um 2?

No 2048 original, cada peça nova tem 90 por cento de chance de ser um 2 e 10 por cento de ser um 4. No código deste tutorial isso é feito com randf menor que 0.9.