Voltar para o Blog
Quest Log

Como fazer um jogo de quiz no Godot 4: tutorial completo

Interface de um jogo de quiz na engine Godot com pergunta quatro botoes de resposta placar e barra de tempo

Aprenda como fazer um jogo de quiz no Godot 4: perguntas em Array de Dictionary, botões conectados por código, timer por pergunta, placar e tela final.

Se você está procurando como fazer um jogo de quiz no Godot 4, chegou no projeto certo para o momento certo. Um quiz de perguntas e respostas é um dos melhores primeiros jogos que existem: o escopo é fechado, não tem física, não tem arte complicada, e mesmo assim você pratica três coisas que vai usar em qualquer jogo pela frente: estrutura de dados, sinais e fluxo de telas. Neste tutorial você vai montar um quiz completo com placar, timer por pergunta, embaralhamento e tela de resultado, tudo em GDScript com tipagem estática.

Por que um quiz é o primeiro projeto ideal

Antes de abrir o editor, vale entender por que esse projeto funciona tão bem para quem está começando.

Primeiro, o escopo é pequeno e bem definido. Um quiz tem começo, meio e fim: mostra pergunta, jogador responde, mostra resultado. Você sabe exatamente quando terminou, o que é raro em primeiro projeto (a maioria morre porque nunca "acaba").

Segundo, é 100% interface. Você só vai usar nós Control: containers, labels, botões e uma barra de progresso. Nada de CharacterBody2D, colisão ou câmera. Isso elimina metade das fontes de bug de um iniciante.

Terceiro, ele te obriga a aprender o que realmente importa: como organizar dados (as perguntas), como reagir a eventos (o clique do botão, o fim do tempo) e como transitar entre estados (pergunta atual, próxima pergunta, tela final). Esses três músculos você vai usar em todo jogo que fizer depois.

Como fazer um jogo de quiz: a estrutura de dados das perguntas

Todo quiz é, no fundo, uma lista de perguntas. Cada pergunta precisa de três informações: o texto da pergunta, as opções de resposta e qual opção é a correta. Em GDScript, o formato mais direto é um Array[Dictionary] constante:

const PERGUNTAS: Array[Dictionary] = [
    {
        "pergunta": "Qual linguagem é a nativa do Godot?",
        "opcoes": ["C#", "GDScript", "Lua", "Java"],
        "correta": 1
    },
    {
        "pergunta": "Qual nó emite o sinal timeout?",
        "opcoes": ["Timer", "Button", "Label", "Area2D"],
        "correta": 0
    },
    {
        "pergunta": "Qual container empilha filhos na vertical?",
        "opcoes": ["HBoxContainer", "GridContainer", "VBoxContainer", "PanelContainer"],
        "correta": 2
    },
]

O campo correta guarda o índice da resposta certa dentro de opcoes. Índice, não texto: comparar dois int é mais barato e menos frágil do que comparar strings.

Esse formato resolve para um quiz de até algumas dezenas de perguntas. Se o seu projeto crescer, duas evoluções naturais:

  • Resource customizado: crie uma classe Pergunta que estende Resource com @export var pergunta: String, @export var opcoes: Array[String] e @export var correta: int. Você ganha edição pelo Inspector, validação de tipo e arquivos .tres separados por tema.
  • Arquivo JSON: para bancos grandes ou editados por outra pessoa (um professor montando as perguntas, por exemplo), guarde tudo num .json e carregue em runtime:
func carregar_perguntas(caminho: String) -> Array[Dictionary]:
    var arquivo: FileAccess = FileAccess.open(caminho, FileAccess.READ)
    var dados: Variant = JSON.parse_string(arquivo.get_as_text())
    var resultado: Array[Dictionary] = []
    resultado.assign(dados)
    return resultado

Para este tutorial, o const no script basta. Comece simples.

Montando a UI com Control nodes

Crie uma cena nova com um nó raiz Control (chame de Quiz) e monte esta árvore:

  • Quiz (Control, âncora Full Rect)
    • VBoxContainer
      • LabelPlacar (Label)
      • BarraTempo (ProgressBar)
      • LabelPergunta (Label)
      • Botao0 (Button)
      • Botao1 (Button)
      • Botao2 (Button)
      • Botao3 (Button)
    • TimerPergunta (Timer)

O VBoxContainer empilha tudo na vertical e cuida do espaçamento sozinho, sem você posicionar nada na mão. No LabelPergunta, ative Autowrap para perguntas longas quebrarem linha. No TimerPergunta, defina Wait Time como 10 segundos e marque One Shot, porque o tempo de uma pergunta não deve reiniciar sozinho.

Marque cada nó que o script vai acessar com Access as Unique Name (clique direito no nó, ícone de porcentagem). Isso permite referenciar com %LabelPergunta sem depender do caminho na árvore, e a cena continua funcionando se você reorganizar os containers depois.

Se você nunca estruturou navegação entre cenas, vale ler o guia de como criar um menu principal no Godot, porque o quiz encaixa perfeitamente nesse fluxo: menu, jogo, resultado, e de volta ao menu.

Conectando os quatro botões por código, num loop

Aqui está o primeiro truque que separa código de iniciante de código limpo. Você poderia conectar cada botão pelo editor e criar quatro funções _on_botao_0_pressed, _on_botao_1_pressed e assim por diante. Funciona, mas duplica lógica e não escala.

O jeito certo: conectar todos à mesma função, passando o índice do botão com bind:

extends Control

@onready var label_pergunta: Label = %LabelPergunta
@onready var label_placar: Label = %LabelPlacar
@onready var barra_tempo: ProgressBar = %BarraTempo
@onready var temporizador: Timer = %TimerPergunta
@onready var botoes: Array[Button] = [%Botao0, %Botao1, %Botao2, %Botao3]

var indice_atual: int = 0
var pontuacao: int = 0
var perguntas_rodada: Array[Dictionary] = []


func _ready() -> void:
    for i: int in botoes.size():
        botoes[i].pressed.connect(_ao_responder.bind(i))
    temporizador.timeout.connect(_ao_esgotar_tempo)
    _iniciar_quiz()

O bind(i) "gruda" o valor de i na chamada. Quando o jogador clicar no Botao2, o sinal pressed dispara e _ao_responder recebe 2 automaticamente. Uma função, quatro botões, zero duplicação. Se essa mecânica de conectar e emitir ainda parece mágica, o tutorial de sinais no Godot 4 destrincha como signals funcionam por baixo do capô.

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

Lógica de verificação e placar

Com os botões conectados, a verificação é uma comparação de inteiros:

func _iniciar_quiz() -> void:
    pontuacao = 0
    indice_atual = 0
    perguntas_rodada = PERGUNTAS.duplicate()
    perguntas_rodada.shuffle()
    _atualizar_placar()
    _mostrar_pergunta()


func _ao_responder(indice_escolhido: int) -> void:
    temporizador.stop()
    var atual: Dictionary = perguntas_rodada[indice_atual]
    if indice_escolhido == int(atual["correta"]):
        pontuacao += 1
    _atualizar_placar()
    _avancar()


func _ao_esgotar_tempo() -> void:
    _avancar()


func _avancar() -> void:
    indice_atual += 1
    if indice_atual >= perguntas_rodada.size():
        _mostrar_resultado()
    else:
        _mostrar_pergunta()


func _atualizar_placar() -> void:
    label_placar.text = "Pontos: %d" % pontuacao

Repare em dois detalhes. O temporizador.stop() na primeira linha de _ao_responder evita que o timer estoure depois que o jogador já respondeu, o que pularia uma pergunta sem querer. E pontuacao é int declarado com tipo: se em algum lugar você tentar somar uma string ou um float por engano, o editor avisa antes de rodar.

Embaralhando perguntas e opções sem quebrar a resposta

Embaralhar a ordem das perguntas é fácil: perguntas_rodada.shuffle(), como fizemos em _iniciar_quiz (o duplicate() antes garante que a constante original não é modificada).

Embaralhar as opções de cada pergunta tem uma pegadinha clássica. Se você chamar shuffle() no array de opções, o índice guardado em correta passa a apontar para outra resposta, e o quiz começa a aceitar respostas erradas e recusar as certas. O bug é silencioso: nada quebra, o jogo só fica errado.

A solução: guarde o texto da resposta correta antes de embaralhar e recupere o novo índice depois:

func _preparar_pergunta(original: Dictionary) -> Dictionary:
    var opcoes: Array[String] = []
    opcoes.assign(original["opcoes"])
    var texto_correta: String = opcoes[int(original["correta"])]
    opcoes.shuffle()
    return {
        "pergunta": original["pergunta"],
        "opcoes": opcoes,
        "correta": opcoes.find(texto_correta)
    }


func _mostrar_pergunta() -> void:
    var atual: Dictionary = _preparar_pergunta(perguntas_rodada[indice_atual])
    perguntas_rodada[indice_atual] = atual
    label_pergunta.text = atual["pergunta"]
    for i: int in botoes.size():
        botoes[i].text = atual["opcoes"][i]
    temporizador.start()

O opcoes.find(texto_correta) devolve a nova posição da resposta certa depois do embaralhamento. Guardamos a versão preparada de volta em perguntas_rodada[indice_atual] para que _ao_responder compare contra o índice já reindexado, não contra o original.

Timer por pergunta com barra visual

O TimerPergunta já está configurado (10 segundos, One Shot) e conectado ao _ao_esgotar_tempo. Falta dar feedback visual, porque um limite de tempo invisível só frustra. É para isso que serve a ProgressBar:

func _process(_delta: float) -> void:
    if not temporizador.is_stopped():
        barra_tempo.value = temporizador.time_left / temporizador.wait_time * 100.0

A cada frame, a barra reflete a fração de tempo restante. time_left e wait_time são propriedades reais do nó Timer, então não precisamos de contador manual. Quando o tempo esgota, o sinal timeout dispara, _ao_esgotar_tempo chama _avancar() e o jogador simplesmente não pontua naquela pergunta.

Um refinamento barato: pinte a barra de vermelho abaixo de 30%, trocando o theme_override da ProgressBar conforme o valor. Detalhes assim fazem o quiz parecer um jogo de verdade.

Tela de resultado e reiniciar

Quando as perguntas acabam, mostre o desempenho e ofereça revanche. Você pode criar um segundo VBoxContainer na mesma cena (escondido no início) com um Label para a nota e um Button de reiniciar:

signal quiz_terminado(pontuacao_final: int, total: int)

@onready var painel_resultado: VBoxContainer = %PainelResultado
@onready var label_resultado: Label = %LabelResultado
@onready var botao_reiniciar: Button = %BotaoReiniciar


func _mostrar_resultado() -> void:
    temporizador.stop()
    for botao: Button in botoes:
        botao.hide()
    label_pergunta.hide()
    barra_tempo.hide()
    painel_resultado.show()
    var total: int = perguntas_rodada.size()
    var porcentagem: float = float(pontuacao) / float(total) * 100.0
    label_resultado.text = "Você acertou %d de %d (%.0f%%)" % [pontuacao, total, porcentagem]
    quiz_terminado.emit(pontuacao, total)


func _ao_reiniciar() -> void:
    get_tree().reload_current_scene()

Conecte botao_reiniciar.pressed.connect(_ao_reiniciar) no _ready. O get_tree().reload_current_scene() recarrega a cena do zero: pontuação, índice, visibilidade dos painéis, tudo volta ao estado inicial sem você precisar resetar variável por variável. Para um primeiro projeto, é a forma mais segura de reiniciar.

O sinal tipado quiz_terminado é opcional aqui, mas é um bom hábito: se amanhã você quiser tocar um som de vitória, salvar estatísticas ou desbloquear uma conquista, qualquer outro nó pode escutar esse evento sem que o script do quiz saiba da existência dele.

Ideias para evoluir o projeto

Terminou o quiz básico? Ótimo, agora ele vira laboratório. Algumas evoluções em ordem de dificuldade:

  • Feedback visual na resposta: pinte o botão de verde ou vermelho antes de avançar, com um pequeno atraso usando await get_tree().create_timer(0.8).timeout. Para transições mais elegantes, aprenda a animar UI por código com tween: fazer a pergunta deslizar para fora e a próxima entrar muda completamente a sensação do jogo.
  • Fases por tema: separe as perguntas em arquivos ou Resources por assunto (história, ciência, games) e deixe o jogador escolher o tema num menu antes de começar.
  • Dificuldade progressiva: reduza o wait_time do timer a cada pergunta certa, ou valha mais pontos por resposta rápida usando time_left.
  • Ranking local: salve as melhores pontuações num arquivo com FileAccess e store_var, e mostre o top 5 na tela de resultado. É a introdução perfeita a persistência de dados.

O mais importante: publique. Um quiz terminado e jogável ensina mais do que três projetos ambiciosos abandonados pela metade. Escopo fechado, projeto entregue, próximo desafio.

Perguntas frequentes

Preciso saber física ou animação para fazer um jogo de quiz no Godot?

Não. Um quiz usa só nós de interface (Control, VBoxContainer, Label, Button, ProgressBar) e lógica em GDScript. É exatamente por isso que ele é um ótimo primeiro projeto: você pratica dados, sinais e fluxo de telas sem precisar de física, sprites ou animação.

Como guardar as perguntas do quiz: Dictionary, Resource ou JSON?

Para um quiz pequeno, um Array[Dictionary] constante no próprio script resolve e é fácil de editar. Se o projeto crescer, um Resource customizado dá validação no editor e autocompletar. Para bancos grandes ou editáveis por outras pessoas, carregue de um arquivo JSON com JSON.parse_string().

Como conectar vários botões à mesma função no Godot 4?

Conecte por código num loop usando bind para passar o índice: botoes[i].pressed.connect(_ao_responder.bind(i)). Assim a função recebe qual botão foi clicado sem você duplicar código nem criar quatro funções quase iguais.

Como embaralhar as opções sem perder a resposta correta?

Guarde o texto da resposta correta antes de chamar shuffle() no array de opções. Depois de embaralhar, recupere o novo índice com opcoes.find(texto_correta). Se você embaralhar sem reindexar, o índice antigo passa a apontar para outra opção e o quiz aceita respostas erradas.

Como reiniciar o quiz quando o jogador termina?

O jeito mais simples é recarregar a cena inteira com get_tree().reload_current_scene(). Isso zera pontuação, índice e estado dos botões de uma vez, porque a cena volta ao estado inicial. Conecte essa chamada ao botão de reiniciar da tela de resultado.