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

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:
- Lógica pura: o coração do jogo é um único algoritmo (deslizar e fundir uma linha). Você escreve, testa e entende cada linha.
- Zero física: nada de RigidBody, Area2D ou colisão. Menos conceitos novos ao mesmo tempo, mais foco em programação.
- 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:
- Comprimir: remover os zeros, empurrando os números para o início.
- 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]. - 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.
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
Dictionarymapeando cada potência de 2 para uma cor e aplique emtile.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.0para1.15e de volta para1.0com doistween_propertyencadeados. Fusão precisa ser sentida. - Deslizamento real: em vez de reconstruir o tabuleiro, mantenha referência aos Panels e anime
positioncom 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
FileAccessouConfigFilee mostre ao lado do placar. - Suporte a swipe: para exportar para celular, detecte gestos em
_inputcomparando 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.


