Voltar para o Blog
Quest Log

Como Criar um Sistema de Quests e Missões para Seu Jogo

Aventureiro observando um quadro de missões com pergaminhos e um baú de recompensa em uma taverna de RPG

Aprenda a criar um sistema de quest para seu jogo no Godot 4: estrutura de dados com Resources, objetivos, progresso via signals e recompensas.

Como Criar um Sistema de Quests e Missões para Seu Jogo

Um sistema de quest no seu jogo é, no fundo, quatro problemas separados: descrever a missão (dados), acompanhar o que o jogador fez (progresso), avisar o resto do jogo quando algo muda (eventos) e entregar o prêmio no final (recompensa). Quem tenta resolver os quatro num script só acaba com um monstro de if que quebra toda vez que o design pede uma quest nova.

A boa notícia: separando essas quatro partes, o sistema fica pequeno, e adicionar a quest número 50 custa o mesmo que a número 2. É exatamente isso que vou montar aqui, em GDScript no Godot 4, com código que roda como está. Se você usa Unity ou outra engine, a arquitetura é a mesma, só muda a sintaxe.

A regra de ouro: quest é dado, não código

O erro mais comum que vejo é cada quest virar um script próprio: quest_matar_goblins.gd, quest_coletar_ervas.gd, e por aí vai. Funciona com 3 quests. Com 30, qualquer mudança no sistema obriga você a editar 30 arquivos.

A solução é tratar quest como conteúdo, não como lógica. A lógica (iniciar, progredir, concluir, recompensar) é escrita uma vez, num gerenciador. Cada quest é só um pacote de dados que descreve título, objetivos e recompensa. No Godot, o formato perfeito pra isso é o Resource: você define a estrutura em script e cria cada quest como um arquivo .tres no editor, sem escrever uma linha de código por quest.

Outro benefício que aparece depois: quest como dado é fácil de salvar, fácil de balancear (designer edita no Inspector, não no código) e fácil de exportar pra planilha quando o jogo cresce.

Estrutura de dados: a quest e seus objetivos

Duas classes resolvem a parte de dados. Primeiro o objetivo, que é a menor unidade rastreável: "mate 5 goblins", "colete 3 ervas", "fale com o ferreiro".

# quest_objective.gd
class_name QuestObjective
extends Resource

enum Tipo { COLETAR, MATAR, FALAR, ALCANCAR }

@export var id: String
@export var descricao: String
@export var tipo: Tipo = Tipo.COLETAR
@export var alvo: String          # ex: "goblin", "erva_vermelha", "ferreiro"
@export var quantidade: int = 1

O par tipo + alvo é o coração do design. Em vez de cada objetivo ter código próprio, ele declara o que observa: tipo MATAR com alvo "goblin" significa "me incremente toda vez que um goblin morrer". Quem mata o goblin não sabe que existe quest; só anuncia o fato, e o sistema cruza com os objetivos ativos. Esses quatro tipos cobrem a esmagadora maioria das quests de qualquer RPG, e adicionar um tipo novo é uma linha no enum.

A quest em si é um agrupador de objetivos mais a recompensa:

# quest.gd
class_name Quest
extends Resource

@export var id: String
@export var titulo: String
@export var descricao: String
@export var objetivos: Array[QuestObjective] = []
@export var recompensa_xp: int = 0
@export var recompensa_itens: Array[String] = []

Com esses dois scripts salvos, crie uma quest de verdade: no FileSystem, botão direito, New Resource, escolha Quest, salve como res://quests/limpar_a_floresta.tres. No Inspector você preenche título, descrição, e adiciona elementos no array de objetivos (cada um vira um QuestObjective embutido). Designer cria e edita quests sem tocar em código. Esse é o teste de que a separação dado/lógica funcionou.

Uma dica de organização que me poupou retrabalho: o id da quest deve ser igual ao nome do arquivo (limpar_a_floresta). Isso simplifica muito o carregamento na hora de restaurar um save, como você vai ver no final.

O QuestManager: lógica escrita uma vez

Toda a lógica vive num autoload (Project Settings > Globals > Autoload, adicione o script com o nome QuestManager). Autoload porque quests atravessam cenas: o jogador aceita a missão na vila, mata os goblins na floresta e entrega na vila de novo. O estado precisa sobreviver às trocas de cena.

# quest_manager.gd (autoload "QuestManager")
extends Node

signal quest_iniciada(quest: Quest)
signal objetivo_atualizado(quest: Quest, objetivo: QuestObjective, atual: int)
signal quest_concluida(quest: Quest)

var ativas: Dictionary = {}        # id -> Quest
var progresso: Dictionary = {}     # id da quest -> { id do objetivo: int }
var concluidas: Array[String] = []

func iniciar_quest(quest: Quest) -> void:
    if ativas.has(quest.id) or quest.id in concluidas:
        return
    ativas[quest.id] = quest
    var contadores := {}
    for obj in quest.objetivos:
        contadores[obj.id] = 0
    progresso[quest.id] = contadores
    quest_iniciada.emit(quest)

func registrar_evento(tipo: QuestObjective.Tipo, alvo: String, quantia: int = 1) -> void:
    for quest_id in ativas.keys():
        var quest: Quest = ativas[quest_id]
        var mudou := false
        for obj in quest.objetivos:
            if obj.tipo != tipo or obj.alvo != alvo:
                continue
            var atual: int = progresso[quest_id][obj.id]
            if atual >= obj.quantidade:
                continue
            atual = mini(atual + quantia, obj.quantidade)
            progresso[quest_id][obj.id] = atual
            mudou = true
            objetivo_atualizado.emit(quest, obj, atual)
        if mudou:
            _checar_conclusao(quest)

func _checar_conclusao(quest: Quest) -> void:
    for obj in quest.objetivos:
        if progresso[quest.id][obj.id] < obj.quantidade:
            return
    ativas.erase(quest.id)
    concluidas.append(quest.id)
    quest_concluida.emit(quest)

São umas 40 linhas e é o sistema inteiro. O método que importa é registrar_evento(): qualquer coisa que acontece no mundo passa por ele, e ele decide sozinho quais objetivos de quais quests aquele evento avança. Note o detalhe do mini() travando o contador no máximo: sem isso, matar 10 goblins numa quest de 5 deixa o save com 10/5, e qualquer lógica que compare igualdade exata quebra.

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

Conectando o mundo ao sistema com uma linha

Aqui é onde a arquitetura paga o investimento. O inimigo, o item e o NPC não conhecem nenhuma quest. Eles só reportam o que aconteceu:

# no script do inimigo
func morrer() -> void:
    QuestManager.registrar_evento(QuestObjective.Tipo.MATAR, "goblin")
    queue_free()
# no script do item coletável
func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("player"):
        QuestManager.registrar_evento(QuestObjective.Tipo.COLETAR, "erva_vermelha")
        queue_free()
# no fim do diálogo do NPC
QuestManager.registrar_evento(QuestObjective.Tipo.FALAR, "ferreiro")

Esse desacoplamento é o que mantém o projeto são. O goblin da fase 1 e o goblin da fase 9 emitem o mesmo evento. Se amanhã o design criar três quests novas envolvendo goblins, você não toca no goblin: cria três arquivos .tres e pronto. Compare com a alternativa de cada inimigo checar uma lista de quests dentro do próprio script de morte, e dá pra ver o tamanho da dor de cabeça evitada.

E pra dar a quest ao jogador, o NPC carrega o resource e inicia:

@export var quest_oferecida: Quest

func aceitar_quest() -> void:
    QuestManager.iniciar_quest(quest_oferecida)

O @export deixa você arrastar o .tres direto no Inspector do NPC. Mais uma vez: nenhum código por quest.

Progresso na tela: a UI escuta os signals

A UI nunca pergunta nada ao QuestManager todo frame. Ela conecta nos signals e reage quando algo muda, que é o padrão de comunicação do próprio Godot:

# quest_tracker.gd, num Label do HUD
extends Label

func _ready() -> void:
    QuestManager.quest_iniciada.connect(_on_quest_iniciada)
    QuestManager.objetivo_atualizado.connect(_on_objetivo_atualizado)
    QuestManager.quest_concluida.connect(_on_quest_concluida)

func _on_quest_iniciada(quest: Quest) -> void:
    text = quest.titulo

func _on_objetivo_atualizado(quest: Quest, obj: QuestObjective, atual: int) -> void:
    text = "%s: %d/%d" % [obj.descricao, atual, obj.quantidade]

func _on_quest_concluida(quest: Quest) -> void:
    text = "%s concluída!" % quest.titulo

Esse mesmo trio de signals serve pra tudo que reage a quest: tocar um som de "objetivo completo", mostrar um toast na tela, atualizar o diário de missões, marcar um ponto no minimapa. Cada um conecta no que interessa, e o QuestManager continua sem saber que nenhum deles existe.

Recompensas: entregue via evento, não dentro do manager

A tentação é dar XP e itens dentro do _checar_conclusao(). Resista. O QuestManager não deve conhecer o sistema de inventário nem o de level, senão você cria o acoplamento que passou o artigo inteiro evitando. Quem entrega a recompensa é quem escuta o signal:

# num script do player ou de um sistema de recompensas
func _ready() -> void:
    QuestManager.quest_concluida.connect(_on_quest_concluida)

func _on_quest_concluida(quest: Quest) -> void:
    xp += quest.recompensa_xp
    for item_id in quest.recompensa_itens:
        inventario.append(item_id)

A quest carrega os dados da recompensa (quanto XP, quais itens), mas a entrega acontece no sistema dono daquele dado. Se o seu jogo tiver recompensa de reputação depois, é mais um listener, zero mudança no manager.

Salvando e carregando o estado

Como todo o estado vive em três variáveis do QuestManager, salvar é trivial:

func salvar() -> Dictionary:
    return {
        "ativas": ativas.keys(),
        "progresso": progresso,
        "concluidas": concluidas,
    }

func carregar(dados: Dictionary) -> void:
    ativas.clear()
    for quest_id in dados["ativas"]:
        ativas[quest_id] = load("res://quests/%s.tres" % quest_id)
    progresso = dados["progresso"]
    concluidas.clear()
    concluidas.assign(dados["concluidas"])

Repare que o save guarda só os ids das quests ativas, nunca o resource inteiro. O .tres é conteúdo do jogo e já está no projeto; o save só precisa saber quais estão em andamento e os contadores. É aqui que a convenção "id igual ao nome do arquivo" se paga: o load() reconstrói tudo a partir do id. Esse dicionário você serializa com JSON.stringify() junto do resto do seu save.

Pra onde crescer depois

O sistema acima aguenta um jogo comercial pequeno sem mudar de forma. Quando o design pedir mais, as extensões naturais são:

  • Pré-requisitos: um campo @export var requer: Array[String] na Quest, e o iniciar_quest() recusa se algum id não estiver em concluidas. Cadeias de missão saem de graça.
  • Objetivos sequenciais: um índice de "objetivo atual" no progresso, e registrar_evento() só aceita eventos do objetivo da vez. É o que diferencia "faça nessa ordem" de "faça em qualquer ordem".
  • Entrega manual: muitas quests terminam falando com o NPC de novo. Modele isso como um objetivo FALAR no final da lista, sem nenhum mecanismo novo.
  • Quests falháveis: um signal quest_falhada e um array falhadas, pro caso de missão com timer ou NPC que pode morrer.

Fechando

A decisão que carrega o sistema inteiro nas costas é uma só: quest é dado, lógica é escrita uma vez. Disso decorre todo o resto, o Resource editável no Inspector, o registrar_evento() que desacopla o mundo das missões, os signals que alimentam UI e recompensa, o save que cabe num dicionário.

Monte a versão mínima primeiro: uma quest de matar 3 inimigos, o contador no HUD, o XP no final. Funcionou, adicione a segunda quest sem escrever código novo. Se você conseguiu, a arquitetura está certa e dali em diante é só produzir conteúdo. Se precisou abrir o manager pra segunda quest funcionar, tem acoplamento escondido, e é melhor achar agora do que na quest 40.