Voltar para o Blog
Quest Log

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

Tela de um jogo da velha feito na Godot 4 com grade de nove botões e marcações de X e O

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.

Ferramenta:Godot

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:

  1. Adicione um VBoxContainer e, no menu de âncoras, use Full Rect para ocupar a tela.
  2. Dentro dele, adicione um Label chamado RotuloStatus. Ele vai mostrar de quem é a vez e o resultado.
  3. Adicione um GridContainer chamado Grade e, no inspetor, defina Columns = 3. É isso que transforma 9 botões soltos em um tabuleiro 3x3.
  4. 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.
  5. Por fim, adicione um Button chamado BotaoReiniciar com 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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

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.

Fazendo a Máquina Jogar Menos Burra

A jogada aleatória é um ponto de partida honesto, mas dá pra subir um degrau sem entrar no minimax. A ideia é checar duas coisas em ordem antes de sortear. Primeiro: existe uma jogada que ganha o jogo agora? Se existe, jogue nela. Segundo: o X tem uma jogada que venceria na próxima? Se tem, bloqueie. Só quando nenhuma das duas valer é que a máquina sorteia uma célula vazia.

O truque é reaproveitar o estado em array e a função checar_vitoria que você já escreveu. Para testar uma jogada hipotética, marque a posição no array, cheque a vitória e desfaça a marcação:

func encontrar_jogada_vencedora(jogador: int) -> int:
    for i: int in range(tabuleiro.size()):
        if tabuleiro[i] != VAZIO:
            continue
        tabuleiro[i] = jogador
        var venceu: bool = checar_vitoria(jogador)
        tabuleiro[i] = VAZIO
        if venceu:
            return i
    return -1

A função devolve o índice que vence para aquele jogador, ou -1 se não existe nenhum. Repare que ela simula e desfaz: o array volta exatamente como estava, então chamar isso não suja o estado real do jogo. Agora a escolha da máquina vira uma ordem de prioridade clara:

func escolher_jogada_maquina() -> int:
    var ganhar: int = encontrar_jogada_vencedora(JOGADOR_O)
    if ganhar != -1:
        return ganhar
    var bloquear: int = encontrar_jogada_vencedora(JOGADOR_X)
    if bloquear != -1:
        return bloquear
    var vazias: Array[int] = []
    for i: int in range(tabuleiro.size()):
        if tabuleiro[i] == VAZIO:
            vazias.append(i)
    if vazias.is_empty():
        return -1
    return vazias[randi() % vazias.size()]

Para ligar, troque o sorteio puro dentro de jogada_da_maquina por uma chamada a escolher_jogada_maquina e use o índice retornado em marcar. Com essas duas regras, a máquina deixa de entregar vitória de graça e passa a fechar as suas. Ela ainda não joga perfeito (não planeja duas jogadas à frente nem arma garfo), mas a diferença que o jogador sente é enorme. Esse é o degrau honesto antes do minimax, e ele treina um conceito que serve pra qualquer jogo de turno: simular uma jogada no estado, avaliar o resultado e desfazer.

Problemas Comuns e Como Resolver

Quando algo não roda, o problema quase sempre é um destes. Use a tabela como checklist antes de sair relendo o script inteiro.

SintomaCausa provávelConserto
Erro de nó nulo no _readyCaminho de @onready não bate com a árvoreConfira os nomes dos nós contra o caminho escrito
Os 9 botões aparecem numa coluna sóColumns do GridContainer não está em 3Ajuste Columns = 3 no inspetor da Grade
O clique marca a célula erradaOrdem dos filhos da grade mudouDeixe os 9 botões como filhos diretos da Grade, sem nós no meio
A máquina joga fora da vezTurno trocado na hora erradaCheque a ordem: marcar, processar fim, trocar turno, só então a máquina
O jogo continua depois da vitóriajogo_ativo não foi para falseGaranta que processar_fim_de_jogo roda após cada jogada

Com o jogo rodando, o passo natural é sair do tabuleiro estático e encarar movimento e colisão: o guia de fazer um jogo 2D completo na Godot é a ponte, e o hub de Godot reúne o resto dos tutoriais da engine pra você puxar um assunto de cada vez.

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:

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.