Voltar para o Blog
Quest Log

Sistema de Pontuacao e Recorde no Godot 4 (Score e High Score)

Tela de um jogo no Godot mostrando o score atual e o recorde no canto superior da HUD

Monte um sistema de pontuacao godot com autoload, sinal para a HUD, Label de score e high score salvo em ConfigFile. Tutorial pratico em GDScript.

Quase todo jogo arcade depende de um numero subindo na tela. Pontos por inimigo destruido, moedas coletadas, tempo de sobrevivencia. Montar um sistema de pontuacao godot que seja confiavel, facil de ler de qualquer cena e que ainda guarde o recorde entre sessoes parece simples, mas costuma virar bagunca quando voce espalha variaveis de score por varios scripts. Neste post a gente organiza isso de um jeito que escala: um autoload central, um sinal que avisa a UI quando o numero muda, um Label na HUD e o high score gravado em disco com ConfigFile.

Sistema de Pontuacao e Recorde no Godot 4 (Score e High Score)

A ideia central e separar quem guarda o dado de quem mostra o dado. O gameplay so chama uma funcao para somar pontos. A HUD so escuta um sinal e atualiza o texto. Nenhum dos dois precisa conhecer os detalhes do outro. Isso evita aquele acoplamento em que o inimigo precisa de uma referencia direta ao Label da interface para somar 10 pontos.

Vou assumir que voce ja fez seu primeiro jogo: sabe criar cenas, anexar scripts e conectar nodes. O que talvez ainda nao tenha visto e o padrao de autoload (singleton) e como persistir dados de forma limpa.

Por que usar um autoload para o score

Um autoload e um script (ou cena) que o Godot carrega uma vez e mantem vivo durante toda a execucao do jogo. Ele fica acessivel por nome de qualquer outra cena, sem precisar de get_node com caminhos frageis. Isso resolve um problema real: quando o jogador morre e voce troca de cena, uma variavel comum de score seria destruida junto com a cena. O autoload sobrevive.

Crie um arquivo score_manager.gd:

extends Node

signal score_changed(novo_valor: int)
signal high_score_changed(novo_valor: int)

var score: int = 0
var high_score: int = 0

func adicionar_pontos(quantidade: int) -> void:
    score += quantidade
    score_changed.emit(score)
    if score > high_score:
        high_score = score
        high_score_changed.emit(high_score)

func resetar_score() -> void:
    score = 0
    score_changed.emit(score)

Agora registre como autoload em Project > Project Settings > Globals (aba Autoload). Adicione o arquivo score_manager.gd e de a ele o nome Score. A partir desse ponto, qualquer script pode chamar Score.adicionar_pontos(10).

Repare que adicionar_pontos faz tres coisas e nada mais: soma, avisa que o valor mudou e checa o recorde. Toda a logica de pontuacao mora em um lugar so. Se amanha voce quiser dobrar pontos durante um combo, muda aqui e o jogo inteiro respeita a regra.

O sinal que conecta o score a UI

O signal score_changed e o que torna o sistema desacoplado. Em vez da HUD perguntar "qual o score?" a cada frame dentro de um _process, ela e avisada apenas quando algo realmente muda. Isso e mais barato e mais claro.

Quem dispara o sinal e o ScoreManager. Quem escuta e a HUD. O inimigo nem sabe que existe uma interface. Ele so faz:

extends Area2D

@export var pontos_ao_morrer: int = 10

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("player"):
        Score.adicionar_pontos(pontos_ao_morrer)
        queue_free()

Esse padrao de sinal global tambem aparece em outros sistemas que ficam melhores centralizados, como um sistema de conquistas, onde varios eventos do jogo precisam notificar uma camada que o jogador nao ve diretamente.

Mostrando o score com um Label na HUD

Crie uma cena de HUD usando um node CanvasLayer como raiz. O CanvasLayer garante que a interface fique fixa na tela mesmo que a camera se mova pelo mundo. Dentro dele, adicione dois nodes Label: um para o score atual e outro para o recorde.

No script da HUD, conecte os sinais do autoload no _ready e atualize o texto:

extends CanvasLayer

@onready var label_score: Label = $LabelScore
@onready var label_recorde: Label = $LabelRecorde

func _ready() -> void:
    Score.score_changed.connect(_on_score_changed)
    Score.high_score_changed.connect(_on_high_score_changed)
    # Sincroniza o estado inicial caso o score ja tenha valor
    _on_score_changed(Score.score)
    _on_high_score_changed(Score.high_score)

func _on_score_changed(novo_valor: int) -> void:
    label_score.text = "Pontos: %s" % formatar_numero(novo_valor)

func _on_high_score_changed(novo_valor: int) -> void:
    label_recorde.text = "Recorde: %s" % formatar_numero(novo_valor)

Um detalhe que muita gente esquece: chamar _on_score_changed(Score.score) no _ready. Se voce nao fizer isso, o Label so vai mostrar algo depois da primeira mudanca de pontos. Se o jogador comeca a partida e fica parado, a tela mostraria um Label vazio. Sincronizar o estado inicial resolve.

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

Formatar o numero para ficar legivel

Um score de 1250000 na tela e dificil de ler. Vale separar os milhares com ponto, no padrao brasileiro. O Godot 4 nao tem uma funcao pronta de formatacao de milhar com separador, entao escrevemos uma pequena funcao que insere o ponto a cada tres digitos, da direita para a esquerda:

func formatar_numero(valor: int) -> String:
    var texto: String = str(abs(valor))
    var resultado: String = ""
    var contador: int = 0
    # Percorre os digitos de tras para frente
    for i in range(texto.length() - 1, -1, -1):
        resultado = texto[i] + resultado
        contador += 1
        if contador % 3 == 0 and i != 0:
            resultado = "." + resultado
    if valor < 0:
        resultado = "-" + resultado
    return resultado

Com isso, 1250000 vira 1.250.000. A funcao trata o sinal negativo separadamente, caso o seu jogo permita score abaixo de zero (penalidades, por exemplo). E uma logica simples, mas que melhora muito a leitura na HUD sem depender de bibliotecas externas.

Se voce preferir o estilo internacional com virgula, troque o "." por ",". A funcao continua igual.

Salvar o high score em ConfigFile

Aqui esta a parte que sobrevive ao fechamento do jogo. O ConfigFile e uma classe do Godot que le e escreve arquivos no formato .ini, organizado em secoes e chaves. E uma escolha boa para poucos dados, como configuracoes e recordes, porque o arquivo gerado e legivel e o codigo e curto.

Vamos salvar em user://, que e a pasta de dados do usuario gerenciada pelo Godot. No Windows ela fica em %APPDATA%, no Linux em ~/.local/share. Voce nunca grava na pasta do jogo em si, porque em uma build instalada ela pode ser somente leitura.

Adicione ao score_manager.gd:

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

func salvar_high_score() -> void:
    var config := ConfigFile.new()
    config.set_value("pontuacao", "high_score", high_score)
    config.save(CAMINHO_SAVE)

func carregar_high_score() -> void:
    var config := ConfigFile.new()
    var erro := config.load(CAMINHO_SAVE)
    if erro != OK:
        # Arquivo ainda nao existe (primeira vez jogando)
        high_score = 0
        return
    high_score = config.get_value("pontuacao", "high_score", 0)
    high_score_changed.emit(high_score)

O config.load retorna um codigo de erro. Quando vale OK, deu certo. Qualquer outro valor (tipicamente na primeira execucao, quando o arquivo nao existe) cai no if e mantemos o recorde em zero. Sempre cheque esse retorno, porque tentar ler de um arquivo inexistente sem tratamento gera valores inesperados.

Repare no terceiro argumento de get_value: o 0. Ele e o valor padrao caso a chave nao exista no arquivo. Isso protege contra um save corrompido ou parcial.

Para carregar no inicio do jogo, chame carregar_high_score no _ready do proprio autoload:

func _ready() -> void:
    carregar_high_score()

E para gravar, o melhor momento e quando o jogador morre ou termina a fase. Se voce ja tem uma tela de game over, e ali que voce dispara o salvamento:

func _on_jogador_morreu() -> void:
    Score.salvar_high_score()
    get_tree().change_scene_to_file("res://cenas/game_over.tscn")

Salvar so no fim da partida evita escrever no disco a cada ponto somado, o que seria desperdicio. O ConfigFile so atualiza o recorde quando realmente vale a pena: ao encerrar a sessao de jogo.

Juntando tudo no fluxo da partida

Vale revisar a ordem das coisas para nao errar na integracao. Quando o jogo inicia, o autoload carrega o recorde do disco. Durante a partida, cada inimigo destruido chama Score.adicionar_pontos, que dispara o sinal e a HUD se atualiza. Quando o score passa o recorde, o segundo Label tambem muda em tempo real, o que e um detalhe que deixa o jogador atento de que esta batendo o proprio recorde.

Ao iniciar uma nova partida, lembre de chamar Score.resetar_score(). Como o autoload nao morre entre cenas, o score anterior continuaria la se voce esquecesse. Um bom lugar para o reset e no _ready da cena principal de gameplay:

extends Node2D

func _ready() -> void:
    Score.resetar_score()

Se o seu jogo guarda mais coisas alem do recorde (progresso de fase, itens desbloqueados, configuracoes de audio), em algum momento o ConfigFile pode ficar apertado e voce vai querer um sistema mais robusto. Nesse caso, dê uma olhada em como estruturar salvar e carregar no Godot com mais dados e formatos como JSON ou recursos serializados.

Fechando o sistema de pontuacao godot

O que torna esse desenho bom nao e nenhum truque escondido, e a separacao de responsabilidades. O autoload guarda e centraliza a regra de pontos. O sinal avisa quem se importa. A HUD so reage. O ConfigFile cuida da persistencia sem misturar leitura de disco com logica de jogo. Cada peca faz uma coisa, e por isso voce consegue mexer em uma sem quebrar as outras.

A partir daqui da pra evoluir sem reescrever nada: multiplicadores de combo, pontos por tempo, varios recordes por modo de jogo (basta usar mais chaves no ConfigFile). A base ja aguenta. Comece pelo autoload com o sinal, faca o Label atualizar, e so depois adicione a persistencia. Testar em partes evita ficar cacando bug em tres lugares ao mesmo tempo.