Implémenter les données et les systèmes d'achat du joueur

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

Arrière-plan

Roblox fournit un ensemble d'APIs pour s'interfacer avec des magasins de données via DataStoreService .L'utilisation la plus courante de ces API est pour sauvegarder, charger et répliquer les données du joueur **.C'est-à-dire, les données associées à la progression du joueur, aux achats et à d'autres caractéristiques de session qui persistent entre les sessions de jeu individuelles.

La plupart des expériences sur Roblox utilisent ces API pour mettre en œuvre une forme de système de données du joueur.Ces implémentations diffèrent dans leur approche, mais cherchent généralement à résoudre le même ensemble de problèmes.

Problèmes communs

Voici quelques-uns des problèmes les plus courants que les systèmes de données joueur tentent de résoudre :

  • Dans l'accès à la mémoire : DataStoreService les demandes font des demandes Web qui fonctionnent en asynchrone et sont soumises à des limites de taux.Cela est approprié pour un chargement initial au début de la session, mais pas pour les opérations de lecture et d'écriture de fréquence élevée pendant le cours normal du partie.Les systèmes de données joueur de la plupart des développeurs stockent ces données en mémoire sur le serveur Roblox, limitant les demandes DataStoreService à les scénarios suivants :

    • Lecture initiale au début d'une session
    • Écriture finale à la fin de la session
    • Écriture périodique à une fréquence pour atténuer le scénario où l'écriture finale échoue
    • Écrit pour garantir que les données sont sauvegardées lors du traitement d'un acheter
  • Stockage efficace : Le stockage de toutes les données de session d'un joueur dans une seule table vous permet de mettre à jour plusieurs valeurs atomiquement et de gérer la même quantité de données en moins de demandes.Il élimine également le risque de désynchronisation inter valeur et facilite les rétrogradations à raisonner.

    Certains développeurs implémentent également une serialisation personnalisée pour compresser de grandes structures de données (généralement pour enregistrer le contenu généré par l'utilisateur 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 de la réplication des données du joueur vers le client vous permet de transmettre ces informations sans avoir à créer des systèmes de réplication sur mesure pour chaque composante des données.Les développeurs souhaitent souvent que l'option soit sélective quant à ce qui est et n'est pas répliqué au client.

  • Gestion des erreurs : Lorsque les magasins de données ne peuvent pas être accédés, la plupart des solutions mettront en œuvre un mécanisme de réessai et un mécanisme de rétrogradation aux données « par défaut ».Une attention spéciale est nécessaire pour garantir que les données de rechange ne remplacent pas ultérieurement les données « réelles », et que cela soit correctement communiqué au joueur.

  • Réessayez : Lorsque les magasins de données sont inaccessibles, la plupart des solutions mettent en œuvre un mécanisme de réessai et un mécanisme de rechange pour les données par défaut.Prenez soin de veiller à ce que les données de rechange ne remplacent pas ultérieurement 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 enregistre des informations obsolètes.Cela peut entraîner une perte de données et des failles de duplication d'objets communs.

  • Traitement d'achat atomique : Vérifiez, récompensez et enregistrez les achats atomiquement pour empêcher les articles d'être perdus ou attribués plusieurs fois.

coded'échantillon

Roblox a un code de référence pour vous aider à concevoir et à construire des systèmes de données joueur.Le reste de cette page examine les détails de l'arrière-plan, les détails de l'implémentation et les avertissements généraux.


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

Explorer window showing the purchasing system model.

Architecture

Ce diagramme de haut niveau illustre les systèmes clés dans l'échantillon et la façon dont ils interagissent avec le code dans le reste de l'expérience.

An architecture diagram for the code sample.

Réessais

Classe : DataStoreWrapper >

Arrière-plan

Comme DataStoreService fait des demandes Web sous le capot, ses demandes ne sont pas garanties de réussir.Lorsque cela se produit, les méthodes DataStore lancent des erreurs, vous permettant de les gérer.

Un « gotcha » commun peut se produire si vous essayez de gérer les échecs du magasin de données comme celui-ci :


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 ce soit un mécanisme de réessai parfaitement valide pour une fonction générique, il n'est pas adapté aux demandes DataStoreService car il ne garantit pas l'ordre dans lequel les demandes sont faites.Préserver l'ordre des demandes est important pour les demandes DataStoreService car elles interagissent avec l'état.Envisagez le scénario suivant :

  1. La demande A est faite pour définir la valeur de la clé K à 1.
  2. La demande échoue, une re tentative est donc prévue dans 2 secondes.
  3. Avant que la réessai se produise, demandez à B de définir la valeur de K à 2, mais la réessai de la demande A remplace immédiatement cette valeur et définit K à 1.

Bien que UpdateAsync opère sur la dernière version de la valeur de la clé, UpdateAsync les demandes doivent toujours être traitées afin d'éviter les états transitoires invalides (par exemple, un achat soustrait des pièces avant qu'une addition de pièces ne soit traitée, ce qui donne des pièces négatives).

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

Approche

An process diagram illustrating the retry system

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 :

  1. Ajoutez la demande à une file d'attente.Chaque clé a sa propre file d'attente, où les demandes sont traitées dans l'ordre et en série.Le thread demandeur se rend jusqu'à ce que la demande soit terminée.

    Cette fonctionnalité est basée sur la classe ThreadQueue, qui est un programmeur de tâches et un limiteur de taux basé sur des coroutes.Plutôt que de retourner une promesse, ThreadQueue produit le thread actuel jusqu'à ce que l'opération soit terminée et lance une erreur s'il échoue.Cela est plus cohérent avec les modèles Luau asynchrones idiomatiques.

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

  3. Lorsqu'une demande est terminée, la méthode de demande 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 effacer les demandes expirées.Cette dernière option est particulièrement utile dans les scénarios où le serveur s'arrête et il n'y a pas de temps pour traiter toutes les demandes, mais les plus récentes.

Avertissements

DataStoreWrapper suit le principe selon lequel, en dehors des scénarios extrêmes, chaque demande de stockage de données devrait être autorisée à se terminer (avec succès ou autrement), même si une demande plus récente la rend obsolète.Lorsqu'une nouvelle demande se produit, les demandes expirées ne sont pas supprimées de la file d'attente, mais sont plutôt autorisées à se terminer avant que la nouvelle demande ne soit lancée.La raison de ceci est liée à l'applicabilité de ce module en tant qu'utilité de stockage de données générique plutôt qu'un outil spécifique pour les données du joueur, et est la suivante :

  1. Il est difficile de décider d'un ensemble de règles intuitif pour savoir quand une demande peut être 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() retournerait 1, mais si nous retirons la demande SetAsync() de la file d'attente en raison du fait qu'elle est rendue obsolète par la plus récente, elle retournerait 0 .

    La progression logique est que lorsqu'une nouvelle demande d'écriture est ajoutée, seules les requêtes périmées sont prunées jusqu'à la dernière demande de lecture.UpdateAsync(), de loin l'opération la plus courante (et la seule utilisée par ce système), peut à la fois lire et écrire, ce qui rendrait difficile de réconcilier dans ce design sans ajouter de complexité supplémentaire.

    DataStoreWrapper pourrait vous obliger à spécifier si une demande UpdateAsync() a été autorisée à lire et/ou écrire, mais elle n'aurait aucune application dans 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 (abordé en détail plus tard).

  2. Une fois retiré 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 demande DataStoreWrapper est faite, le thread actuel est libéré jusqu'à ce qu'il soit terminé.Si nous supprimions les demandes expirées de la file d'attente, nous devrions décider de retourner false, "Removed from queue" ou de ne jamais retourner et de jeter le thread actif.Les deux approches comportent leurs propres inconvénients et déchargent la complexité supplémentaire sur le consommateur.

En fin de compte, notre point de vue est que l'approche simple (traitement de chaque demande) est préférable ici et crée un environnement plus clair dans lequel naviguer lorsqu'on aborde des problèmes complexes comme le verrouillage de session.La seule exception à cela est pendant DataModel:BindToClose() , où le nettoyage de la file d'attente devient nécessaire pour sauvegarder les données de tous les utilisateurs à temps et la valeur des appels de fonction individuels n'est plus une préoccupation en cours.Pour tenir compte de cela, nous exposons une méthode skipAllQueuesToLastEnqueued .Pour plus de contexte, voir données du joueur.

Verrouillage de session

Classe : SessionLockedDataStoreWrapper >

Arrière-plan

Les données du joueur sont stockées en mémoire sur le serveur et ne sont lues et écrites que dans les magasins de données sous-jacents lorsque cela est nécessaire.Vous pouvez lire et mettre à jour les données du joueur en mémoire instantanément sans avoir besoin de demandes Web et éviter de dépasser les limites DataStoreService .

Pour que ce modèle fonctionne comme prévu, il est impératif que plus d'un serveur ne soit pas capable de charger les données d'un joueur dans la mémoire à partir du 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 jusqu'à ce que le serveur A relève son verrouillage dessus pendant une sauvegarde finale.Sans mécanisme de verrouillage, le serveur B pourrait charger les données du joueur obsolètes 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 enregistre 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 son prochain enregistrement.

Bien que Roblox ne permette qu'un client de se connecter à un seul 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 fait une demande DataStore de sauvegarde de ses données, mais la demande échoue et nécessite plusieurs essais pour se terminer avec succès.Pendant la période de réessai, le joueur rejoint le serveur B.
  2. Le serveur A fait trop de UpdateAsync() appels à la même clé et se voit ralenti.La demande de sauvegarde finale est placée dans une file d'attente.Pendant que la demande est dans la file d'attente, le joueur rejoint le serveur B.
  3. Sur le serveur A, un peu de code connecté à l'événement PlayerRemoving est généré 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.
  4. Les performances du serveur A se sont détériorées au point que la sauvegarde finale est retardée jusqu'à ce que le joueur rejoigne le serveur B.

Ces scénarios devraient être rares, mais ils se survernir, notamment dans les situations où un joueur se déconnecte d'un serveur et se connecte à un autre en succession rapide (par exemple, en se téléportant).Certains utilisateurs malveillants pourraient même essayer d'abuser de ce comportement pour terminer des actions sans qu'ils persistent.Cela peut être particulièrement impactant dans les jeux qui permettent aux joueurs d'échanger et est une source commune d'exploits de duplication d'objets.

La verrouillage de session résout cette vulnérabilité en garantissant que lorsque la clé DataStore du joueur est lue en premier par le serveur, le serveur écrit atomiquement une verrouillage dans les métadonnées de la clé dans le même appel UpdateAsync().Si cette valeur de verrouillage est présente lorsque tout autre serveur essaie de lire ou d'écrire la clé, le serveur ne procède pas.

Approche

An process diagram illustrating the session locking system

SessionLockedDataStoreWrapper est un métapackage autour de la classe DataStoreWrapper. fournit une fonctionnalité de file d'attente et de réessai, qui est complétée par le verrouillage de session.

SessionLockedDataStoreWrapper passe chaque demande DataStore requête—indépendamment du fait qu'il s'agisse de GetAsync , SetAsync ou UpdateAsync —par le biais de UpdateAsync .C'est parce que UpdateAsync permet à une clé d'être à la fois lue et écrite à l'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 transmise dans UpdateAsync pour chaque demande effectue les opérations suivantes :

  1. Vérifie que la clé est sûre d'accès, abandonnant l'opération si elle ne l'est pas. "Sûre d'accès" signifie :

    • L'objet de métadonnées de la clé n'inclut pas une valeur non reconnue LockId qui a été mise à jour pour la dernière fois moins de temps avant l'expiration du verrouillage.Cela explique le respect d'une verrouillage placé par un autre serveur et l'ignorance de ce verrouillage si il expire.

    • Si ce serveur a placé sa propre valeur LockId dans les métadonnées de la clé précédemment, cette valeur est toujours dans les métadonnées de la clé.Cela explique la situation où un autre serveur a pris la verrouillage de ce serveur (par expiration ou par force) et l'a libéré plus tard.Alternativement formulé, même si LockId est nil, un autre serveur aurait pu remplacer et supprimer une verrouillage dans le temps écoulé depuis que 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 en function(value) return value end.

  3. En fonction des paramètres transmis dans la demande, 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é en tant que 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à une verrouillage sur cette clé, il n'apporte aucune modification.Il planifie également une tâche pour vous avertir si vous n'accédez pas à nouveau à la clé pour maintenir le verrou dans le temps d'expiration du verrou.

    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é dans le sous-jacent DataStoreWrapper afin que l'opération soit réessayée si elle a été interrompue à l'étape 1 en raison du verrouillage de la session.

Un message d'erreur personnalisé est également renvoyé au consommateur, permettant au système de données du joueur de signaler une erreur alternative en cas de verrouillage de session au client.

Avertissements

Le régime de verrouillage de session repose sur un serveur qui libère toujours son verrou sur une clé lorsqu'il en a fini avec elle.Cela devrait toujours se produire par l'intermédiaire d'une instruction pour déverrouiller la clé en tant qu'élément 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 dans la logique ou d'un bug similaire, l'instruction pour déverrouiller la clé n'a pas été faite.

Pour maintenir la verrouillage sur une clé, vous devez y accéder régulièrement aussi longtemps qu'elle est chargée en mémoire.Cela se ferait normalement en tant qu'élément de la boucle d'enregistrement automatique en arrière-plan dans la plupart des systèmes de données du joueur, 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 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 établisse un nouveau verrou.

Traitement des produits développeur

Singleton : ReceiptHandler

Arrière-plan

Le rappel ProcessReceipt effectue le travail critique de déterminer quand finaliser un acheter.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 la « manipulation » d'un achat puisse varier entre les expériences, nous utilisons les critères suivants

  1. L'achat n'a pas été traité précédemment.

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

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

    Chaque acheter, même les consommables à usage unique, devrait être reflété dans le DataStore afin que l'historique d'achat des utilisateurs soit inclus avec leurs données de session.

Cela nécessite d'effectuer les opérations suivantes avant de retourner PurchaseGranted :

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

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

  • Les données du joueur en mémoire sur le serveur actuel étant potentiellement obsolètes, vous devez récupérer la dernière valeur du DataStore avant de vérifier l'historique PurchaseId
  • Le rappel pour le même achat s'exécutant sur un autre serveur, vous obligeant à lire et à écrire l'historique PurchaseId et à enregistrer les données du joueur mis à jour avec l'achat reflété atomiquement pour empêcher les conditions de course

Le verrouillage de session garantit que, si une tentative d'écrire dans le DataStore du joueur a réussi, aucun autre serveur n'a lu ou écrit avec succès dans le DataStore du joueur entre les données chargées et enregistrées sur ce serveur.En bref, les données du joueur en mémoire sur ce serveur sont la version la plus récente disponible.Il y a quelques avertissements, mais ils n'impactent pas ce comportement.

Approche

Les commentaires dans ReceiptProcessor définissent l'approche :

  1. Vérifiez que les données du joueur sont actuellement chargées sur ce serveur et qu'elles se sont chargées sans erreur.

    Comme ce système utilise la verrouillage de session, cette vérification vérifie également que les données en mémoire sont la version la plus récente.

    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 charger.Le système écoute également le joueur quittant le jeu avant que ses données ne soient chargées, car il ne devrait pas être rendu indéfiniment et bloquer à nouveau cet appel de rappel sur ce serveur pour cet achat si le joueur se reconnecte.

  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, l'array de PurchaseIds du système dans la mémoire est la version la plus récente.Si le PurchaseId est enregistré comme traité et reflété dans une valeur qui a été chargée ou enregistrée dans le DataStore , retournez PurchaseGranted .Si elle est enregistrée comme traitée, mais non reflétée dans le DataStore , retournez NotProcessedYet .

  3. Mise à jour des données du joueur localement sur ce serveur pour "récompenser" l'acheter.

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

  4. Mise à jour des données du joueur localement sur ce serveur pour stocker le PurchaseId .

  5. Envoyez une demande pour enregistrer les données en mémoire dans le DataStore, en retournant PurchaseGranted si la demande est réussie. Sinon, retournez NotProcessedYet .

    Si cette demande de sauvegarde n'a pas réussi, une demande de sauvegarde ultérieure des données de session mémorielles du joueur pourrait encore réussir.Pendant le prochain appel ProcessReceipt , l'étape 2 gère cette situation et renvoie PurchaseGranted .

Données du joueur

Singletons : PlayerData.Server , PlayerData.Client

Arrière-plan

Les modules qui fournissent une interface pour le code du jeu pour lire et écrire les données de session du joueur de manière synchronisée sont courants dans les expériences Roblox.Cette section couvre PlayerData.Server et PlayerData.Client.

Approche

PlayerData.Server et PlayerData.Client gèrent ce qui suivre:

  1. Chargement des données du joueur dans la mémoire, y compris la gestion des cas dans lesquels il échoue à se charger
  2. Fournir une interface pour le code du serveur pour interroger et modifier les données du joueur
  3. Repliquer les modifications des données du joueur au client afin que le code du client puisse y accéder
  4. Replication des erreurs de chargement et/ou d'enregistrement vers le client afin qu'il puisse afficher des dialogues d'erreur
  5. Enregistrement des données du joueur périodiquement, lorsque le joueur part et lorsque le serveur s'arrête

Charger les données du joueur

An process diagram illustrating the loading system
  1. SessionLockedDataStoreWrapper fait une demande getAsync à la boutiquede données.

    Si cette demande échoue, les données par défaut sont utilisées et le profil est marqué comme « échoué » pour garantir qu'il ne soit pas écrit dans le magasin de données plus tard.

    Une option alternative consiste à expulser 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 produit plutôt que de les retirer de l'expérience.

  2. Un premier chargement est envoyé à PlayerDataClient contenant les données chargées et le statut d'erreur (le cas échéant).

  3. Tous les fils obtenus en utilisant waitForDataLoadAsync pour le joueur sont repris.

Fournir une interface pour le code du 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 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 synchronique sans céder.
  • Les méthodes hasLoaded et waitForDataLoadAsync sont disponibles pour garantir que les données ont été chargées avant que vous y accédiez.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 de devoir vérifier les erreurs de chargement avant chaque interaction avec les données sur le client.
  • Une méthode hasErrored peut demander si le chargement initial du joueur a échoué, ce qui lui fait utiliser des données par défaut.Vérifiez cette méthode avant d'autoriser le joueur à effectuer des achats, car les achats ne peuvent pas être enregistrés dans des données sans chargerréussi.
  • Un signal A playerDataUpdated se déclenche avec le A player , le A key et le A value chaque fois que les données d'un joueur sont modifiées.Les systèmes individuels peuvent s'abonner à cela.

Repliquer les modifications au client

  • Tout changement des 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 indiquer les clés qui ne doivent pas être envoyées au client
  • PlayerDataClient inclut une méthode pour obtenir la valeur d'une clé (obtenir) et un signal qui se déclenche lorsqu'il est mis à jour (mis à jour).Une méthode hasLoaded et un signal loaded sont également inclus, afin que le client puisse attendre le chargement et la réplication des données 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

Repliquer les erreurs au client

  • Les états d'erreur rencontrés lors de l'enregistrement ou du chargement des données du joueur sont répliqués à PlayerDataClient .
  • Accédez à cette information avec les méthodes getLoadError et getSaveError ainsi que les signaux loaded et saved.
  • Il y a 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 invitations d'achat du client et mettre en œuvre des dialogues d'avertissement. Cette image montre un exemple de dialogue :
A screenshot of an example warning that could be shown when player data fails to load

Enregistrer les données du joueur

A process diagram illustrating the saving system
  1. Lorsque le joueur quitte le jeu, le système effectue les étapes suivantes :

    1. Vérifiez si il est sûr d'écrire les données du joueur dans le boutiquede données.Les scénarios où il serait dangereux incluent les données du joueur qui ne se chargent pas ou sont encore en cours de chargement.
    2. Faites une demande via le SessionLockedDataStoreWrapper pour écrire la valeur actuelle des données en mémoire dans le magasin de données et supprimer le verrouillage 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. Sur une boucle périodique, le serveur écrit les données de chaque joueur dans le stock de données (sauf si c'est sûr de les enregistrer).Cette rédundance de bienvenue atténue la perte en cas d'écrasement du serveur et est également nécessaire pour maintenir la verrouillage de session.

  3. Lorsqu'une demande de fermeture du serveur est reçue, ce qui suit se produit dans un rappel BindToClose :

    1. Une demande est faite pour enregistrer les données de chaque joueur sur le serveur, suivant le processus normalement traversé lorsqu'un joueur quitte le serveur.Ces demandes sont faites en parallèle, car les rappels BindToClose n'ont que 30 secondes pour être terminés.
    2. Pour accélérer les sauvegardes, toutes les autres demandes dans la file d'attente de chaque clé sont effacées de la base DataStoreWrapper (voir Réessayez).
    3. Le rappel ne retourne pas jusqu'à ce que toutes les demandes aient été traitées.