Implémenter des systèmes de données joueur et de gestion des achats

*Ce contenu est traduit en utilisant l'IA (Beta) et peut contenir des erreurs. Pour consulter cette page en anglais, clique ici.

Contexte

Roblox fournit un ensemble d'API pour interagir avec des magasins de données via DataStoreService. Le cas d'utilisation le plus courant de ces API est la sauvegarde, le chargement et la réplication des données joueur. 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 différentes sessions de jeu.

La plupart des jeux sur Roblox utilisent ces API pour mettre en place une forme de système de données joueur. Ces mises en œuvre 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 joueur 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 au cours du gameplay normal. La plupart des systèmes de données joueur 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
    • Écrits périodiques à intervalle pour atténuer le scénario où l'écriture finale échoue
    • Écritures pour garantir 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 inter-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 le contenu généré par les utilisateurs en 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 joueur 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 veulent 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 accédés, la plupart des solutions mettront en œuvre un mécanisme de réessai et une alternative 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 ultérieurement 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 une alternative aux données par défaut. Faites attention à ce 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 dans lesquels un serveur sauvegarde des informations périmées. Cela peut entraîner des pertes de données et des failles de duplication d'objets courants.

  • 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.

Exemple de code

Roblox a du code de référence pour vous aider à concevoir et à construire des systèmes de données joueur. Le reste de cette page examine le contexte, les détails de mise en œuvre et les mises en garde générales.


Après avoir importé le modèle dans Studio, vous devriez voir la structure de dossiers suivante :

Fenêtre Explorateur montrant le modèle du système d'achat.

Architecture

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

Un diagramme d'architecture pour l'exemple de code.

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 de DataStore lancent des erreurs, vous permettant de les gérer.

Un commun "piège" peut se produire si vous essayez de gérer les échecs de magasin de données comme ceci :


local function retrySetAsync(dataStore, key, value)
for _ = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
break
end
task.wait(TIME_BETWEEN_ATTEMPTS)
end
end

Bien que ceci soit un mécanisme de réessai parfaitement valide pour une fonction générique, il 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 :

  1. La requête A est faite pour définir la valeur de la clé K à 1.
  2. La requête échoue, donc un réessai est programmé pour s'exécuter dans 2 secondes.
  3. Avant que le réessai n'ait lieu, 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 tout de même être traitées dans l'ordre pour éviter des états transitoires invalides (par exemple, un achat soustrait des pièces avant qu'un ajout de pièces soit traité, entraînant une quantité négative de pièces).

Notre système de données joueur utilise une nouvelle classe, DataStoreWrapper, qui fournit des réessais avec suspension garantis d'être traités dans l'ordre par clé.

Approche

Un diagramme de processus illustrant le système de réessai

DataStoreWrapper fournit des méthodes correspondant aux méthodes de DataStore : DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() et DataStore:RemoveAsync().

Ces méthodes, lorsqu'elles sont appelées :

  1. 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 de manière séquentielle. Le fil d'exécution demande à suspendre son traitement 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 et un limiteur de taux basé sur les coroutines. Plutôt que de renvoyer une promesse, ThreadQueue suspend le fil d'exécution actuel jusqu'à ce que l'opération soit terminée et lance une erreur si elle échoue. Cela est plus cohérent avec les modèles asynchrones idiomatiques de Luau.

  2. Si une requête échoue, elle réessaie avec un retour arrière exponentiel configurable. Ces réessais font partie du rappel soumis à la ThreadQueue, ils sont donc garantis de se terminer avant que la prochaine requête dans la file d'attente pour cette clé ne commence.

  3. Lorsqu'une requête est terminée, la méthode de requête renvoie 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 nettoyer les requêtes obsolètes. Cette dernière option est particulièrement utile dans les scénarios où le serveur s'arrête 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 qu'en dehors de scénarios extrêmes, chaque requête de magasin de données devrait être autorisée à se terminer (avec ou sans succès), même si une requête plus récente la rend redondante. Lorsqu'une nouvelle requête se produit, les anciennes requê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 enracinée dans l'applicabilité de ce module en tant qu'utilitaire générique de magasin de données plutôt que comme un outil spécifique aux données joueur, et est la suivante :

  1. Il est difficile de décider d'un ensemble de règles intuitives pour savoir quand une requête peut être supprimé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() renvoie 1, mais si nous supprimons la requête SetAsync() de la file d'attente parce qu'elle a été rendue redondante par la plus récente, elle renverrait 0.

    La progression logique est que lorsqu'une nouvelle requête d'écriture est ajoutée, seuls les anciens requêtes doivent être éliminées aussi loin que la plus récente requête de lecture. UpdateAsync, de loin l'opération la plus courante (et la seule utilisée par ce système), peut lire et écrire, il serait donc difficile de concilier cela au sein de ce modèle sans ajouter de 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 joueur, 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).

  2. Une fois supprimées de la file d'attente, il est difficile de décider d'une règle intuitive sur comment cela doit être géré. Lorsqu'une requête DataStoreWrapper est effectuée, le fil d'exécution actuel est suspendu jusqu'à ce qu'elle soit terminée. Si nous supprimions les requêtes obsolètes de la file d'attente, nous devrions décider de retourner false, "Retiré de la file" ou de ne jamais retourner et de supprimer le fil actif. Les deux approches comportent leurs propres inconvénients et ajoutent une complexité supplémentaire au consommateur.

En fin de compte, notre point de vue est que l'approche simple (traitement de 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 se produit lors de DataModel:BindToClose(), où il devient nécessaire de nettoyer la file d'attente pour sauvegarder les données de tous les utilisateurs à temps et la valeur renvoyée par les appels de fonction individuels n'est plus une préoccupation en cours. Pour plus de contexte, voir Données joueur.

Verrouillage de session

Classe : SessionLockedDataStoreWrapper

Contexte

Les données joueur sont stockées en mémoire sur le serveur et ne sont lues et écrites dans les magasins de données sous-jacents que si nécessaire. Vous pouvez lire et mettre à jour les données joueur 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'un seul serveur puisse charger les données d'un joueur en mémoire depuis le 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 mécanisme de verrouillage, le serveur B pourrait charger des données de joueur périmées du magasin de données avant que le serveur A ait une 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 périmées, le serveur B écrasera ces données plus récentes lors de sa prochaine sauvegarde.

Même si Roblox n'autorise qu'un client à ê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 prochaine session. Considérez les scénarios suivants qui peuvent se produire lorsqu'un joueur quitte le serveur A :

  1. 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 réussir. Pendant la période de réessai, le joueur rejoint le serveur B.
  2. Le serveur A effectue trop d'appels UpdateAsync() à la même clé et se fait limiter. 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.
  3. Sur le serveur A, un certain code lié à l'événement PlayerRemoving suspend avant que les données du joueur soient enregistrées. Avant que cette opération ne se termine, le joueur rejoint le serveur B.
  4. Les performances du serveur A se sont dégradées au point où 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 de la téléportation). Certains utilisateurs malveillants pourraient même tenter d'abuser de ce comportement pour réaliser des actions sans qu'elles ne persistent. Cela peut être particulièrement impactant dans des jeux qui permettent aux joueurs d'échanger et constitue une source courante d'exploits de duplication d'objets.

Le verrouillage de session aborde cette vulnérabilité en garantissant que lorsque la clé de DataStore d'un joueur est lue pour la première fois par le serveur, le serveur écrit atomiquement un verrou dans les métadonnées de la clé dans le 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

Un diagramme de processus illustrant le système de verrouillage de session

SessionLockedDataStoreWrapper est un méta-wrapper autour de la classe DataStoreWrapper. DataStoreWrapper fournit des fonctionnalités de file d'attente et de réessai, que SessionLockedDataStoreWrapper complète avec le verrouillage de session.

SessionLockedDataStoreWrapper fait passer chaque requête DataStore—qu'elle soit GetAsync, SetAsync ou UpdateAsync—à travers UpdateAsync. Cela est dû au fait que UpdateAsync permet qu'une clé soit à la fois lue et écrite de manière atomique. Il est également possible d'abandonner l'écriture en fonction de la valeur lue en renvoyant nil dans le rappel de transformation.

La fonction de transformation passée à UpdateAsync pour chaque requête effectue les opérations suivantes :

  1. Vérifie que la clé est sûre à accéder, en abandonnant l'opération si ce n'est pas le cas. "Sûr à 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 il y a moins de temps que l'expiration du 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 déjà 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 repris le verrou de ce serveur (par expiration ou de force) et l'a ensuite libéré. Pour le dire autrement, même si LockId est nil, un autre serveur pourrait toujours avoir remplacé et retiré un verrou depuis le moment où vous avez verrouillé la clé.

  2. 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.

  3. En fonction des paramètres passés dans la requête, UpdateAsync verrouille ou déverrouille la clé :

    1. Si la clé doit être verrouillée, UpdateAsync définit le LockId dans les métadonnées de la clé avec 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 modifie rien. Il programme également une tâche pour vous avertir si vous n'accédez pas à la clé à nouveau pour maintenir le verrou dans les délais d'expiration.

    2. 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é au 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 renvoyé au consommateur, permettant au système de données joueur de signaler une erreur alternative dans le cas de verrouillage de session au client.

Mises en garde

Le régime de verrouillage de session repose sur un serveur qui libère toujours son verrou sur une clé lorsqu'il a fini de l'utiliser. Cela devrait toujours se produire 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 s'est écrasé 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é donnée.

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 serait normalement fait dans le cadre de la boucle de sauvegarde automatique en arrière-plan dans la plupart des systèmes de données 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 pour lire ou écrire la clé échouent à moins qu'il n'établisse un nouveau verrou.

Traitement des produits du développeur

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 "traitement" d'un achat puisse différer entre les jeux, nous utilisons les critères suivants :

  1. L'achat n'a pas déjà été traité.

  2. L'achat est reflété dans la session actuelle.

  3. L'achat a été enregistré dans le DataStore.

    Chaque achat, même des consommables uniques, doit être reflété dans le DataStore afin que l'historique d'achats des utilisateurs soit inclus avec leurs données de session.

Cela nécessite de réaliser les opérations suivantes avant de renvoyer PurchaseGranted :

  1. Vérifiez que le PurchaseId n'a pas déjà été enregistré comme traité.
  2. Accordez l'achat dans les données joueur en mémoire du joueur.
  3. Enregistrez le PurchaseId comme traité dans les données joueur en mémoire du joueur.
  4. Écrivez les données joueur en mémoire du joueur dans le DataStore.

Le verrouillage de session simplifie ce flux, car vous n'avez plus à vous soucier des scénarios suivants :

  • Les données joueur en mémoire dans le serveur actuel pouvant être périmées, vous obligeant à obtenir la dernière valeur du DataStore avant de vérifier l'historique du PurchaseId.
  • Le rappel pour le même achat s'exécutant dans un autre serveur, vous obligeant à lire et écrire l'historique du PurchaseId et à sauvegarder les données joueur mises à jour avec l'achat reflété de manière atomique pour éviter des 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 et la sauvegarde des données dans ce serveur. En résumé, les données joueur en mémoire dans ce serveur sont la version la plus à jour disponible. Il y a certaines mises en garde, mais elles n'affectent pas ce comportement.

Approche

Les commentaires dans ReceiptProcessor décrivent l'approche :

  1. 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 le chargement de ses données, car il ne doit pas suspendre indéfiniment et bloquer le rappel d'être invoqué à nouveau sur ce serveur pour cet achat si le joueur rejoint à nouveau.

  2. 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, renvoyez PurchaseGranted. S'il est enregistré comme traité, mais non reflété dans le DataStore, renvoyez NotProcessedYet.

  3. Mettez à jour les données joueur localement dans ce serveur pour "attribuer" l'achat.

    ReceiptProcessor utilise une approche de rappel générique et attribue un rappel différent pour chaque DeveloperProductId.

  4. Mettez à jour les données du joueur localement dans ce serveur pour stocker le PurchaseId.

  5. Soumettez une requête pour sauvegarder les données en mémoire dans le DataStore, renvoyant PurchaseGranted si la requête réussit. Sinon, renvoyez 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 renvoie PurchaseGranted.

Données joueur

Singletons : PlayerData.Server, PlayerData.Client

Contexte

Les modules qui fournissent une interface pour le code afin de lire et d'écrire des données de session de joueur de manière synchrone 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 :

  1. Chargement des données du joueur en mémoire, y compris la gestion des cas où cela échoue
  2. Fourniture d'une interface pour que le code serveur interroge et change les données du joueur
  3. Réplication des changements dans les données du joueur au client afin que le code client puisse y accéder
  4. Réplication des erreurs de chargement et/ou de sauvegarde au client afin qu'il puisse afficher des dialogues d'erreur
  5. Sauvegarde des données du joueur périodiquement, lorsque le joueur quitte et lorsque le serveur s'arrête

Charger les données joueur

Un diagramme de processus illustrant le système de chargement
  1. SessionLockedDataStoreWrapper effectue une requête getAsync vers le magasin de données.

    Si cette requête échoue, les données par défaut sont utilisées et le profil est marqué comme "erreur" pour s'assurer qu'il n'est pas écrit dans le magasin de données plus tard.

    Une option alternative consiste à expulser le joueur, mais nous vous recommandons de laisser le joueur jouer avec les données par défaut et un message clair sur ce qui s'est passé plutôt que de le retirer du jeu.

  2. Une charge initiale est envoyée à PlayerDataClient contenant les données chargées et l'état d'erreur (le cas échéant).

  3. Tous les fils 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 du joueur sont organisées dans 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 suspension.
  • 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 faire cela une fois pendant un écran de chargement avant que d'autres systèmes ne soient démarrés afin d'éviter de devoir vérifier les erreurs de chargement avant chaque interaction avec les données côté client.
  • Une méthode hasErrored peut interroger si le chargement initial du joueur a échoué, l'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 est déclenché 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 des changements au client

  • Tout changement apporté aux données du joueur dans PlayerDataServer est répliqué dans PlayerDataClient, sauf si cette clé a é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'il est mis à jour (updated). Une méthode hasLoaded et un signal loaded sont également inclus, de sorte que le client puisse attendre que les données soient chargées et répliquées avant de commencer 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 états d'erreur rencontrés lors de la sauvegarde ou du chargement des données joueur sont répliqués dans PlayerDataClient.
  • Accédez à cette information 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 côté client et mettre en œuvre des dialogues d'avertissement. Cette image montre un exemple de dialogue :
Une capture d'écran d'un exemple d'avertissement qui pourrait être affiché lorsque les données joueur échouent à se charger

Sauvegarder les données joueur

Un diagramme de processus illustrant le système de sauvegarde
  1. Lorsque le joueur quitte le jeu, le système prend les étapes suivantes :

    1. Vérifiez s'il est sûr d'écrire les données du joueur dans le magasin de données. Des scénarios où cela serait dangereux incluent les données du joueur ayant échoué à se charger ou étant encore en cours de chargement.
    2. Faites une requête via le SessionLockedDataStoreWrapper pour écrire la valeur de données actuelle en mémoire dans le magasin de données et retirez le verrou de session une fois terminé.
    3. Efface les données du joueur (et d'autres variables telles que les métadonnées et les états d'erreur) de la mémoire du serveur.
  2. 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 les pertes en cas de crash du serveur et est également nécessaire pour maintenir le verrou de session.

  3. Lorsqu'une requête d'arrêt du serveur est reçue, ce qui suit se produit dans un rappel BindToClose :

    1. Une requête est faite pour sauvegarder les données de chaque joueur sur le serveur, en suivant le processus normalement effectué lorsqu'un joueur quitte le serveur. Ces requêtes sont effectuées en parallèle, car les rappels BindToClose n'ont que 30 secondes pour se compléter.
    2. 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).
    3. Le rappel ne retourne pas tant que toutes les requêtes sont terminées.
©2026 Société Roblox. Roblox, le logo Roblox et Powering Imagination font partie de nos marques déposées aux États-Unis et dans d'autres pays.