Voltar para o Blog
Quest Log

Save na Nuvem (Cloud Save) na Godot 4: Progresso Entre Dispositivos

Ilustração de um save de jogo sincronizando entre um PC e um celular através de uma nuvem

Save na nuvem na Godot 4 sem inventar API: as opções reais (Steam Cloud, plataformas móveis, backend próprio) e um tutorial de cloud save via HTTPRequest.

Cloud save é uma daquelas features que parecem triviais até você abrir a documentação e perceber que a Godot 4 não tem save na nuvem nativo. Não existe um botão "sincronizar progresso" pronto. O que existe é um conjunto de peças (salvar local, fazer requisições HTTP, ler e escrever JSON) que você monta para fazer o progresso do jogador seguir de um dispositivo para outro. Este post é sobre montar isso com honestidade, sem inventar API que não existe.

Antes de escrever qualquer linha, vale separar as opções reais das promessas de tutorial mágico. Save na nuvem na Godot não é uma coisa só, são três caminhos diferentes, cada um com um custo e um público. Vou explicar os três e depois mergulhar no que dá mais controle: seu próprio backend, com o nó HTTPRequest.

As três opções reais de save na nuvem na Godot 4

Nenhuma delas é "a Godot faz sozinha". Escolha pela plataforma onde você lança.

1. Steam Cloud via GodotSteam. Se você já publica na Steam, essa é a rota mais simples. O Steam consegue sincronizar a pasta de save de duas formas: o Auto-Cloud, que você configura no painel do Steamworks apontando quais arquivos em user:// devem subir, sem uma linha de código a mais; ou a API ISteamRemoteStorage, exposta na Godot pelo módulo GodotSteam, quando você quer controlar o que sobe e quando. Nos dois casos você continua salvando local normalmente e o Steam cuida da sincronização entre os PCs do jogador. Se esse é o seu caso, o guia de integrar Steam com a Godot (GodotSteam) mostra como ligar o módulo antes de mexer no Cloud.

2. Serviços de plataforma (mobile). No Android existe o Google Play Games Saved Games; no iOS existe o iCloud. Os dois resolvem cloud save de forma integrada à conta do jogador, mas a Godot não fala com eles de fábrica. Você quase sempre depende de um plugin de terceiros (um Android plugin ou um iOS plugin da comunidade) para expor essas APIs ao GDScript. É o caminho certo para jogos mobile, com a ressalva de que a qualidade e a manutenção do plugin variam.

3. Backend próprio via HTTP. Você salva local (como faz no sistema de save e load na Godot) e, além disso, envia e baixa o arquivo de save para uma API REST sua, ou um serviço BaaS. É a opção mais trabalhosa e a que te dá mais controle: funciona em qualquer plataforma, não depende de loja e você decide as regras. É nela que este tutorial foca, porque é a que usa API real da Godot e vale para qualquer projeto.

O ponto honesto que vale repetir: a URL, o servidor e o banco de dados são responsabilidade sua. A Godot só faz a requisição. Não existe endpoint mágico que eu possa te dar; o que dá para ensinar é como o jogo conversa com esse endpoint.

O plano do backend próprio

A ideia é simples e não muda com a linguagem do servidor. Cada jogador tem um save (um JSON). O jogo faz duas operações contra a sua API:

  • Subir (POST): manda o JSON atual do save para o servidor gravar.
  • Baixar (GET): pede o save que está no servidor para restaurar o progresso.

O save continua sendo gravado local com FileAccess, exatamente como num jogo offline. A nuvem é uma cópia que espelha o local. O trabalho difícil não é subir nem baixar, é decidir quem manda quando os dois discordam. Deixo isso para o fim, porque é onde os jogadores perdem progresso.

Subindo o save com HTTPRequest (POST)

O nó HTTPRequest é o que fala com a internet na Godot 4. Você adiciona o nó, conecta o sinal request_completed e chama o método request(). A assinatura completa é request(url: String, custom_headers: PackedStringArray, method: HTTPClient.Method, request_data: String).

Monte o corpo com JSON.stringify e mande como POST. O token de autenticação vai no header, nunca no corpo em texto puro nem fixo no código.

extends Node

# A URL do SEU backend. Isto NÃO é um endpoint pronto da Godot,
# é a sua API. Troque pela sua.
const API_BASE: String = "https://api.seujogo.com"

@onready var http: HTTPRequest = $HTTPRequest

var _token: String = ""  # preenchido pelo seu login

func _ready() -> void:
    http.request_completed.connect(_on_request_completed)

func subir_save(dados: Dictionary) -> void:
    # Carimba timestamp e versão ANTES de subir. É o que resolve
    # conflito depois.
    dados["timestamp"] = Time.get_unix_time_from_system()
    dados["versao"] = 1

    var corpo: String = JSON.stringify(dados)
    var headers: PackedStringArray = [
        "Content-Type: application/json",
        "Authorization: Bearer %s" % _token
    ]

    var erro: int = http.request(
        API_BASE + "/save",
        headers,
        HTTPClient.METHOD_POST,
        corpo
    )

    # request() pode falhar antes mesmo de sair da máquina:
    # sem internet, URL inválida, outra requisição em andamento.
    if erro != OK:
        push_error("Falha ao iniciar upload do save: %d" % erro)

Repare que request() retorna um código na hora. Ele não te diz se o servidor recebeu, só se a Godot conseguiu disparar a requisição. Um HTTPRequest só cuida de uma requisição por vez; se você chamar de novo antes de terminar, ela falha. Para operações em paralelo, use nós separados ou uma fila.

Tratando a resposta e os erros de rede

Todo o resultado chega pelo sinal request_completed(result, response_code, headers, body). São dois níveis de erro para checar, e ignorar qualquer um deles é como o jogo trava na mão do jogador.

O result diz se a requisição chegou ao servidor (compare com HTTPRequest.RESULT_SUCCESS). Se o jogador está sem internet, é aqui que você descobre. O response_code é o código HTTP do servidor (200, 401, 500), e só faz sentido olhar se o result foi sucesso.

func _on_request_completed(
    result: int,
    response_code: int,
    _headers: PackedStringArray,
    body: PackedByteArray
) -> void:
    # Nível 1: a requisição chegou ao servidor?
    if result != HTTPRequest.RESULT_SUCCESS:
        push_error("Sem conexão com o servidor (result=%d)" % result)
        # Aqui o save local já existe. O jogo segue offline e
        # tenta sincronizar de novo depois.
        return

    # Nível 2: o servidor aceitou?
    if response_code == 401:
        push_error("Token inválido ou expirado. Refazer login.")
        return
    if response_code < 200 or response_code >= 300:
        push_error("Servidor respondeu erro HTTP %d" % response_code)
        return

    # Sucesso. Se for uma resposta de download, o corpo tem o save.
    var texto: String = body.get_string_from_utf8()
    var dados: Variant = JSON.parse_string(texto)
    if typeof(dados) != TYPE_DICTIONARY:
        push_error("Resposta do servidor não é um save válido.")
        return

    _aplicar_save_da_nuvem(dados)

A regra de ouro do tratamento de erro em cloud save: rede que falha nunca deve custar o progresso do jogador. O save local é a base. A nuvem é um bônus que sincroniza quando dá. Se o upload falhar, o jogo continua jogável offline e tenta de novo mais tarde, não solta um crash nem uma tela de erro no meio da partida. Um erro de sincronização mal tratado é um jeito rápido de perder jogador; o inverso, sincronizar sem atrito, é parte de reter jogadores no seu jogo.

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

Baixando o save da nuvem (GET)

O download é o mesmo HTTPRequest, agora com HTTPClient.METHOD_GET e sem corpo. O save volta no body da resposta, que você já sabe tratar pelo handler acima.

func baixar_save() -> void:
    var headers: PackedStringArray = [
        "Authorization: Bearer %s" % _token
    ]

    var erro: int = http.request(
        API_BASE + "/save",
        headers,
        HTTPClient.METHOD_GET,
        ""  # GET não leva corpo
    )

    if erro != OK:
        push_error("Falha ao iniciar download do save: %d" % erro)

Um detalhe prático: se o jogador nunca jogou naquele dispositivo, o servidor pode responder 404 (não há save ainda). Isso não é um erro de verdade, é o primeiro acesso. Trate o 404 como "começar do zero", não como falha.

A fonte da verdade: resolvendo o conflito sem perder progresso

Aqui mora o problema difícil de qualquer cloud save. Imagine: o jogador jogou no PC (save da nuvem avançou), depois abriu no notebook que estava offline há uma semana (save local antigo). Se o notebook subir o save dele por cima, o progresso do PC morre.

A solução não é técnica, é de decisão. Você precisa de um critério para escolher quem manda. Os dois campos que carimbei no upload (timestamp e versao) existem justamente para isso.

O fluxo seguro ao entrar no jogo:

func sincronizar_ao_entrar(local: Dictionary, nuvem: Dictionary) -> Dictionary:
    # 1. Se um dos lados não existe, o outro vence sem discussão.
    if nuvem.is_empty():
        return local
    if local.is_empty():
        return nuvem

    # 2. Estruturas de versão diferentes? Migre ANTES de comparar,
    #    senão você compara maçã com laranja.
    var v_local: int = int(local.get("versao", 0))
    var v_nuvem: int = int(nuvem.get("versao", 0))
    if v_local != v_nuvem:
        # migrar_save() é seu: adapta o save antigo pro formato novo.
        return migrar_save(local, nuvem)

    # 3. Mesmo formato: o timestamp mais recente é a fonte da verdade.
    var t_local: float = float(local.get("timestamp", 0.0))
    var t_nuvem: float = float(nuvem.get("timestamp", 0.0))

    if abs(t_local - t_nuvem) < 1.0:
        return nuvem  # praticamente iguais, tanto faz

    return t_nuvem > t_local if nuvem else local

Escolher pelo timestamp resolve a maioria dos casos, mas não todos. Quando a diferença é grande e os dois lados têm progresso real (o jogador avançou em ambos os dispositivos), o mais honesto é não escolher por ele. Mostre uma tela: "Encontramos dois saves. Qual você quer manter?", com data e um resumo de cada um. Perder cinco segundos do jogador escolhendo é infinitamente melhor que apagar horas de jogo dele em silêncio.

Detalhe que vale ouro: numere o timestamp a partir do relógio do servidor quando puder, não do dispositivo. Relógio de celular errado (fuso trocado, data manual) sabota qualquer comparação baseada em tempo do cliente.

Segurança: o cliente nunca é confiável

Cloud save mistura duas coisas que dão dor de cabeça juntas: rede e dados do jogador. Alguns princípios que não são opcionais:

Nunca confie 100% no cliente. O JSON que sobe passou pela máquina do jogador, e qualquer um consegue editá-lo antes do upload. Para um single-player casual, tudo bem: quem trapaceia só engana a si mesmo. Para jogo com ranking, economia compartilhada ou qualquer coisa competitiva, valide o estado no servidor. O save é uma afirmação do cliente, não um fato.

Proteja o token. A autenticação vai por HTTPS, sempre, e o token fica em memória durante a sessão, nunca gravado em texto puro no save nem embutido fixo no binário exportado (binário se abre e se lê). Se o token expira, o servidor responde 401 e o jogo refaz o login, como no handler lá em cima.

Trate o save como dado, não como código. Este é um motivo forte para usar JSON e não store_var binário ou Resource vindo da rede: um .tres pode embutir script, e carregar um arquivo desses baixado da internet executa código arbitrário na máquina do jogador. JSON é só dado, JSON.parse_string não executa nada.

Fechando

Save na nuvem na Godot 4 não vem pronto, e essa é a informação mais útil que dá para passar. Você escolhe entre três caminhos reais (Steam Cloud pela GodotSteam se lança na Steam, serviços de plataforma via plugin no mobile, ou backend próprio com HTTPRequest) e monta a sincronização com as peças que a engine oferece. No caminho do backend, o núcleo é pequeno: subir com POST, baixar com GET, tratar os dois níveis de erro (rede e HTTP) e, o mais importante, decidir a fonte da verdade por timestamp e versão para nunca sobrescrever progresso às cegas.

Próximo passo: comece pelo save local sólido, sem nuvem nenhuma. Só depois de gravar e carregar um JSON com timestamp e versao funcionando, plugue o upload contra uma API de teste sua. Suba, mate o save local, baixe de volta e confira se o progresso voltou inteiro. Quando esse ciclo estiver redondo, o resto é só ligar na sua conta de jogador.

Perguntas frequentes

A Godot 4 tem save na nuvem nativo?

Não. A Godot não traz nenhum "cloud save" pronto de fábrica. Ela te dá as ferramentas para salvar local (FileAccess, JSON) e para falar com a internet (HTTPRequest), mas a sincronização entre dispositivos é responsabilidade sua ou de um serviço externo. As opções reais são Steam Cloud via GodotSteam, serviços de plataforma (Google Play Games e iCloud, quase sempre via plugin de terceiros) ou um backend próprio que recebe e devolve o arquivo de save.

Qual é a forma mais simples de cloud save na Godot?

Se você já publica na Steam, é o Steam Cloud. O Steam consegue sincronizar a pasta de save automaticamente (Auto-Cloud, configurado no painel do Steamworks) ou pela API ISteamRemoteStorage, exposta na Godot pelo módulo GodotSteam. Você continua salvando local com FileAccess e o Steam cuida do resto. Fora da Steam, o caminho mais controlável é um backend próprio com HTTPRequest.

Como resolvo conflito entre o save local e o da nuvem?

Guarde um timestamp e um número de versão dentro do próprio save. Ao entrar no jogo, baixe o save da nuvem, compare o timestamp com o local e adote o mais recente como fonte da verdade. Se as versões de estrutura forem diferentes, migre antes de comparar. Quando não der para decidir com segurança, pergunte ao jogador em vez de sobrescrever cegamente, porque save sobrescrito é progresso perdido e review negativa.

Save na nuvem via HTTPRequest é seguro?

É tão seguro quanto o seu backend. O cliente nunca deve ser tratado como confiável: qualquer jogador consegue editar o JSON antes de subir. Para jogos single-player casuais, tudo bem confiar no cliente. Para qualquer coisa competitiva ou com ranking, valide o estado no servidor. Proteja a autenticação (envie o token por HTTPS, nunca deixe chaves fixas no binário) e trate o save como dado do usuário, não como verdade absoluta.

Preciso de um servidor caro para ter cloud save?

Não necessariamente. Para o padrão deste tutorial (subir e baixar um JSON por jogador), qualquer API REST simples resolve, inclusive um serviço BaaS (backend como serviço) com plano gratuito para começar. O custo cresce com o número de jogadores e o tamanho dos saves, não com a complexidade. A URL e o backend são responsabilidade sua; a Godot só faz as requisições.