Module 05 · Leçon 2 sur 6
Sauvegarder sans perdre de données
Sauvegarde l’étape, les pièces et l’inventaire de chaque joueur avec les DataStores, de façon fiable, même quand Roblox a un raté ou que le serveur ferme.
- ⏱️ 40 min
- 📝 Quiz de 7 questions
- 🔄 Vérifié le 24 octobre 2026
Sur cette page
- Les DataStores en deux minutes
- Activer l’accès dans Studio
- Les trois règles d’une sauvegarde fiable
- Règle 1 : tout appel réseau peut échouer
- Règle 2 : un échec n’est pas un nouveau joueur
- Règle 3 : peu de requêtes, au bon moment
- SetAsync ou UpdateAsync ?
- Le code complet de l’Obby des Nuages
- Le ModuleScript DonneesJoueurs
- Le Script GestionJoueurs
- Et si le chargement échoue ?
- Pourquoi BindToClose ?
- Et dans Studio ?
- Les limites à connaître
- Versions et métadonnées
- Le verrouillage de session
- Les bibliothèques reconnues
- Quiz de fin de leçon
🎯 Dans cette leçon, tu vas apprendre à…
- Charger et sauvegarder les données d’un joueur avec DataStoreService
- Gérer les erreurs avec pcall, des nouvelles tentatives et UpdateAsync
- Éviter les pertes de données au départ du joueur et à la fermeture du serveur
- Connaître les limites, le verrouillage de session et les bibliothèques reconnues
Rien ne fait fuir un joueur plus vite qu’une progression perdue. Il a passé une heure à grimper jusqu’à l’étape 30 de l’Obby des Nuages, il revient le lendemain… et il est revenu au tout début. Dans cette leçon, tu construis une sauvegarde fiable : elle charge les données à l’arrivée, les garde en mémoire, les sauvegarde régulièrement et ne les écrase jamais par erreur.
C’est la leçon la plus technique du module. Prends ton temps, et teste chaque étape dans Studio.
Les DataStores en deux minutes
Un DataStore est une base de données fournie par Roblox, partagée par tous les serveurs de ton jeu. On l’utilise via le service DataStoreService. Il fonctionne comme un grand dictionnaire :
- une clé (un texte) identifie une entrée, par exemple
"Joueur_123456"; - une valeur est associée à cette clé : un nombre, un texte, un booléen ou une table.
Les valeurs sont stockées sous forme de texte JSON. Garde donc des données simples : nombres, textes, booléens et tables. Un objet Roblox comme une Part ou une couleur Color3 ne se stocke pas tel quel : on enregistre plutôt son nom ou ses composantes.
Les méthodes principales :
| Méthode | Rôle |
|---|---|
GetAsync(cle) |
lit la valeur (renvoie nil si la clé n’existe pas) |
SetAsync(cle, valeur) |
écrit la valeur, en écrasant l’ancienne sans la lire |
UpdateAsync(cle, fonction) |
lit la valeur la plus récente, la passe à ta fonction, puis écrit ce qu’elle renvoie |
IncrementAsync(cle, n) |
ajoute n à un nombre entier stocké |
RemoveAsync(cle) |
supprime l’entrée |
Activer l’accès dans Studio
Par défaut, les tests dans Studio n’ont pas accès aux DataStores. Pour l’activer, ton jeu doit être publié, puis :
- ouvre File › Experience Settings ;
- va dans Security ;
- active Enable Studio Access to API Services ;
- clique sur Save.
Sans cette option, tu obtiens une erreur StudioAccessToApisNotAllowed. Avec le code de cette leçon, le chargement échoue alors à chaque test et tu es expulsé au bout de quelques secondes : c’est normal, active l’option (ou teste sur ton jeu publié).
La documentation met en garde : Studio lit et écrit les mêmes données que le jeu en ligne. Pour un jeu déjà lancé, active plutôt cette option sur une copie de test de ton jeu, pour ne pas abîmer les sauvegardes des vrais joueurs.
Les trois règles d’une sauvegarde fiable
Règle 1 : tout appel réseau peut échouer
Chaque appel à un DataStore passe par Internet. Il peut échouer : service momentanément indisponible, limite de requêtes atteinte… Quand ça arrive, la méthode déclenche (« lève ») une erreur, qui arrêterait ton script. On entoure donc chaque appel de pcall (« protected call »), qui attrape l’erreur au lieu de planter :
-- Script serveur : exemple minimal de lecture protégée
local DataStoreService = game:GetService("DataStoreService")
local magasin = DataStoreService:GetDataStore("DonneesJoueurs_v1")
local succes, resultat = pcall(function()
return magasin:GetAsync("Joueur_123456")
end)
if succes then
print("Données lues :", resultat) -- nil si la clé n'existe pas encore
else
warn("Échec de la lecture :", resultat) -- resultat contient le message d'erreur
end
En cas d’échec, on réessaie quelques fois en attendant un peu plus à chaque tentative. La page « Best practices for data stores » recommande un délai croissant (exponential backoff) avec un peu de hasard, pour que tous les serveurs ne réessaient pas au même instant, en plafonnant le délai et le nombre d’essais. Elle conseille aussi de ne pas réessayer une requête invalide (par exemple une valeur impossible à enregistrer) : notre version, volontairement simple, réessaie tout, mais trois fois au maximum.
Règle 2 : un échec n’est pas un nouveau joueur
C’est le piège qui détruit des sauvegardes. GetAsync renvoie nil pour un joueur qui n’a jamais joué : on lui donne alors des données de départ. Mais si GetAsync échoue, il ne faut surtout pas faire pareil ! Sinon, au départ du joueur, ces données vides seront sauvegardées par-dessus sa vraie progression.
Règle 3 : peu de requêtes, au bon moment
Chaque serveur dispose d’un budget de requêtes par minute. On ne sauvegarde donc pas à chaque pièce ramassée. Le schéma recommandé par la documentation :
- charger une fois à l’arrivée du joueur ;
- garder les données en mémoire sur le serveur pendant la partie ;
- sauvegarder au départ du joueur, à la fermeture du serveur, régulièrement (sauvegarde automatique) et aux moments critiques, comme un achat avec des Robux (module 7).
SetAsync ou UpdateAsync ?
SetAsync est rapide et ne compte que dans la limite d’écriture. Mais il écrase la valeur sans la lire : si deux serveurs écrivent la même clé en même temps, les données peuvent devenir incohérentes.
UpdateAsync lit la valeur la plus récente, te la donne, et écrit ce que ta fonction renvoie. Il est un peu plus lent et compte à la fois dans les limites de lecture et d’écriture, mais c’est la méthode que la documentation conseille dès que plusieurs serveurs peuvent écrire la même clé ou que la nouvelle valeur dépend de l’ancienne. Deux détails importants :
- si ta fonction renvoie
nil, l’écriture est annulée ; - ta fonction ne doit pas attendre (pas de
task.wait, pas d’appel réseau dedans). Elle peut être appelée plusieurs fois si un autre serveur a modifié la clé entre-temps.
Le code complet de l’Obby des Nuages
On découpe la sauvegarde en deux scripts, tous deux côté serveur :
- un ModuleScript
DonneesJoueursdans ServerScriptService : il charge, garde en mémoire et sauvegarde. D’autres scripts (comme la boutique, leçon 3) pourront l’utiliser avecrequire; - un Script
GestionJoueursdans ServerScriptService : il branche le module sur les événements du jeu. Il remplace le ScriptLeaderstatsde la leçon 1 (supprime ce dernier).
Vérifie aussi qu’aucun autre script ne crée de dossier leaderstats : depuis la leçon 1, le Script « Progression » du module 4 ne le fait plus. C’est important : un second dossier leaderstats (à 0) pourrait être celui que la sauvegarde lit, et elle enregistrerait alors 0 pièce.
Dans l’ordre, dans Studio :
- Dans l’Explorer, survole ServerScriptService, clique sur ⊕ et choisis ModuleScript. Renomme-le
DonneesJoueurs, efface le code de départ que Studio y a mis (local module = {}…return module), puis colle le code du module ci-dessous. - Toujours dans ServerScriptService, crée un Script nommé
GestionJoueurset colle son code (plus bas). - Supprime le Script
Leaderstatsde la leçon 1 (clic droit › Delete). - Active l’accès aux API (section précédente), puis lance un test : ton nom doit apparaître dans le tableau avec
EtapeetPieces(à 0 la première fois). Pour vérifier que la sauvegarde fonctionne vraiment, suis le Défi en fin de leçon.
Avec Claude Code, ces deux fichiers se demandent avec le prompt de l’encadré « Avec Claude Code », plus bas. Relis ensuite le code produit ligne par ligne avec les explications de cette section : c’est le code de ton jeu où une erreur coûte le plus cher.
Le code utilise des annotations de type, comme player: Player ou { [string]: any } (« une table dont les clés sont des textes et les valeurs n’importe quoi »). Elles sont facultatives en Luau : elles ne changent rien à l’exécution, mais aident Studio à repérer tes fautes de frappe.
Le ModuleScript DonneesJoueurs
-- ModuleScript « DonneesJoueurs » dans ServerScriptService (côté serveur)
-- Charge, garde en mémoire et sauvegarde les données de chaque joueur.
local DataStoreService = game:GetService("DataStoreService")
local magasin = DataStoreService:GetDataStore("DonneesJoueurs_v1")
local VERSION_DONNEES = 1 -- à augmenter quand la structure des données change
local MAX_TENTATIVES = 3
local DELAI_MAX_FERMETURE = 25 -- secondes : Roblox n'attend que 30 s à la fermeture
local DonneesJoueurs = {}
-- Données en mémoire pendant la partie : [Player] = table de données
local sessions: { [Player]: { [string]: any } } = {}
-- Nombre de sauvegardes en cours, toutes origines confondues
-- (départ du joueur, sauvegarde automatique, fermeture du serveur)
local sauvegardesEnCours = 0
-- Structure d'une sauvegarde toute neuve
local function donneesParDefaut(): { [string]: any }
return {
version = VERSION_DONNEES,
etape = 0, -- dernier checkpoint validé (0 = aucun)
pieces = 0,
inventaire = {}, -- objets possédés (leçon 3)
equipe = "", -- objet équipé (leçon 3), "" = aucun
}
end
-- Complète une ancienne sauvegarde avec les champs ajoutés depuis
local function completer(donnees: { [string]: any }): { [string]: any }
for cle, valeurParDefaut in donneesParDefaut() do
if donnees[cle] == nil then
donnees[cle] = valeurParDefaut
end
end
-- La sauvegarde a maintenant la structure actuelle : on met à jour son
-- numéro de version (sans jamais baisser celui d'une sauvegarde plus récente)
if donnees.version < VERSION_DONNEES then
donnees.version = VERSION_DONNEES
end
return donnees
end
-- Exécute une requête réseau, avec plusieurs tentatives en cas d'échec
local function avecTentatives(requete: () -> any): (boolean, any)
local succes: boolean, resultat: any = false, nil
for tentative = 1, MAX_TENTATIVES do
succes, resultat = pcall(requete)
if succes then
return true, resultat
end
warn(`DataStore : tentative {tentative}/{MAX_TENTATIVES} échouée ({resultat})`)
if tentative < MAX_TENTATIVES then
-- Attente croissante (environ 2 s, puis 4 s) + un peu de hasard
task.wait(2 ^ tentative + math.random())
end
end
return false, resultat
end
-- Copie les leaderstats (la référence pendant la partie) dans la session
local function lireLeaderstats(player: Player, donnees: { [string]: any })
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats == nil then
return
end
local etape = leaderstats:FindFirstChild("Etape")
local pieces = leaderstats:FindFirstChild("Pieces")
if etape and etape:IsA("IntValue") then
donnees.etape = etape.Value
end
if pieces and pieces:IsA("IntValue") then
donnees.pieces = pieces.Value
end
end
-- Charge les données ; renvoie nil en cas d'échec
function DonneesJoueurs.Charger(player: Player): { [string]: any }?
local cle = "Joueur_" .. player.UserId
local succes, resultat = avecTentatives(function()
return magasin:GetAsync(cle)
end)
if not succes then
-- Échec : on ne crée PAS de session (règle 2)
return nil
end
local donnees
if resultat == nil then
donnees = donneesParDefaut() -- premier passage : nouveau joueur
else
donnees = completer(resultat)
end
sessions[player] = donnees
return donnees
end
-- Renvoie les données en mémoire d'un joueur (nil si pas chargées)
function DonneesJoueurs.Obtenir(player: Player): { [string]: any }?
return sessions[player]
end
-- Sauvegarde les données d'un joueur ; renvoie true SEULEMENT si elles ont
-- réellement été écrites dans le DataStore (false sinon)
function DonneesJoueurs.Sauvegarder(player: Player): boolean
local donnees = sessions[player]
if donnees == nil then
return false -- jamais chargées correctement : surtout, ne rien écrire
end
lireLeaderstats(player, donnees)
sauvegardesEnCours += 1
local cle = "Joueur_" .. player.UserId
local ecritureAnnulee = false
local succes, erreur = avecTentatives(function()
return magasin:UpdateAsync(cle, function(ancienne)
-- Si la sauvegarde vient d'une version plus récente du jeu,
-- on n'y touche pas : renvoyer nil annule l'écriture
if ancienne ~= nil and (ancienne.version or 0) > VERSION_DONNEES then
ecritureAnnulee = true
return nil
end
-- (cette fonction peut être rappelée : on remet l'indicateur à jour)
ecritureAnnulee = false
-- Nouvelle valeur + UserId du joueur associé à la clé
return donnees, { player.UserId }
end)
end)
sauvegardesEnCours -= 1
if not succes then
warn(`Sauvegarde impossible pour {player.Name} : {erreur}`)
return false
end
if ecritureAnnulee then
warn(`Sauvegarde annulée pour {player.Name} : données d'une version plus récente du jeu`)
return false -- rien n'a été écrit
end
return true
end
-- Oublie les données en mémoire (à appeler quand le joueur est parti)
function DonneesJoueurs.Liberer(player: Player)
sessions[player] = nil
end
-- Fermeture du serveur : sauvegarde EN PARALLÈLE toutes les sessions encore
-- en mémoire, puis attend la fin de TOUTES les sauvegardes en cours,
-- y compris celles déjà lancées par PlayerRemoving
function DonneesJoueurs.SauvegarderTout()
for player in sessions do
task.spawn(DonneesJoueurs.Sauvegarder, player)
end
local debut = os.clock()
while sauvegardesEnCours > 0 and os.clock() - debut < DELAI_MAX_FERMETURE do
task.wait()
end
end
return DonneesJoueurs
Les fonctions que les autres scripts peuvent appeler (on les réutilisera jusqu’au module 11) :
| Fonction | Rôle | Renvoie |
|---|---|---|
DonneesJoueurs.Charger(player) |
lit la sauvegarde et crée la session | la table de données, ou nil en cas d’échec |
DonneesJoueurs.Obtenir(player) |
donne accès à la table en mémoire (la même table, pas une copie) | la table, ou nil si pas chargée |
DonneesJoueurs.Sauvegarder(player) |
écrit la session dans le DataStore | true seulement si l’écriture a eu lieu |
DonneesJoueurs.Liberer(player) |
oublie la session d’un joueur parti | rien |
DonneesJoueurs.SauvegarderTout() |
fermeture du serveur : sauvegarde tout et attend | rien |
Quelques explications :
- La clé suit la convention du projet :
"Joueur_" .. player.UserId. On utilise toujours leUserId, qui ne change jamais, et pas le nom du joueur, qui peut changer. La documentation recommande justement des clés stables commeUser_123456. - Une seule clé par joueur, qui contient toute sa table : moins de requêtes, et tout est enregistré d’un bloc.
- La valeur renvoyée par
Sauvegarderesttrueseulement si les données ont vraiment été écrites. Elle vautfalsesi le chargement avait échoué, si toutes les tentatives ont échoué, ou si l’écriture a été annulée (sauvegarde d’une version plus récente du jeu). Le module 7 s’en servira pour ne confirmer un achat en Robux qu’une fois la sauvegarde faite. - Le champ
versionprépare l’avenir. Quand tu ajouteras un champ (par exemplemeilleurTemps), augmenteVERSION_DONNEES:completerremplira le nouveau champ dans les anciennes sauvegardes et passera leurversionau nouveau numéro. À la sauvegarde suivante, la clé porte donc le numéro le plus récent, et un serveur qui tourne encore avec l’ancienne version du jeu refusera de l’écraser (grâce au test surancienne.versiondansSauvegarder). - Le deuxième élément renvoyé par la fonction d’
UpdateAsync,{ player.UserId }, associe la clé au joueur. La référence API le recommande fortement (paramètreuserIds) pour faciliter le suivi et la suppression des données personnelles (RGPD). On le renvoie à chaque sauvegarde : d’après la référence d’UpdateAsync, des UserIds non renvoyés sont effacés. - Le compteur
sauvegardesEnCourscompte toutes les sauvegardes en cours, quelle que soit leur origine.SauvegarderTouts’en sert à la fermeture du serveur (voir plus bas). task.spawn(fonction, argument)lance la fonction tout de suite, dans un fil d’exécution à part : le script continue sans attendre qu’elle finisse. C’est ce qui permet de lancer plusieurs sauvegardes en même temps.
Le Script GestionJoueurs
-- Script « GestionJoueurs » dans ServerScriptService (côté serveur)
-- Remplace le Script « Leaderstats » de la leçon 1 (dont il reprend rendrePrincipale).
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ServerScriptService = game:GetService("ServerScriptService")
local DonneesJoueurs = require(ServerScriptService:WaitForChild("DonneesJoueurs"))
local INTERVALLE_SAUVEGARDE = 180 -- secondes, comme dans l'exemple officiel de Roblox
-- false = comme l'exemple officiel : pas de sauvegarde à la fermeture d'un test Studio.
-- Passe-le à true, sur ta copie de test uniquement, pour tester la sauvegarde dans Studio.
local SAUVEGARDER_EN_STUDIO = false
-- Fait d'une statistique la colonne principale du tableau (leçon 1)
local function rendrePrincipale(stat: IntValue)
local estPrincipale = Instance.new("BoolValue")
estPrincipale.Name = "IsPrimary"
estPrincipale.Value = true
estPrincipale.Parent = stat
end
-- Crée les leaderstats à partir des données chargées
-- (même ordre qu'à la leçon 1 : Etape en colonne principale, puis Pieces)
local function creerLeaderstats(player: Player, donnees: { [string]: any })
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
local etape = Instance.new("IntValue")
etape.Name = "Etape"
etape.Value = donnees.etape
rendrePrincipale(etape)
etape.Parent = leaderstats
local pieces = Instance.new("IntValue")
pieces.Name = "Pieces"
pieces.Value = donnees.pieces
pieces.Parent = leaderstats
leaderstats.Parent = player
end
local function surArrivee(player: Player)
local donnees = DonneesJoueurs.Charger(player)
if donnees == nil then
player:Kick("Tes données n'ont pas pu être chargées. Réessaie dans quelques minutes !")
return
end
-- Le joueur a pu partir pendant le chargement (qui prend parfois quelques secondes)
if player.Parent ~= Players then
DonneesJoueurs.Liberer(player)
return
end
creerLeaderstats(player, donnees)
end
local function surDepart(player: Player)
DonneesJoueurs.Sauvegarder(player) -- ne fait rien si le chargement avait échoué
DonneesJoueurs.Liberer(player)
end
Players.PlayerAdded:Connect(surArrivee)
Players.PlayerRemoving:Connect(surDepart)
for _, player in Players:GetPlayers() do
task.spawn(surArrivee, player)
end
-- Fermeture du serveur : on sauvegarde tout le monde EN PARALLÈLE et on attend
game:BindToClose(function()
if RunService:IsStudio() and not SAUVEGARDER_EN_STUDIO then
return -- recommandation de la référence API pour les tests dans Studio
end
DonneesJoueurs.SauvegarderTout()
end)
-- Sauvegarde automatique : un filet de sécurité si la sauvegarde finale échoue
task.spawn(function()
while true do
-- Un peu de hasard pour que tous les serveurs ne sauvegardent pas en même temps
task.wait(INTERVALLE_SAUVEGARDE + math.random(0, 30))
for _, player in Players:GetPlayers() do
task.spawn(DonneesJoueurs.Sauvegarder, player)
end
end
end)
Et si le chargement échoue ?
Suivons ce qui se passe, ligne par ligne, quand GetAsync échoue trois fois de suite :
Chargerrenvoienilsans remplirsessions[player];surArriveeexpulse le joueur avec un message (player:Kick) ; il n’a jamais reçu de leaderstats ;- l’expulsion déclenche
PlayerRemoving, doncsurDepart:Sauvegarderne trouve aucune session et renvoiefalsesans rien écrire. La sauvegarde automatique etSauvegarderToutfont de même.
Sa vraie progression reste donc intacte dans le DataStore. Il la retrouvera en revenant, quand le service répondra de nouveau.
Pourquoi BindToClose ?
Quand un serveur s’arrête (mise à jour du jeu, dernier joueur parti, redémarrage), les sauvegardes lancées par PlayerRemoving pourraient ne pas avoir le temps de finir. game:BindToClose enregistre une fonction appelée avant l’arrêt. D’après la référence API, le serveur attend 30 secondes que les fonctions liées se terminent, puis s’arrête quoi qu’il arrive. D’où trois précautions dans SauvegarderTout :
- elle sauvegarde en parallèle avec
task.spawn: 20 sauvegardes d’une seconde chacune prennent environ une seconde, et non vingt ; - elle parcourt les sessions en mémoire, et pas
Players:GetPlayers(): à la fermeture, les joueurs sont déconnectés et peuvent avoir déjà quitté la liste des joueurs alors que leur sauvegarde de départ n’est pas finie ; - elle attend que le compteur
sauvegardesEnCoursrevienne à 0, donc aussi la fin des sauvegardes déjà lancées parPlayerRemoving, avec une limite de 25 secondes pour rester sous les 30 secondes.
Un même joueur peut ainsi être sauvegardé deux fois de suite (départ + fermeture) : c’est sans danger, car les deux écritures contiennent la même table. C’est pareil si un joueur part pendant une sauvegarde automatique : deux UpdateAsync sont en cours pour lui en même temps, mais tous deux écrivent la même table en mémoire, qui contient ses données les plus récentes. Quel que soit celui qui termine en dernier, rien n’est perdu. Et comme Liberer ne fait qu’oublier la table (sans la vider), une sauvegarde encore en cours garde accès à ses données.
Et dans Studio ?
La référence de DataModel:BindToClose conseille de vérifier RunService:IsStudio(), et son exemple officiel de sauvegarde ne sauvegarde pas à la fermeture d’un test Studio, pour deux raisons données en commentaire : ne pas écrire des données de test dans les données réelles du jeu, et ne pas bloquer l’arrêt du test. On suit cette recommandation : avec SAUVEGARDER_EN_STUDIO = false, la fonction liée s’arrête tout de suite dans Studio.
Conséquence : dans Studio, seule la sauvegarde de PlayerRemoving est tentée quand tu cliques sur Stop, et rien ne garantit qu’elle ait le temps de se terminer avant la fin du test. Attention : si l’accès aux API est activé, cette sauvegarde-là écrit bel et bien dans les données du jeu. C’est une raison de plus pour ne tester que sur une copie de test.
Pour tester ta sauvegarde (sur ta copie de test), passe temporairement SAUVEGARDER_EN_STUDIO à true : l’arrêt du test peut alors prendre quelques secondes, le temps que les sauvegardes se terminent. Remets-le à false ensuite. Sur les serveurs en ligne, IsStudio() renvoie false : la sauvegarde à la fermeture a toujours lieu.
Les limites à connaître
Les chiffres suivants viennent de la page officielle « Data store error codes and limits », au moment de la rédaction (octobre 2026). Ils évoluent : consulte la page avant de concevoir un système gourmand.
Requêtes par serveur (limites par défaut, par minute) :
| Type | Méthodes | Limite par minute |
|---|---|---|
| Lecture | GetAsync, UpdateAsync… |
60 + nombre de joueurs × 40 |
| Écriture | SetAsync, IncrementAsync, UpdateAsync |
60 + nombre de joueurs × 40 |
| Liste | ListKeysAsync… |
5 + nombre de joueurs × 2 |
| Suppression | RemoveAsync |
60 + nombre de joueurs × 40 |
Il existe aussi une limite à l’échelle du jeu (tous serveurs confondus), qui dépend du nombre de joueurs connectés, ainsi qu’un débit maximal par clé : 25 Mo de lecture et 4 Mo d’écriture par minute.
Taille des données : le nom d’un DataStore et le nom d’une clé sont limités à 50 caractères ; la valeur d’une clé à 4 194 304 caractères une fois convertie en JSON (environ 4 Mo). Une table d’obby n’en approche même pas.
Stockage total : 500 Mo + 1 Mo × nombre de joueurs uniques ayant déjà rejoint le jeu. Seule la dernière version de chaque clé compte.
Avec notre code, un joueur coûte une lecture à l’arrivée (GetAsync), puis un UpdateAsync toutes les trois minutes environ et un au départ. Chaque UpdateAsync compte à la fois comme une lecture et comme une écriture. On reste très loin des limites.
Versions et métadonnées
Chaque écriture dans un DataStore crée une nouvelle version de la clé. Les anciennes versions sont conservées 30 jours après avoir été remplacées (la dernière version n’expire jamais). Tu peux les lister avec ListVersionsAsync et en relire une avec GetVersionAsync. C’est ton filet de sécurité si un bug corrompt des sauvegardes : tu peux revenir en arrière.
Chaque clé peut aussi porter des métadonnées :
- des métadonnées fournies par Roblox : numéro de version, date de création et de dernière mise à jour (objet
DataStoreKeyInfo, renvoyé en second parGetAsync) ; - des UserIds associés à la clé (comme notre
{ player.UserId }) ; - des métadonnées personnalisées, via
DataStoreSetOptions:SetMetadata, limitées à 300 caractères au total.
Le verrouillage de session
Notre code a une faiblesse connue. Imagine : un joueur quitte le serveur A, dont la sauvegarde est lente (nouvelles tentatives, file d’attente). Pendant ce temps, il rejoint le serveur B, qui charge… l’ancienne sauvegarde. Le serveur A finit sa sauvegarde, puis, plus tard, le serveur B sauvegarde ses données périmées par-dessus. Résultat : des pertes, voire des objets dupliqués si ton jeu permet des échanges. La documentation de Roblox décrit précisément ce scénario.
Le verrouillage de session (session locking) règle le problème : quand un serveur charge les données d’un joueur, il pose un « verrou » dans les métadonnées de la clé, dans le même UpdateAsync que la lecture. Tant que ce verrou est actif, les autres serveurs refusent de lire ou d’écrire cette clé. Le serveur retire le verrou lors de la sauvegarde finale, et le verrou expire s’il n’est plus rafraîchi (par exemple si le serveur plante).
Pour l’Obby des Nuages version 1 (des pièces et des traînées cosmétiques, sans échanges entre joueurs), le risque reste limité et notre code suffit pour apprendre. Pour un jeu avec des échanges, des objets rares ou beaucoup d’achats, utilise une solution qui gère le verrouillage.
Les bibliothèques reconnues
La documentation de Roblox indique que les modules de DataStore tiers sont « toujours une option », souvent préférable à un système écrit de zéro, à condition de vérifier qui les maintient et comment récupérer tes données sans eux. Deux pistes :
- ProfileStore, de loleris (MadStudioRoblox), successeur de ProfileService. Ce module communautaire (il n’est pas développé par Roblox) gère pour toi le verrouillage de session et la sauvegarde automatique (toutes les 300 secondes par défaut, d’après son auteur). Son auteur précise qu’il est conçu pour les données des joueurs, pas pour les classements ni les données globales. Liens de l’auteur : documentation de ProfileStore et fil de présentation sur le DevForum (octobre 2024).
- Le code de référence de Roblox (« Player data and purchasing systems ») : il montre une implémentation complète avec nouvelles tentatives ordonnées et verrouillage de session. Roblox prévient qu’il sert d’exemple à étudier, pas à copier tel quel sans tests approfondis.
Ce qu’il faut retenir
DataStoreServicesauvegarde des données côté serveur ; on active l’accès dans Studio via Experience Settings › Security, de préférence sur une copie de test.- Chaque appel est protégé par
pcall, avec quelques nouvelles tentatives ; un échec de chargement ne doit jamais être traité comme un nouveau joueur. - On charge une fois, on garde en mémoire, et on sauvegarde au départ, à la fermeture (
BindToClose, 30 secondes maximum, en attendant toutes les sauvegardes en cours) et régulièrement ; dans Studio, on suit l’exemple officiel qui saute la sauvegarde de fermeture. UpdateAsyncest préférable àSetAsyncquand plusieurs serveurs peuvent écrire la même clé ; un champversionpermet de faire évoluer la structure.- Le verrouillage de session évite les écrasements entre serveurs ; ProfileStore le gère pour toi si ton jeu en a besoin.
Sources et pour aller plus loin
- Data stores — guide Roblox (accès dans Studio, DataStores partagés par les places d’un jeu)
- Create and publish games and places — Publish As (Roblox)
- Data store error codes and limits — limites officielles
- Best practices for data stores — Roblox
- Versioning, listing, and caching — Roblox
- Implement player data and purchasing systems (verrouillage de session) — Roblox
- DataModel:BindToClose — référence API
- GlobalDataStore:UpdateAsync — référence API
- ProfileStore — documentation de l’auteur (module communautaire)
- ProfileStore — fil de présentation de l’auteur sur le DevForum (module communautaire)