Como Fazer um Jogo da Velha na Godot 4: Tutorial Completo Para Iniciantes

Jogo da velha na Godot 4: tutorial passo a passo com GridContainer, Array[int], turnos, checagem de vitória e oponente de máquina em GDScript tipado.
Fazer um jogo da velha na Godot é o melhor primeiro projeto de lógica que você pode escolher. Não tem física, não tem colisão, não tem sprite animado: é só estado, turnos e uma condição de vitória. Tudo que dá dor de cabeça em jogos maiores fica de fora, e o que sobra é exatamente o que você precisa treinar no começo: representar o jogo em dados e escrever as regras em código. Neste tutorial você vai montar o jogo completo na Godot 4, com GDScript tipado, do zero até um oponente simples de máquina.
Por Que o Jogo da Velha na Godot é o Melhor Primeiro Projeto de Lógica
Antes de abrir o editor, vale entender o que esse projeto ensina. Um jogo da velha tem quatro problemas clássicos de programação de jogos, todos em miniatura:
- Estado: o tabuleiro precisa ser representado em memória, não só na tela. A tela é consequência do estado, nunca o contrário.
- Turnos: o jogo alterna entre dois jogadores, e o código precisa saber de quem é a vez e impedir jogadas fora de hora.
- Condição de vitória: após cada jogada, o jogo verifica se alguém venceu ou se deu empate.
- Reinício: o estado precisa voltar ao zero sem fechar e abrir o jogo.
Esses quatro problemas aparecem em qualquer jogo de tabuleiro, card game ou puzzle que você fizer depois. Se quiser ver a mesma estrutura em um projeto bem maior, o tutorial de jogo de xadrez na Godot usa exatamente os mesmos conceitos, só que com regras muito mais pesadas. Comece pelo jogo da velha.
Montando a Cena: GridContainer e 9 Buttons
Crie uma cena nova com um nó Control como raiz e renomeie para JogoDaVelha. Dentro dele, monte esta árvore:
JogoDaVelha (Control)
└── VBoxContainer
├── RotuloStatus (Label)
├── Grade (GridContainer)
│ ├── Button (x9)
└── BotaoReiniciar (Button)
Os passos no editor:
- Adicione um VBoxContainer e, no menu de âncoras, use Full Rect para ocupar a tela.
- Dentro dele, adicione um Label chamado
RotuloStatus. Ele vai mostrar de quem é a vez e o resultado. - Adicione um GridContainer chamado
Gradee, no inspetor, defina Columns = 3. É isso que transforma 9 botões soltos em um tabuleiro 3x3. - Adicione um Button dentro da
Grade, defina Custom Minimum Size como 100x100 para a célula ficar clicável com conforto, e aumente a fonte em Theme Overrides > Font Sizes (algo como 48). Duplique esse botão 8 vezes com Ctrl+D. - Por fim, adicione um Button chamado
BotaoReiniciarcom o texto "Reiniciar".
Detalhe importante: não renomeie os 9 botões um por um nem conecte sinais pelo editor. O GridContainer entrega os filhos em ordem, do primeiro ao último, e vamos usar isso a nosso favor no código, conectando tudo em um único loop.
O Estado do Jogo: Tabuleiro Como Arrayint
Aqui está a decisão de design mais importante do projeto: o tabuleiro é um Array[int] de 9 posições, onde 0 é célula vazia, 1 é X e 2 é O. Os índices seguem a ordem dos botões na grade:
0 | 1 | 2
--+---+--
3 | 4 | 5
--+---+--
6 | 7 | 8
Anexe um script ao nó raiz JogoDaVelha e comece declarando o estado:
extends Control
const VAZIO: int = 0
const JOGADOR_X: int = 1
const JOGADOR_O: int = 2
var tabuleiro: Array[int] = []
var jogador_atual: int = JOGADOR_X
var jogo_ativo: bool = true
var contra_maquina: bool = true
@onready var grade: GridContainer = $VBoxContainer/Grade
@onready var rotulo_status: Label = $VBoxContainer/RotuloStatus
@onready var botao_reiniciar: Button = $VBoxContainer/BotaoReiniciar
Repare que tudo é tipado: Array[int], int, bool, e os nós com o tipo real de cada um. Isso faz o editor apontar erros antes de você rodar o jogo, o que é ouro para iniciante.
Conectando os Botões e Alternando Turnos
No _ready, conectamos os 9 botões da grade com um loop. O truque é o bind(i): cada botão chama a mesma função, mas passando o próprio índice, então uma função só atende o tabuleiro inteiro.
func _ready() -> void:
for i: int in range(grade.get_child_count()):
var botao: Button = grade.get_child(i)
botao.pressed.connect(_ao_clicar_celula.bind(i))
botao_reiniciar.pressed.connect(reiniciar_jogo)
reiniciar_jogo()
Essa é a forma de conectar sinais na Godot 4: sinal.connect(funcao). Nada de strings com o nome do sinal, isso era Godot 3.
Agora a jogada em si. A regra é simples: se o jogo acabou ou a célula está ocupada, ignore o clique. Caso contrário, marque a célula, cheque o resultado e passe o turno:
func _ao_clicar_celula(indice: int) -> void:
if not jogo_ativo or tabuleiro[indice] != VAZIO:
return
marcar(indice, jogador_atual)
if processar_fim_de_jogo(jogador_atual):
return
trocar_turno()
if contra_maquina and jogador_atual == JOGADOR_O:
jogada_da_maquina()
func marcar(indice: int, jogador: int) -> void:
tabuleiro[indice] = jogador
var botao: Button = grade.get_child(indice)
botao.text = "X" if jogador == JOGADOR_X else "O"
func trocar_turno() -> void:
jogador_atual = JOGADOR_O if jogador_atual == JOGADOR_X else JOGADOR_X
rotulo_status.text = "Vez do %s" % ("X" if jogador_atual == JOGADOR_X else "O")
Note a separação: marcar atualiza o array e o botão juntos, então o estado e a tela nunca divergem. Esse hábito de ter uma única função que altera o estado vai te salvar em projetos maiores.
Checando a Vitória: as 8 Linhas Vencedoras
Aqui está o coração da lógica. No jogo da velha existem exatamente 8 formas de vencer: 3 linhas horizontais, 3 colunas e 2 diagonais. Em vez de escrever um monte de if encadeado, guarde as 8 trincas de índices em uma constante e percorra todas:
const LINHAS_VENCEDORAS: Array[Array] = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], # linhas
[0, 3, 6], [1, 4, 7], [2, 5, 8], # colunas
[0, 4, 8], [2, 4, 6], # diagonais
]
func checar_vitoria(jogador: int) -> bool:
for linha: Array in LINHAS_VENCEDORAS:
if tabuleiro[linha[0]] == jogador \
and tabuleiro[linha[1]] == jogador \
and tabuleiro[linha[2]] == jogador:
return true
return false
Compare com a alternativa: 8 condições escritas à mão, cada uma com 3 comparações. A constante transforma a regra do jogo em dados, e a função vira um loop genérico. Se um dia você fizer um jogo da velha 4x4, só muda a constante.
O empate é ainda mais curto: se não sobrou nenhum 0 no array e ninguém venceu, deu velha.
func checar_empate() -> bool:
return not tabuleiro.has(VAZIO)
Fim de Jogo, Label de Status e Botão de Reiniciar
Agora juntamos as duas checagens em uma função que roda após cada jogada. Ela retorna true se o jogo terminou, e é por isso que o _ao_clicar_celula faz return quando ela responde que sim:
func processar_fim_de_jogo(jogador: int) -> bool:
if checar_vitoria(jogador):
jogo_ativo = false
rotulo_status.text = "%s venceu!" % ("X" if jogador == JOGADOR_X else "O")
return true
if checar_empate():
jogo_ativo = false
rotulo_status.text = "Deu velha! Empate."
return true
return false
E o reinício, que também serve de inicialização no _ready: zera o array, limpa o texto dos botões e devolve a vez ao X.
func reiniciar_jogo() -> void:
tabuleiro.clear()
for i: int in range(9):
tabuleiro.append(VAZIO)
for botao: Node in grade.get_children():
if botao is Button:
botao.text = ""
jogador_atual = JOGADOR_X
jogo_ativo = true
rotulo_status.text = "Vez do X"
Rode a cena com F6. Neste ponto o jogo já funciona completo para dois jogadores no mesmo teclado: turnos alternam, vitória e empate aparecem no Label, e o botão de reiniciar limpa tudo. Se algo não funcionar, os suspeitos de sempre são os caminhos dos @onready (confira os nomes dos nós) e o Columns = 3 do GridContainer.
Bônus: Oponente Simples de Máquina
Com a variável contra_maquina em true, o O passa a ser controlado pelo jogo. A estratégia mais simples possível: listar as células vazias e escolher uma ao acaso.
func jogada_da_maquina() -> void:
if not jogo_ativo:
return
var vazias: Array[int] = []
for i: int in range(tabuleiro.size()):
if tabuleiro[i] == VAZIO:
vazias.append(i)
if vazias.is_empty():
return
var escolha: int = vazias[randi() % vazias.size()]
marcar(escolha, JOGADOR_O)
if processar_fim_de_jogo(JOGADOR_O):
return
trocar_turno()
Sejamos honestos: essa máquina joga mal. Ela não bloqueia sua vitória nem tenta ganhar, só preenche células. Mas repare no que ela reaproveita: marcar, processar_fim_de_jogo e trocar_turno são as mesmas funções do jogador humano. Como o estado vive no array e não nos botões, a máquina joga pelo mesmo caminho que você.
O próximo nível real é o algoritmo minimax, que simula todas as jogadas futuras e joga de forma perfeita (no jogo da velha, perfeito significa nunca perder). É um ótimo segundo desafio, mas é assunto para quando este projeto estiver redondo. Um degrau intermediário honesto: antes da jogada aleatória, verifique com checar_vitoria se existe uma célula que vence na hora ou que bloqueia o X, e priorize essas.
Próximos Passos
Você saiu do zero para um jogo completo: cena com GridContainer, estado em Array[int], turnos, 8 linhas vencedoras como constante, empate, status na tela, reinício e até um oponente básico. Mais importante que o jogo em si é o padrão que você praticou: estado em dados, tela como reflexo do estado, regras como funções pequenas.
Para continuar nessa trilha, três caminhos naturais:
- Jogo de tabuleiro digital: leva a mesma ideia de tabuleiro como array para jogos com movimento de peças e casas especiais.
- Jogo de xadrez na Godot: o chefão dos jogos de tabuleiro, com regras de movimento por peça e validação de jogadas.
- Ideias de jogos simples para iniciantes: uma lista de projetos no mesmo espírito do jogo da velha, cada um treinando um fundamento diferente.
Escolha o próximo, termine, e repita. É assim que se aprende a fazer jogos.
Perguntas frequentes
Preciso saber programar para fazer um jogo da velha na Godot?
Você precisa do básico: variáveis, funções, if e for. O jogo da velha é justamente um bom projeto para praticar esses fundamentos, porque toda a lógica cabe em um único script curto, sem física nem animação.
Por que usar Array[int] em vez de uma matriz 3x3 para o tabuleiro?
Um Array[int] de 9 posições é mais simples de indexar, de percorrer e de comparar com as linhas vencedoras. Como o GridContainer já entrega os botões em ordem linear de 0 a 8, o array plano casa direto com a interface, sem conversão de linha e coluna.
Como o jogo detecta a vitória no jogo da velha?
Existem apenas 8 combinações vencedoras: 3 linhas, 3 colunas e 2 diagonais. Você guarda essas 8 trincas de índices em uma constante e, após cada jogada, verifica se alguma trinca tem as três posições marcadas pelo mesmo jogador.
Dá para fazer o computador jogar contra mim?
Sim. A versão mais simples escolhe uma célula vazia aleatória, o que já torna o jogo jogável sozinho. O próximo nível é o algoritmo minimax, que joga de forma perfeita, mas a versão aleatória é o ponto de partida honesto para iniciantes.
Quanto tempo leva para fazer um jogo da velha na Godot 4?
Seguindo este tutorial, entre 1 e 2 horas na primeira vez. A cena tem poucos nós (GridContainer, 9 Buttons, um Label e um botão de reiniciar) e o script completo fica em torno de 80 linhas de GDScript.


