Pular para o conteúdo
PTEntrar

Como salvar os dados do jogador no Roblox?

Guarde tudo de um jogador numa tabela só, carregue na entrada, grave com UpdateAsync quando ele sair e grave de novo no BindToClose para um desligamento não comer a sessão. Coloque um número de versão antes de publicar: dado que você não consegue migrar é dado que você nunca mais muda.

Em resumo

  • Uma chave por jogador, uma tabela. Não um DataStore por estatística.
  • UpdateAsync, não SetAsync: dois servidores podem segurar o mesmo jogador.
  • BindToClose, ou todo desligamento perde o que não foi salvo.

Save perdido é o único bug que o jogador nunca perdoa. É também o bug que quase sempre são os mesmos três erros: uma estatística salva na chave dela, um SetAsync correndo contra outro servidor, e nenhum BindToClose. Resolva esses três e o resto é detalhe.

Passo a passo

1. Uma tabela, com versão

A versão são quatro caracteres que não custam nada hoje e salvam o jogo depois. Sem ela você não consegue renomear um campo sem adivinhar como são as linhas antigas.

local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData_v1")

local VERSAO = 1

local function vazio()
	return {
		version = VERSAO,
		coins = 0,
		level = 1,
		pets = {},
		settings = { music = true },
	}
end

2. Carregue na entrada e nunca confie no formato

Uma linha escrita pela build do mês passado é uma linha com os campos do mês passado. Preencha os buracos em vez de indexar o nada.

local Players = game:GetService("Players")
local cache: { [number]: any } = {}

local function migrar(dados)
	if type(dados) ~= "table" then return vazio() end
	local novo = vazio()
	for k, v in novo do
		if dados[k] == nil then
			dados[k] = v
		end
	end
	dados.version = VERSAO
	return dados
end

Players.PlayerAdded:Connect(function(player: Player)
	local ok, dados = pcall(function()
		return store:GetAsync("u_" .. player.UserId)
	end)
	-- Uma LEITURA que falha não pode entregar um save vazio ao jogador: é assim
	-- que uma conta de nível 40 vira nível 1. Expulse, dizendo o motivo.
	if not ok then
		player:Kick("Não deu para carregar o seu save. Entre de novo em um minuto.")
		return
	end
	cache[player.UserId] = migrar(dados)
end)

3. Grave com UpdateAsync, e só quando houver o que gravar

UpdateAsync lê e escreve numa operação, então o segundo servidor a tocar a chave vê o resultado do primeiro. SetAsync não vê, e é essa a diferença inteira.

local function salvar(userId: number)
	local dados = cache[userId]
	if not dados then return end
	local ok, err = pcall(function()
		store:UpdateAsync("u_" .. userId, function(antigo)
			-- Sessão mais velha, linha mais nova: fique com a linha. Acontece em
			-- teleporte.
			if type(antigo) == "table" and (antigo.savedAt or 0) > (dados.savedAt or 0) then
				return antigo
			end
			dados.savedAt = os.time()
			return dados
		end)
	end)
	if not ok then
		warn(`[dados] falhou salvar {userId}: {err}`)
	end
end

Players.PlayerRemoving:Connect(function(player: Player)
	salvar(player.UserId)
	cache[player.UserId] = nil
end)

4. Pegue o desligamento

O Roblox dá alguns segundos a um servidor que está fechando. Use esses segundos, ou todo deploy joga fora o que aconteceu desde a última gravação.

game:BindToClose(function()
	for _, player in Players:GetPlayers() do
		-- task.spawn para uma chave lenta não consumir a janela inteira.
		task.spawn(salvar, player.UserId)
	end
	-- Dê um instante para as gravações chegarem. No Studio isto volta na hora.
	task.wait(3)
end)

Qual chamada usar

ChamadaSegura com dois servidoresUse para
UpdateAsyncsimTodo save de dado de jogador. O padrão
SetAsyncnãoSó dado que mais nada escreve — uma ferramenta de admin avulsa
GetAsync—Carregar, uma vez por entrada, e nunca dentro de um laço
IncrementAsyncsimUm contador puro, quando você nunca precisa ler o resto
GetOrderedDataStoresimSó rankings. Ele guarda números, não tabelas

Perguntas

Preciso salvar num cronômetro também?

Sim, a cada dois ou três minutos por jogador, pela mesma função salvar. PlayerRemoving e BindToClose cobrem a saída limpa e o desligamento; uma queda só é coberta pelo cronômetro. Mantenha o intervalo acima de 60 segundos: o orçamento é de cerca de 60 gravações por minuto para o servidor inteiro.

Por que o dado de um jogador voltou vazio uma vez?

Quase sempre um GetAsync que falhou e foi tratado como "jogador novo". O pcall acima separa os dois casos: erro é erro, e o jogador é expulso com um motivo em vez de ganhar uma conta zerada. nil sem erro é jogador novo de verdade.

Preciso de ProfileService ou DataStore2?

Não, mas eles existem porque o código acima é fácil de errar por pouco — especialmente o travamento de sessão, quando o jogador teleporta entre places. Se o seu jogo tem places, use uma biblioteca; se é um place só, o padrão acima basta.

Posso guardar o dado numa pasta de IntValues?

Como espelho ao vivo, sim, e deixa a interface trivial. Como fonte da verdade, não: a pasta morre com o servidor. Escreva da tabela para a pasta na entrada e da pasta para a tabela na mudança, e deixe a tabela como a coisa que é salva.

Atualizado: Testado no Studio: Roblox Studio, outubro de 2026Autor: GustavoLicença: O código deste guia pode ser usado em qualquer experiência do Roblox, inclusive paga

Quero ser avisado

Em breve

Deixe seu e-mail e a gente te avisa na hora em que este pack sair.