Voltar para o Blog
Quest Log

Como Fazer um Jogo Idle / Clicker no Godot 4

Tela de um jogo idle clicker no Godot mostrando um botão grande, o total de moedas e uma lista de upgrades.

Como fazer um jogo idle / clicker no Godot 4: clique, produção automática, upgrades com custo crescente, salvar com ConfigFile e progresso offline.

Como Fazer um Jogo Idle / Clicker no Godot 4

Fazer um jogo idle / clicker no Godot 4 é um dos melhores projetos pra quem quer terminar algo de verdade: o escopo é pequeno, o loop cabe na cabeça e dá pra ter uma versão jogável numa tarde. O gênero, no estilo de Cookie Clicker, gira em torno de uma ideia simples: o jogador clica pra ganhar uma moeda, compra geradores que produzem essa moeda sozinhos, e usa o que acumulou pra comprar upgrades cada vez mais caros. Os números só sobem, e ver eles subindo é a recompensa.

Nesse tutorial a gente monta o jogo por camadas, sempre com GDScript tipado e API real do Godot 4: primeiro o núcleo do clique, depois a produção automática, os upgrades com custo crescente, o save com ConfigFile e, por fim, o progresso offline. No caminho eu vou ser honesto sobre onde mora a dificuldade do gênero, que não é o código.

Antes de escrever a primeira linha, vale lembrar que tudo isso é um core loop. Se você quer entender por que o ciclo de clicar, acumular e comprar prende, leia o que é o core loop de um jogo: o idle é talvez o gênero mais puro pra estudar essa mecânica, porque ele é basicamente só o loop, sem nada em volta.

O núcleo do jogo idle / clicker no Godot

O coração de qualquer clicker é uma variável que conta o recurso e um botão que a incrementa. Vamos chamar a moeda de moedas e usar um Button que dispara o sinal pressed, mais um Label que mostra o total atualizado.

Monte uma cena com um Control na raiz, um Button chamado BotaoClique, um Label chamado LabelMoedas e, mais pra frente, um Label pra produção por segundo. O script abaixo vai na raiz e centraliza o estado do jogo.

extends Control

var moedas: float = 0.0
var moedas_por_clique: float = 1.0

@onready var label_moedas: Label = $LabelMoedas
@onready var botao_clique: Button = $BotaoClique

func _ready() -> void:
    botao_clique.pressed.connect(_on_clique)
    _atualizar_label()

func _on_clique() -> void:
    moedas += moedas_por_clique
    _atualizar_label()

func _atualizar_label() -> void:
    label_moedas.text = "Moedas: %d" % int(moedas)

Repare em duas escolhas. A moeda é float, não int: jogos idle chegam a números enormes e a produção fracionária por frame exige casas decimais. A gente só converte pra int na hora de mostrar, com %d, pra não poluir a tela com decimais. E o sinal pressed é conectado por código no _ready, o que deixa claro no script quem responde ao botão, em vez de esconder a conexão no editor.

Isso já é um clicker funcional: clica, o número sobe, o label atualiza. Mas um jogo que só tem clique é cansativo. A alma do gênero é a próxima camada.

Produção automática com Timer e geradores

O que transforma um clicker num idle é a produção que acontece sozinha. O jogador compra um gerador e, a partir dali, ganha moedas por segundo sem tocar em nada. A forma mais limpa de fazer isso no Godot 4 é acumular a produção a cada frame usando o delta do _process, que já te dá o tempo decorrido em segundos.

Vamos representar cada tipo de gerador com um dicionário tipado: nome, quantidade que o jogador possui, custo atual e quanto cada unidade produz por segundo.

var geradores: Array[Dictionary] = [
    { "nome": "Aprendiz", "quantidade": 0, "custo": 15.0, "producao": 0.2 },
    { "nome": "Oficina", "quantidade": 0, "custo": 100.0, "producao": 1.5 },
    { "nome": "Fábrica", "quantidade": 0, "custo": 1100.0, "producao": 12.0 },
]

func producao_por_segundo() -> float:
    var total: float = 0.0
    for g in geradores:
        total += float(g["quantidade"]) * float(g["producao"])
    return total

func _process(delta: float) -> void:
    moedas += producao_por_segundo() * delta
    _atualizar_label()

A função producao_por_segundo soma a contribuição de todos os geradores que o jogador tem. No _process, a gente multiplica essa produção pelo delta pra creditar só a fração correspondente ao frame. Assim, independente do jogo rodar a 30 ou 144 FPS, o ganho por segundo é o mesmo. Esse é o jeito correto de lidar com tempo no Godot: nunca assuma um número fixo de frames.

Se você preferir ganhos em "pulsos" certinhos a cada segundo, em vez de acúmulo contínuo, dá pra usar um Timer no modo repetição:

@onready var timer_producao: Timer = $TimerProducao

func _ready() -> void:
    timer_producao.wait_time = 1.0
    timer_producao.timeout.connect(_on_tick_producao)
    timer_producao.start()

func _on_tick_producao() -> void:
    moedas += producao_por_segundo()
    _atualizar_label()

As duas abordagens funcionam. O _process com delta dá uma sensação mais suave de número subindo; o Timer dá pulsos visíveis a cada segundo, que combinam com jogos que mostram "+X" piscando. Escolha uma e seja consistente. Para um clicker eu costumo ficar com o delta, porque o crescimento contínuo é mais satisfatório de assistir.

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

Upgrades com custo crescente

Aqui entra o mecanismo que define o ritmo do jogo inteiro: comprar um gerador deixa o próximo da mesma categoria mais caro. Sem isso, o jogador compraria tudo em segundos e o jogo morreria. A fórmula clássica do gênero é um crescimento exponencial do preço, onde cada compra multiplica o custo por um fator fixo.

const FATOR_CUSTO: float = 1.15

func comprar_gerador(indice: int) -> bool:
    var g: Dictionary = geradores[indice]
    var custo: float = float(g["custo"])
    if moedas < custo:
        return false
    moedas -= custo
    g["quantidade"] = int(g["quantidade"]) + 1
    g["custo"] = custo * FATOR_CUSTO
    _atualizar_label()
    return true

O fator 1.15 significa que cada unidade comprada deixa a próxima 15% mais cara. É exatamente a curva que Cookie Clicker usa, e ela é boa porque cresce devagar no começo, deixando o jogador comprar várias unidades rápido, e acelera depois, criando aqueles momentos de espera ansiosa por mais um gerador. A função retorna bool pra você saber se a compra deu certo e poder, por exemplo, tocar um som de erro quando faltar moeda.

Pra mostrar e habilitar os botões de compra na interface, percorra os geradores e compare o custo com o saldo:

func _atualizar_botoes_compra(botoes: Array[Button]) -> void:
    for i in range(geradores.size()):
        var g: Dictionary = geradores[i]
        var custo: float = float(g["custo"])
        botoes[i].text = "%s (x%d) - %d moedas" % [g["nome"], int(g["quantidade"]), int(custo)]
        botoes[i].disabled = moedas < custo

Esse disabled = moedas < custo é um detalhe pequeno que melhora muito a leitura: o botão acende quando você pode comprar, então o jogador sabe na hora o que está ao alcance. É feedback visual de graça.

Vou ser direto sobre uma coisa: o código que você viu até aqui é praticamente o jogo todo. A parte difícil de um idle não é programar, é escolher os números. Custo inicial, fator de crescimento, produção de cada gerador, ritmo de desbloqueio: são essas curvas que decidem se o jogo é viciante ou tedioso. Acertar isso é trabalho de planilha e de teste, e vale estudar as fórmulas de balanceamento antes de definir os valores no chute. Um FATOR_CUSTO de 1.07 e outro de 1.30 produzem jogos completamente diferentes com o mesmo código.

Salvar e carregar com ConfigFile

Um jogo idle que não salva é inútil: a ideia é justamente voltar e ver o que acumulou. A classe ConfigFile do Godot 4 é perfeita pra isso, porque o estado de um clicker é uma lista plana de chaves e valores. Os arquivos ficam em user://, que o Godot mapeia pra uma pasta segura do sistema operacional.

const CAMINHO_SAVE: String = "user://idle_save.cfg"

func salvar_jogo() -> void:
    var config: ConfigFile = ConfigFile.new()
    config.set_value("jogo", "moedas", moedas)
    config.set_value("jogo", "saida_unix", Time.get_unix_time_from_system())
    for i in range(geradores.size()):
        config.set_value("geradores", "qtd_%d" % i, geradores[i]["quantidade"])
        config.set_value("geradores", "custo_%d" % i, geradores[i]["custo"])
    config.save(CAMINHO_SAVE)

A gente grava as moedas, o instante da saída (pro progresso offline, já já) e, pra cada gerador, a quantidade e o custo atual. Salvar o custo é importante: como ele cresce a cada compra, guardar só a quantidade não bastaria pra reconstruir o preço sem refazer a conta toda.

O carregamento espelha o save. O método load retorna um código de erro, e a gente checa por OK antes de ler qualquer coisa, pra não quebrar na primeira vez que o jogo roda, quando o arquivo ainda nem existe.

func carregar_jogo() -> void:
    var config: ConfigFile = ConfigFile.new()
    var erro: Error = config.load(CAMINHO_SAVE)
    if erro != OK:
        return
    moedas = config.get_value("jogo", "moedas", 0.0)
    for i in range(geradores.size()):
        geradores[i]["quantidade"] = config.get_value("geradores", "qtd_%d" % i, 0)
        geradores[i]["custo"] = config.get_value("geradores", "custo_%d" % i, geradores[i]["custo"])
    _atualizar_label()

O terceiro argumento de get_value é o padrão, usado quando a chave não existe. Isso te dá tolerância a saves antigos: se você adicionar um gerador novo numa atualização, o jogo carrega os antigos e assume zero pro recém-chegado, sem dar erro. Pra disparar o save, conecte salvar_jogo ao fechamento da janela tratando a notificação NOTIFICATION_WM_CLOSE_REQUEST no _notification, e chame carregar_jogo no _ready. Se quiser entender as variações dessa técnica em outros gêneros, o sistema de save e load no Godot detalha as alternativas ao ConfigFile.

Progresso offline com timestamp

O recurso que define o gênero idle é render moedas mesmo com o jogo fechado. A lógica é direta: na saída a gente guardou saida_unix com Time.get_unix_time_from_system, que retorna o tempo Unix em segundos. Ao abrir de novo, comparamos esse valor com o instante atual pra saber quantos segundos o jogador ficou fora, e creditamos a produção desse período.

const MULT_OFFLINE: float = 0.5
const MAX_OFFLINE_SEGUNDOS: float = 8.0 * 60.0 * 60.0

func aplicar_progresso_offline() -> float:
    var config: ConfigFile = ConfigFile.new()
    if config.load(CAMINHO_SAVE) != OK:
        return 0.0
    var saida: float = config.get_value("jogo", "saida_unix", 0.0)
    if saida <= 0.0:
        return 0.0
    var agora: float = Time.get_unix_time_from_system()
    var segundos_fora: float = clampf(agora - saida, 0.0, MAX_OFFLINE_SEGUNDOS)
    var ganho: float = producao_por_segundo() * segundos_fora * MULT_OFFLINE
    moedas += ganho
    _atualizar_label()
    return ganho

Chame aplicar_progresso_offline no _ready, logo depois de carregar_jogo, e mostre o ganho retornado numa janelinha de boas-vindas do tipo "Enquanto você esteve fora, ganhou X moedas". Esse retorno é metade da graça de abrir o jogo de novo.

Duas decisões de design importam aqui. O MULT_OFFLINE de 0.5 faz o tempo offline render metade do que renderia jogando, pra que ficar com o jogo aberto ainda valha a pena; sem isso, o jogo se joga sozinho e perde o sentido. E o clampf com MAX_OFFLINE_SEGUNDOS limita o crédito a oito horas, pra que voltar depois de um mês não estoure o jogo com um ganho absurdo de uma vez. Esses dois números são, de novo, balanceamento: o código é o mesmo, mas o equilíbrio muda tudo.

Um cuidado técnico: Time.get_unix_time_from_system usa o relógio do sistema, que o jogador pode adiantar pra "trapacear" e ganhar mais offline. Pra um jogo de portfólio ou single player isso não importa. Se um dia virar um jogo competitivo ou com compras, a hora teria que vir de um servidor, não da máquina local. Mas pra aprender e pra a esmagadora maioria dos casos, o relógio do sistema resolve.

Próximos passos

Você tem, agora, um jogo idle / clicker completo no Godot 4: clique que rende moeda, geradores que produzem sozinhos via delta, upgrades que ficam mais caros a cada compra, save e load com ConfigFile e progresso offline com timestamp. Esse é o esqueleto inteiro do gênero, e ele cabe em poucas dezenas de linhas tipadas.

O que vem depois não é mais código, é mais conteúdo e mais tuning. Algumas direções naturais: prestígio (resetar o progresso em troca de um bônus permanente que acelera a próxima run), conquistas que dão multiplicadores, upgrades que melhoram o clique manual ou a produção de um gerador específico, e efeitos visuais como número subindo a cada clique pra dar peso. A maior parte do tempo, porém, vai pra ajustar as curvas até o ritmo ficar gostoso, e isso só se descobre jogando o seu próprio jogo por horas.

Se esse projeto te mostrou que você gosta de construir sistemas e quer parar de montar peça por peça sem enxergar o todo, o caminho é aprender a engine a fundo, com base sólida de GDScript tipado e arquitetura. É exatamente isso que a gente ensina passo a passo no melhor curso de Godot: do clicker simples até projetos que você realmente termina e publica.

Perguntas frequentes

O que é um jogo idle / clicker?

É um gênero em que o jogador acumula um recurso clicando e, principalmente, comprando geradores que produzem esse recurso sozinhos, mesmo sem interação. O exemplo clássico é Cookie Clicker: você clica num biscoito, compra fábricas e elas passam a gerar biscoitos por segundo. A graça está em ver os números crescerem e em decidir no que investir.

Idle game e clicker são a mesma coisa?

São primos próximos e na prática se misturam. Clicker enfatiza o clique manual como motor inicial; idle (ou incremental) enfatiza a produção automática que roda enquanto você não faz nada, inclusive com o jogo fechado. A maioria dos jogos do gênero tem os dois lados: você clica no começo e, conforme compra geradores, passa a só observar e otimizar.

Por que o custo dos upgrades sobe a cada compra?

Para que a progressão não estoure. Se o preço fosse fixo, em poucos minutos você compraria tudo e o jogo acabaria. Com um custo que cresce de forma exponencial, cada novo gerador exige acumular bem mais que o anterior, o que cria a tensão de esperar e a recompensa de finalmente poder comprar. Esse crescimento é o coração do balanceamento do gênero.

Como salvar o progresso de um jogo idle no Godot 4?

A forma mais simples e legível é a classe ConfigFile, que grava pares chave/valor num arquivo .cfg dentro de user://. Você salva o total de moedas, a quantidade de cada gerador e um timestamp. Para projetos maiores dá para usar JSON ou recursos, mas o ConfigFile resolve bem a estrutura plana de um clicker.

Como creditar a produção enquanto o jogo esteve fechado?

Você guarda o instante da saída com Time.get_unix_time_from_system e, ao abrir de novo, calcula a diferença em segundos contra o instante atual. Multiplique esse tempo ausente pela produção por segundo dos geradores e some o resultado às moedas. É comum aplicar um teto e um multiplicador menor que 1 para que ficar offline não renda mais que jogar ativamente.

O que é mais difícil em um jogo idle: o código ou o balanceamento?

O balanceamento, sem dúvida. O loop de código é simples e você monta o esqueleto em uma tarde. O desafio real é afinar os números: custo inicial, taxa de crescimento do preço, produção de cada gerador e ritmo de desbloqueio. Errar essas curvas faz o jogo virar tedioso ou acabar rápido demais. É tuning, e tuning se faz testando.