Contexte
Roblox fournit un ensemble d'APIs pour interagir avec les magasins de données via DataStoreService. Le cas d'utilisation le plus courant pour ces APIs est de sauvegarder, charger et répliquer les données des joueurs. C'est-à-dire, des données associées aux progrès du joueur, aux achats et à d'autres caractéristiques de session qui persistent entre les sessions de jeu individuelles.
La plupart des jeux sur Roblox utilisent ces APIs pour implémenter une forme de système de données des joueurs. Ces implémentations diffèrent dans leur approche, mais cherchent généralement à résoudre le même ensemble de problèmes.
Problèmes courants
Voici quelques-uns des problèmes les plus courants que les systèmes de données des joueurs tentent de résoudre :
Accès en mémoire : Les requêtes DataStoreService effectuent des requêtes web qui fonctionnent de manière asynchrone et sont soumises à des limites de taux. Cela est approprié pour un chargement initial au début de la session, mais pas pour des opérations de lecture et d'écriture à haute fréquence pendant le cours normal du jeu. La plupart des systèmes de données des joueurs des développeurs stockent ces données en mémoire sur le serveur Roblox, limitant les requêtes DataStoreService aux scénarios suivants :
- Lecture initiale au début d'une session
- Écriture finale à la fin de la session
- Écritures périodiques à un intervalle pour atténuer le scénario où l'écriture finale échoue
- Écritures pour s'assurer que les données sont sauvegardées lors du traitement d'un achat
Stockage efficace : Stocker toutes les données de session d'un joueur dans une seule table vous permet de mettre à jour plusieurs valeurs de manière atomique et de gérer la même quantité de données avec moins de requêtes. Cela élimine également le risque de désynchronisation entre les valeurs et facilite les retours en arrière.
Certains développeurs mettent également en œuvre une sérialisation personnalisée pour compresser de grandes structures de données (généralement pour sauvegarder du contenu généré par les utilisateurs dans le jeu).
Réplication : Le client a besoin d'un accès régulier aux données d'un joueur (par exemple, pour mettre à jour l'interface utilisateur). Une approche générique pour répliquer les données des joueurs au client vous permet de transmettre ces informations sans avoir à créer des systèmes de réplication sur mesure pour chaque composant de données. Les développeurs souhaitent souvent avoir la possibilité d'être sélectifs quant à ce qui est et n'est pas répliqué au client.
Gestion des erreurs : Lorsque les DataStores ne peuvent pas être accessibles, la plupart des solutions mettront en œuvre un mécanisme de réessai et un retour aux données 'par défaut'. Une attention particulière est nécessaire pour s'assurer que les données de secours ne remplacent pas plus tard les données 'réelles', et que cela est communiqué au joueur de manière appropriée.
Réessais : Lorsque les magasins de données sont inaccessibles, la plupart des solutions mettent en œuvre un mécanisme de réessai et un retour aux données par défaut. Prenez soin de vous assurer que les données de secours ne remplacent pas plus tard les données "réelles", et communiquez la situation au joueur de manière appropriée.
Verrouillage de session : Si les données d'un seul joueur sont chargées et en mémoire sur plusieurs serveurs, des problèmes peuvent survenir où un serveur sauvegarde des informations obsolètes. Cela peut entraîner une perte de données et des failles de duplication d'objets courantes.
Gestion atomique des achats : Vérifiez, attribuez et enregistrez les achats de manière atomique pour éviter que des objets ne soient perdus ou attribués plusieurs fois.
Code d'exemple
Roblox a du code de référence pour vous aider à concevoir et à construire des systèmes de données des joueurs. Le reste de cette page examine le contexte, les détails d'implémentation et les mises en garde générales.
Après avoir importé le modèle dans Studio, vous devriez voir la structure de dossier suivante :

Architecture
Ce diagramme de haut niveau illustre les systèmes clés dans l'exemple et comment ils interagissent avec le code dans le reste du jeu.

Réessais
Classe : DataStoreWrapper
Contexte
Comme DataStoreService effectue des requêtes web en arrière-plan, ses requêtes ne sont pas garanties de réussir. Lorsque cela se produit, les méthodes DataStore génèrent des erreurs, vous permettant de les gérer.
Un "piège" courant peut se produire si vous essayez de gérer les échecs de magasin de données de cette manière :
local MAX_ATTEMPTS = 5
local BASE_DELAY = 2
local MAX_DELAY = 32
local function retrySetAsync(dataStore, key, value)
for attempt = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
return result
end
if attempt < MAX_ATTEMPTS then
local backoff = math.min(MAX_DELAY, BASE_DELAY * (2 ^ (attempt - 1)))
local jitter = math.random() * backoff
task.wait(math.min(MAX_DELAY, backoff + jitter))
end
end
endRéessayez les échecs transitoires avec un backoff exponentiel et un jitter aléatoire afin que les serveurs ne réessaient pas simultanément. Limitez le délai et le nombre de tentatives.
Même avec ce schéma de délai, ce mécanisme de réessai n'est pas adapté aux requêtes DataStoreService car il ne garantit pas l'ordre dans lequel les requêtes sont effectuées. Préserver l'ordre des requêtes est important pour les requêtes DataStoreService car elles interagissent avec l'état. Considérez le scénario suivant :
- La requête A est faite pour définir la valeur de la clé K à 1.
- La requête échoue, donc un réessai est programmé pour s'exécuter après le délai de backoff.
- Avant que le réessai ne se produise, la requête B définit la valeur de K à 2, mais le réessai de la requête A écrase immédiatement cette valeur et définit K à 1.
Même si UpdateAsync fonctionne sur la dernière version de la valeur de la clé, les requêtes UpdateAsync doivent toujours être traitées dans l'ordre pour éviter des états transitoires invalides (par exemple, un achat soustrait des pièces avant qu'une addition de pièces ne soit traitée, entraînant des pièces négatives).
Notre système de données des joueurs utilise une nouvelle classe, DataStoreWrapper, qui fournit des réessais avec des délais garantis d'être traités dans l'ordre par clé.
Approche

DataStoreWrapper fournit des méthodes correspondant aux méthodes DataStore : DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() et DataStore:RemoveAsync().
Ces méthodes, lorsqu'elles sont appelées :
Ajoutent la requête à une file d'attente. Chaque clé a sa propre file d'attente, où les requêtes sont traitées dans l'ordre et en série. Le thread demandeur attend jusqu'à ce que la requête soit terminée.
Cette fonctionnalité est basée sur la classe ThreadQueue, qui est un planificateur de tâches basé sur des coroutines et un limiteur de taux. Plutôt que de retourner une promesse, ThreadQueue suspend le thread actuel jusqu'à ce que l'opération soit terminée et génère une erreur si elle échoue. Cela est plus cohérent avec les modèles asynchrones idiomatiques de Luau.
Si une requête échoue, elle est réessayée avec un backoff exponentiel configurable. Ces réessais font partie du rappel soumis à la ThreadQueue, donc ils sont garantis de se terminer avant que la prochaine requête dans la file d'attente pour cette clé ne commence.
Lorsque la requête est terminée, la méthode de requête retourne avec le modèle success, result.
DataStoreWrapper expose également des méthodes pour obtenir la longueur de la file d'attente pour une clé donnée et pour effacer les requêtes obsolètes. Cette dernière option est particulièrement utile dans les scénarios où le serveur est en train de s'arrêter et qu'il n'y a pas de temps pour traiter d'autres requêtes que les plus récentes.
Mises en garde
DataStoreWrapper suit le principe que, en dehors de scénarios extrêmes, chaque requête de magasin de données doit être autorisée à se terminer (avec succès ou non), même si une requête plus récente la rend redondante. Lorsqu'une nouvelle requête se produit, les requêtes obsolètes ne sont pas supprimées de la file d'attente, mais sont plutôt autorisées à se terminer avant que la nouvelle requête ne commence. La raison de cela est ancrée dans l'applicabilité de ce module en tant qu'utilitaire de magasin de données générique plutôt qu'un outil spécifique pour les données des joueurs, et est la suivante :
Il est difficile de décider d'un ensemble de règles intuitives pour quand une requête peut être en toute sécurité retirée de la file d'attente. Considérez la file d'attente suivante :
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Le comportement attendu est que GetAsync() renverrait 1, mais si nous retirons la requête SetAsync() de la file d'attente en raison de son caractère redondant par rapport à la plus récente, elle renverrait 0.
La progression logique est que lorsqu'une nouvelle requête d'écriture est ajoutée, il ne faut élaguer les requêtes obsolètes que jusqu'à la requête de lecture la plus récente. UpdateAsync(), de loin l'opération la plus courante (et la seule utilisée par ce système), peut à la fois lire et écrire, donc il serait difficile de concilier cela dans cette conception sans ajouter une complexité supplémentaire.
DataStoreWrapper pourrait vous obliger à spécifier si une requête UpdateAsync() était autorisée à lire et/ou écrire, mais cela n'aurait aucune applicabilité à notre système de données des joueurs, où cela ne peut pas être déterminé à l'avance en raison du mécanisme de verrouillage de session (couvrant plus de détails plus tard).
Une fois retirées de la file d'attente, il est difficile de décider d'une règle intuitive pour comment cela devrait être géré. Lorsqu'une requête DataStoreWrapper est faite, le thread actuel est suspendu jusqu'à ce qu'elle soit terminée. Si nous retirions les requêtes obsolètes de la file d'attente, nous devrions décider de retourner false, "Retiré de la file d'attente" ou de ne jamais retourner et de supprimer le thread actif. Les deux approches ont leurs propres inconvénients et déchargent une complexité supplémentaire sur le consommateur.
En fin de compte, notre avis est que l'approche simple (traiter chaque requête) est préférable ici et crée un environnement plus clair à naviguer lors de l'approche de problèmes complexes comme le verrouillage de session. La seule exception à cela est lors de DataModel:BindToClose(), où il devient nécessaire de vider la file d'attente pour sauvegarder les données de tous les utilisateurs à temps et la valeur que les appels de fonction individuels retournent n'est plus une préoccupation continue. Pour plus de contexte, voir Données des joueurs.
Verrouillage de session
Classe : SessionLockedDataStoreWrapper
Contexte
Les données des joueurs sont stockées en mémoire sur le serveur et ne sont lues et écrites dans les magasins de données sous-jacents que lorsque cela est nécessaire. Vous pouvez lire et mettre à jour les données des joueurs en mémoire instantanément sans avoir besoin de requêtes web et éviter de dépasser les limites de DataStoreService.
Pour que ce modèle fonctionne comme prévu, il est impératif qu'aucun serveur ne puisse charger les données d'un joueur dans la mémoire à partir de DataStore en même temps.
Par exemple, si le serveur A charge les données d'un joueur, le serveur B ne peut pas charger ces données tant que le serveur A n'a pas libéré son verrou lors d'une sauvegarde finale. Sans un mécanisme de verrouillage, le serveur B pourrait charger des données de joueur obsolètes à partir du magasin de données avant que le serveur A ait la chance de sauvegarder la version plus récente qu'il a en mémoire. Ensuite, si le serveur A sauvegarde ses données plus récentes après que le serveur B ait chargé les données obsolètes, le serveur B écraserait ces données plus récentes lors de sa prochaine sauvegarde.
Même si Roblox ne permet qu'à un client d'être connecté à un serveur à la fois, vous ne pouvez pas supposer que les données d'une session sont toujours sauvegardées avant le début de la session suivante. Considérez les scénarios suivants qui peuvent se produire lorsqu'un joueur quitte le serveur A :
- Le serveur A effectue une requête DataStore pour sauvegarder ses données, mais la requête échoue et nécessite plusieurs réessais pour se terminer avec succès. Pendant la période de réessai, le joueur rejoint le serveur B.
- Le serveur A effectue trop d'appels UpdateAsync() à la même clé et est limité. La requête de sauvegarde finale est placée dans une file d'attente. Pendant que la requête est dans la file d'attente, le joueur rejoint le serveur B.
- Sur le serveur A, un code connecté à l'événement PlayerRemoving suspend avant que les données du joueur ne soient sauvegardées. Avant que cette opération ne soit terminée, le joueur rejoint le serveur B.
- La performance du serveur A a diminué au point que la sauvegarde finale est retardée jusqu'après que le joueur ait rejoint le serveur B.
Ces scénarios devraient être rares, mais ils se produisent, en particulier dans des situations où un joueur se déconnecte d'un serveur et se connecte à un autre rapidement (par exemple, lors d'un téléport). Certains utilisateurs malveillants pourraient même tenter d'abuser de ce comportement pour effectuer des actions sans qu'elles ne persistent. Cela peut avoir un impact particulier dans les jeux qui permettent aux joueurs d'échanger et est une source courante d'exploits de duplication d'objets.
Le verrouillage de session aborde cette vulnérabilité en s'assurant que lorsque la clé DataStore d'un joueur est d'abord lue par le serveur, le serveur écrit de manière atomique un verrou dans les métadonnées de la clé à l'intérieur du même appel UpdateAsync(). Si cette valeur de verrou est présente lorsque tout autre serveur tente de lire ou d'écrire la clé, le serveur ne procède pas.
Approche

SessionLockedDataStoreWrapper est un méta-wrapper autour de la classe DataStoreWrapper. DataStoreWrapper fournit des fonctionnalités de mise en file d'attente et de réessai, que SessionLockedDataStoreWrapper complète avec le verrouillage de session.
SessionLockedDataStoreWrapper passe chaque requête DataStore—qu'elle soit GetAsync, SetAsync ou UpdateAsync—à travers UpdateAsync. Cela est dû au fait que UpdateAsync permet à une clé d'être à la fois lue et écrite de manière atomique. Il est également possible d'abandonner l'écriture en fonction de la valeur lue en retournant nil dans le rappel de transformation.
La fonction de transformation passée dans UpdateAsync pour chaque requête effectue les opérations suivantes :
Vérifie que la clé est sûre à accéder, abandonnant l'opération si ce n'est pas le cas. "Sûre à accéder" signifie :
L'objet de métadonnées de la clé n'inclut pas une valeur LockId non reconnue qui a été mise à jour pour la dernière fois il y a moins de temps d'expiration de verrou. Cela tient compte du respect d'un verrou placé par un autre serveur et de l'ignorance de ce verrou s'il a expiré.
Si ce serveur a précédemment placé sa propre valeur LockId dans les métadonnées de la clé, alors cette valeur est toujours dans les métadonnées de la clé. Cela tient compte de la situation où un autre serveur a pris le verrou de ce serveur (par expiration ou par force) et l'a ensuite libéré. En d'autres termes, même si LockId est nil, un autre serveur pourrait toujours avoir remplacé et supprimé un verrou dans le temps écoulé depuis que vous avez verrouillé la clé.
UpdateAsync effectue l'opération DataStore que le consommateur de SessionLockedDataStoreWrapper a demandée. Par exemple, GetAsync() se traduit par function(value) return value end.
En fonction des paramètres passés dans la requête, UpdateAsync verrouille ou déverrouille la clé :
Si la clé doit être verrouillée, UpdateAsync définit le LockId dans les métadonnées de la clé à un GUID. Ce GUID est stocké en mémoire sur le serveur afin qu'il puisse être vérifié la prochaine fois qu'il accède à la clé. Si le serveur a déjà un verrou sur cette clé, il ne fait aucun changement. Il planifie également une tâche pour vous avertir si vous n'accédez pas à la clé à nouveau pour maintenir le verrou dans le temps d'expiration du verrou.
Si la clé doit être déverrouillée, UpdateAsync supprime le LockId dans les métadonnées de la clé.
Un gestionnaire de réessai personnalisé est passé dans le DataStoreWrapper sous-jacent afin que l'opération soit réessayée si elle a été abandonnée à l'étape 1 en raison du verrouillage de session.
Un message d'erreur personnalisé est également retourné au consommateur, permettant au système de données des joueurs de signaler une erreur alternative en cas de verrouillage de session au client.
Mises en garde
Le régime de verrouillage de session repose sur le fait qu'un serveur libère toujours son verrou sur une clé lorsqu'il a terminé avec elle. Cela devrait toujours se faire par une instruction pour déverrouiller la clé dans le cadre de l'écriture finale dans PlayerRemoving ou BindToClose().
Cependant, le déverrouillage peut échouer dans certaines situations. Par exemple :
- Le serveur a planté ou DataStoreService était inopérable pour toutes les tentatives d'accès à la clé.
- En raison d'une erreur de logique ou d'un bug similaire, l'instruction pour déverrouiller la clé n'a pas été faite.
Pour maintenir le verrou sur une clé, vous devez y accéder régulièrement tant qu'elle est chargée en mémoire. Cela se ferait normalement dans le cadre de la boucle de sauvegarde automatique qui s'exécute en arrière-plan dans la plupart des systèmes de données des joueurs, mais ce système expose également une méthode refreshLockAsync si vous devez le faire manuellement.
Si le temps d'expiration du verrou a été dépassé sans que le verrou ne soit mis à jour, alors tout serveur est libre de prendre le verrou. Si un serveur différent prend le verrou, les tentatives du serveur actuel de lire ou d'écrire la clé échouent à moins qu'il n'établisse un nouveau verrou.
Traitement des produits des développeurs
Singleton : ReceiptHandler
Contexte
Le rappel ProcessReceipt effectue le travail critique de déterminer quand finaliser un achat. ProcessReceipt est appelé dans des scénarios très spécifiques. Pour son ensemble de garanties, voir MarketplaceService.ProcessReceipt.
Bien que la définition de "traiter" un achat puisse différer entre les jeux, nous utilisons les critères suivants :
L'achat n'a pas déjà été traité.
L'achat est reflété dans la session actuelle.
Cela nécessite d'effectuer les opérations suivantes avant de retourner PurchaseGranted :
- Vérifiez que le PurchaseId n'a pas déjà été enregistré comme traité.
- Attribuez l'achat dans les données du joueur en mémoire.
- Enregistrez le PurchaseId comme traité dans les données du joueur en mémoire.
- Écrivez les données du joueur en mémoire dans le DataStore.
Le verrouillage de session simplifie ce flux, car vous n'avez plus à vous soucier des scénarios suivants :
- Les données du joueur en mémoire sur le serveur actuel pouvant être obsolètes, nécessitant que vous récupériez la dernière valeur du DataStore avant de vérifier l'historique du PurchaseId.
- Le rappel pour le même achat s'exécutant sur un autre serveur, nécessitant que vous lisiez et écriviez à la fois l'historique du PurchaseId et sauvegardiez les données du joueur mises à jour avec l'achat reflété de manière atomique pour éviter les conditions de course.
Le verrouillage de session garantit que, si une tentative d'écriture dans le DataStore du joueur est réussie, aucun autre serveur n'a réussi à lire ou écrire dans le DataStore du joueur entre le chargement des données et la sauvegarde sur ce serveur. En bref, les données du joueur en mémoire sur ce serveur sont la version la plus à jour disponible. Il y a quelques mises en garde, mais elles n'impactent pas ce comportement.
Approche
Les commentaires dans ReceiptProcessor décrivent l'approche :
Vérifiez que les données du joueur sont actuellement chargées sur ce serveur et qu'elles ont été chargées sans erreurs.
Parce que ce système utilise le verrouillage de session, cette vérification vérifie également que les données en mémoire sont la version la plus à jour.
Si les données du joueur n'ont pas encore été chargées (ce qui est attendu lorsqu'un joueur rejoint un jeu), attendez que les données du joueur soient chargées. Le système écoute également le départ du joueur du jeu avant que ses données ne soient chargées, car il ne doit pas suspendre indéfiniment et bloquer ce rappel d'être invoqué à nouveau sur ce serveur pour cet achat si le joueur rejoint à nouveau.
Vérifiez que le PurchaseId n'est pas déjà enregistré comme traité dans les données du joueur.
En raison du verrouillage de session, le tableau des PurchaseIds que le système a en mémoire est la version la plus à jour. Si le PurchaseId est enregistré comme traité et reflété dans une valeur qui a été chargée ou sauvegardée dans le DataStore, retournez PurchaseGranted. S'il est enregistré comme traité, mais non reflété dans le DataStore, retournez NotProcessedYet.
Mettez à jour les données du joueur localement sur ce serveur pour "attribuer" l'achat.
ReceiptProcessor adopte une approche de rappel générique et assigne un rappel différent pour chaque DeveloperProductId.
Mettez à jour les données du joueur localement sur ce serveur pour stocker le PurchaseId.
Soumettez une requête pour sauvegarder les données en mémoire dans le DataStore, retournant PurchaseGranted si la requête est réussie. Sinon, retournez NotProcessedYet.
Si cette requête de sauvegarde n'est pas réussie, une requête ultérieure pour sauvegarder les données de session en mémoire du joueur pourrait encore réussir. Lors du prochain appel à ProcessReceipt, l'étape 2 gère cette situation et retourne PurchaseGranted.
Données des joueurs
Singletons : PlayerData.Server, PlayerData.Client
Contexte
Les modules qui fournissent une interface pour que le code puisse lire et écrire de manière synchrone les données de session des joueurs sont courants dans les jeux Roblox. Cette section couvre PlayerData.Server et PlayerData.Client.
Approche
PlayerData.Server et PlayerData.Client gèrent les éléments suivants :
- Chargement des données du joueur en mémoire, y compris la gestion des cas où le chargement échoue
- Fournir une interface pour que le code serveur puisse interroger et modifier les données du joueur
- Répliquer les changements dans les données du joueur au client afin que le code client puisse y accéder
- Répliquer les erreurs de chargement et/ou de sauvegarde au client afin qu'il puisse afficher des dialogues d'erreur
- Sauvegarder les données du joueur périodiquement, lorsque le joueur quitte et lorsque le serveur s'arrête
Charger les données du joueur

SessionLockedDataStoreWrapper effectue une requête getAsync au magasin de données.
Si cette requête échoue, les données par défaut sont utilisées et le profil est marqué comme "en erreur" pour s'assurer qu'il n'est pas écrit dans le magasin de données plus tard.
Une option alternative est de renvoyer le joueur, mais nous recommandons de laisser le joueur jouer avec des données par défaut et un message clair sur ce qui s'est passé plutôt que de les retirer du jeu.
Une charge utile initiale est envoyée à PlayerDataClient contenant les données chargées et le statut d'erreur (le cas échéant).
Tous les threads suspendus utilisant waitForDataLoadAsync pour le joueur sont repris.
Fournir une interface pour le code serveur
- PlayerDataServer est un singleton qui peut être requis et accessible par tout code serveur s'exécutant dans le même environnement.
- Les données des joueurs sont organisées en un dictionnaire de clés et de valeurs. Vous pouvez manipuler ces valeurs sur le serveur en utilisant les méthodes setValue, getValue, updateValue et removeValue. Ces méthodes fonctionnent toutes de manière synchrone sans suspendre.
- Les méthodes hasLoaded et waitForDataLoadAsync sont disponibles pour s'assurer que les données ont été chargées avant d'y accéder. Nous recommandons de le faire une fois pendant un écran de chargement avant que d'autres systèmes ne soient démarrés pour éviter d'avoir à vérifier les erreurs de chargement avant chaque interaction avec les données sur le client.
- Une méthode hasErrored peut interroger si le chargement initial du joueur a échoué, les obligeant à utiliser des données par défaut. Vérifiez cette méthode avant de permettre au joueur de faire des achats, car les achats ne peuvent pas être sauvegardés dans les données sans un chargement réussi.
- Un signal playerDataUpdated se déclenche avec le player, key et value chaque fois que les données d'un joueur sont modifiées. Des systèmes individuels peuvent s'abonner à cela.
Répliquer les changements au client
- Tout changement dans les données du joueur dans PlayerDataServer est répliqué à PlayerDataClient, à moins que cette clé n'ait été marquée comme privée en utilisant setValueAsPrivate
- setValueAsPrivate est utilisé pour désigner les clés qui ne doivent pas être envoyées au client
- PlayerDataClient inclut une méthode pour obtenir la valeur d'une clé (get) et un signal qui se déclenche lorsqu'elle est mise à jour (updated). Une méthode hasLoaded et un signal loaded sont également inclus, afin que le client puisse attendre que les données se chargent et se répliquent avant de démarrer ses systèmes
- PlayerDataClient est un singleton qui peut être requis et accessible par tout code client s'exécutant dans le même environnement
Répliquer les erreurs au client
- Les statuts d'erreur rencontrés lors de la sauvegarde ou du chargement des données des joueurs sont répliqués à PlayerDataClient.
- Accédez à ces informations avec les méthodes getLoadError et getSaveError, ainsi que les signaux loaded et saved.
- Il existe deux types d'erreurs : DataStoreError (la requête DataStoreService a échoué) et SessionLocked (voir Verrouillage de session).
- Utilisez ces événements pour désactiver les invites d'achat du client et mettre en œuvre des dialogues d'avertissement. Cette image montre un exemple de dialogue :

Sauvegarder les données du joueur

Lorsque le joueur quitte le jeu, le système prend les étapes suivantes :
- Vérifiez s'il est sûr d'écrire les données du joueur dans le magasin de données. Les scénarios où il serait dangereux incluent le fait que les données du joueur échouent à se charger ou soient encore en cours de chargement.
- Faites une requête via le SessionLockedDataStoreWrapper pour écrire la valeur actuelle des données en mémoire dans le magasin de données et supprimer le verrou de session une fois terminé.
- Effacez les données du joueur (et d'autres variables telles que les métadonnées et les statuts d'erreur) de la mémoire du serveur.
Dans une boucle périodique, le serveur écrit les données de chaque joueur dans le magasin de données (à condition qu'il soit sûr de sauvegarder). Cette redondance bienvenue atténue la perte en cas de plantage du serveur et est également nécessaire pour maintenir le verrou de session.
L'échantillon démarre une boucle partagée après AUTO_SAVE_INTERVAL secondes (180 par défaut) et sauvegarde ensuite chaque joueur chargé en parallèle. Cette boucle ne décale pas les serveurs ou les joueurs, donc les serveurs qui démarrent à des moments similaires peuvent se vider ensemble.
Décalez la première sauvegarde de chaque joueur par une durée aléatoire dans l'intervalle afin que les serveurs en direct ne sauvegardent pas tous en même temps :
local AUTO_SAVE_INTERVAL = 180local function startAutoSave(player)task.spawn(function()task.wait(math.random() * AUTO_SAVE_INTERVAL)while player.Parent doif canSave(player) thensavePlayerData(player)endtask.wait(AUTO_SAVE_INTERVAL)endend)endLorsqu'une requête pour arrêter le serveur est reçue, les étapes suivantes se produisent dans un rappel BindToClose :
- Une requête est faite pour sauvegarder les données de chaque joueur sur le serveur, suivant le processus normalement suivi lorsqu'un joueur quitte le serveur. Ces requêtes sont faites en parallèle, car les rappels BindToClose n'ont que 30 secondes pour se terminer.
- Pour accélérer les sauvegardes, toutes les autres requêtes dans la file d'attente de chaque clé sont effacées du DataStoreWrapper sous-jacent (voir Réessais).
- Le rappel ne retourne pas tant que toutes les requêtes ne sont pas terminées.