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
| Chamada | Segura com dois servidores | Use para |
|---|---|---|
UpdateAsync | sim | Todo save de dado de jogador. O padrão |
SetAsync | não | Só dado que mais nada escreve — uma ferramenta de admin avulsa |
GetAsync | — | Carregar, uma vez por entrada, e nunca dentro de um laço |
IncrementAsync | sim | Um contador puro, quando você nunca precisa ler o resto |
GetOrderedDataStore | sim | Só 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