Voltar para o Blog
Quest Log

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

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.

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.

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

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:

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.