Implementierung von Spielerdaten- und Einkaufssystemen

*Dieser Inhalt wurde mit KI (Beta) übersetzt und kann Fehler enthalten. Um diese Seite auf Englisch zu sehen, klicke hier.

Hintergrund

Roblox bietet eine Reihe von APIs, um über DataStoreService auf Datenspeicher zuzugreifen. Der häufigste Anwendungsfall für diese APIs besteht darin, Spielerdaten zu speichern, zu laden und zu replizieren. Das sind Daten, die mit dem Fortschritt des Spielers, Käufen und anderen Sitzungsmerkmalen verbunden sind, die zwischen einzelnen Spielsitzungen bestehen bleibt.

Die meisten Spiele auf Roblox verwenden diese APIs, um eine Art von Spielerdaten-System zu implementieren. Diese Implementierungen unterscheiden sich in ihrem Ansatz, zielen jedoch im Allgemeinen darauf ab, dieselben Probleme zu lösen.

Häufige Probleme

Im Folgenden sind einige der häufigsten Probleme aufgeführt, die Spielerdaten-Systeme zu lösen versuchen:

  • Zugriff im Speicher: Anfragen an DataStoreService tätigen Webanfragen, die asynchron arbeiten und einer Ratebegrenzung unterliegen. Dies ist für einen initialen Zugriff zu Beginn der Sitzung geeignet, jedoch nicht für häufige Lese- und Schreibvorgänge im normalen Spielverlauf. Die Spielerdaten-Systeme der meisten Entwickler speichern diese Daten im Speicher auf dem Roblox-Server und begrenzen die Anfragen an DataStoreService auf die folgenden Szenarien:

    • Erstlesung zu Beginn einer Sitzung
    • Letzte Speicherung am Ende der Sitzung
    • Regelmäßige Speicherung in einem Intervall, um das Szenario zu mildern, in dem die letzte Speicherung fehlschlägt
    • Speichervorgänge, um sicherzustellen, dass Daten während der Verarbeitung eines Kaufs gespeichert werden
  • Effiziente Speicherung: Das Speichern aller Sitzung Daten eines Spielers in einer einzigen Tabelle ermöglicht es, mehrere Werte atomar zu aktualisieren und die gleiche Menge an Daten mit weniger Anfragen zu verarbeiten. Dies verringert auch das Risiko einer Desynchronisation zwischen Werten und erleichtert Rollbacks.

    Einige Entwickler implementieren auch eine benutzerdefinierte Serialisierung, um große Datenstrukturen zu komprimieren (häufig, um benutzergenerierte Inhalte im Spiel zu speichern).

  • Replikation: Der Client benötigt regelmäßigen Zugriff auf die Daten eines Spielers (zum Beispiel, um die Benutzeroberfläche zu aktualisieren). Ein generischer Ansatz zur Replikation von Spieldaten zum Client ermöglicht es, diese Informationen zu übertragen, ohne maßgeschneiderte Replikationssysteme für jede Datenkomponente zu erstellen. Entwickler möchten oft die Möglichkeit haben, selektiv zu entscheiden, was und was nicht an den Client replikatiert wird.

  • Fehlerbehandlung: Wenn auf Datenstores nicht zugegriffen werden kann, implementiert die Mehrzahl der Lösungen einen Wiederholungsmechanismus und einen Fallback auf 'Standard'-Daten. Besondere Sorgfalt ist erforderlich, um sicherzustellen, dass Fallback-Daten später nicht 'echte' Daten überschreiben und dass dies dem Spieler angemessen kommuniziert wird.

  • Wiederholungen: Wenn Datenstores nicht erreichbar sind, implementieren die meisten Lösungen einen Wiederholungsmechanismus und einen Fallback auf Standarddaten. Besondere Sorgfalt ist darauf zu legen, dass Fallback-Daten später nicht "echte" Daten überschreiben und die Situation dem Spieler entsprechend kommuniziert wird.

  • Sitzungssperre: Wenn die Daten eines einzelnen Spielers auf mehreren Servern geladen und im Speicher behalten werden, können Probleme auftreten, bei denen ein Server veraltete Informationen speichert. Dies kann zu Datenverlust und häufigen Schlupflöchern bei der Duplizierung von Gegenständen führen.

  • Atomare Kaufabwicklung: Verifizieren, belohnen und protokollieren Sie Käufe atomar, um zu verhindern, dass Gegenstände verloren gehen oder mehrfach vergeben werden.

Beispielcode

Roblox hat Referenzcode, um Ihnen beim Entwerfen und Erstellen von Spielerdaten-Systemen zu helfen. Der Rest dieser Seite behandelt Hintergrundinformationen, Umsetzungsdetails und allgemeine Warnhinweise.


Nachdem Sie das Modell in Studio importiert haben, sollten Sie die folgende Ordnerstruktur sehen:

Explorer-Fenster, das das Einkaufsystem-Modell zeigt.

Architektur

Dieses hochrangige Diagramm zeigt die wichtigsten Systeme im Beispiel und wie sie mit Code im Rest des Spiels interagieren.

Ein Architekturschema für den Beispielcode.

Wiederholungen

Klasse: DataStoreWrapper

Hintergrund

Da DataStoreService Webanfragen im Hintergrund durchführt, sind seine Anfragen nicht garantiert, erfolgreich zu sein. Wenn dies der Fall ist, werfen die Methoden DataStore Fehler, sodass Sie diese behandeln können.

Ein häufiges "Problem" kann auftreten, wenn Sie versuchen, mit Datenstore-Fehlern wie folgt umzugehen:


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

Während dies ein perfekter Wiederholungsmechanismus für eine generische Funktion ist, ist es nicht geeignet für Anfragen an DataStoreService, da es die Reihenfolge, in der Anfragen gestellt werden, nicht garantiert. Die Beibehaltung der Reihenfolge von Anfragen ist wichtig für Anfragen an DataStoreService, da sie mit dem Zustand interagieren. Betrachten Sie das folgende Szenario:

  1. Anfrage A wird gestellt, um den Wert des Schlüssels K auf 1 zu setzen.
  2. Die Anfrage schlägt fehl, sodass ein erneuter Versuch in 2 Sekunden geplant wird.
  3. Bevor der Wiederholungsversuch erfolgt, setzt Anfrage B den Wert von K auf 2, aber der Wiederholungsversuch von Anfrage A überschreibt diesen Wert sofort und setzt K auf 1.

Selbst wenn UpdateAsync auf der neuesten Version des Wertes des Schlüssels arbeitet, müssen UpdateAsync-Anfragen dennoch in der Reihenfolge bearbeitet werden, um ungültige vorübergehende Zustände zu vermeiden (z. B. zieht ein Kauf Münzen ab, bevor eine Münzaddition verarbeitet wird, was zu negativen Münzen führt).

Unser Spielerdaten-System verwendet eine neue Klasse, DataStoreWrapper, die ziehende Wiederholungen bereitstellt, die garantiert in der Reihenfolge pro Schlüssel verarbeitet werden.

Ansatz

Ein Prozessdiagramm, das das Wiederholungssystem veranschaulicht

DataStoreWrapper bietet Methoden, die den DataStore-Methoden entsprechen: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() und DataStore:RemoveAsync().

Diese Methoden, wenn sie aufgerufen werden:

  1. Fügen die Anfrage einer Warteschlange hinzu. Jeder Schlüssel hat seine eigene Warteschlange, in der die Anfragen in der Reihenfolge und nacheinander verarbeitet werden. Der anfordernde Thread gibt nach, bis die Anfrage abgeschlossen ist.

    Diese Funktionalität basiert auf der ThreadQueue-Klasse, die einen coroutine-basierten Task-Planer und Ratenlimiter darstellt. Anstelle von Promises gibt ThreadQueue den aktuellen Thread auf, bis die Operation abgeschlossen ist, und wirft einen Fehler, wenn sie fehlschlägt. Dies ist konsistenter mit idiomatischen asynchronen Luau-Mustern.

  2. Wenn eine Anfrage fehlschlägt, wird sie mit einem konfigurierbaren exponentiellen Backoff wiederholt. Diese Wiederholungen sind Teil des Callbacks, das der ThreadQueue übergeben wird, sodass sie garantiert abgeschlossen sind, bevor die nächste Anfrage in der Warteschlange für diesen Schlüssel gestartet wird.

  3. Wenn eine Anfrage abgeschlossen ist, gibt die Anfragemethode das Muster success, result zurück.

DataStoreWrapper stellt auch Methoden zur Verfügung, um die Warteschlangenlänge für einen bestimmten Schlüssel zu erhalten und veraltete Anforderungen zu löschen. Letzteres ist besonders nützlich in Szenarien, in denen der Server heruntergefahren wird und keine Zeit bleibt, um andere Anfragen außer den aktuellsten zu verarbeiten.

Warnhinweise

DataStoreWrapper folgt dem Prinzip, dass, abgesehen von extremen Szenarien, jeder Datenstore-Anfrage erlaubt sein sollte, abgeschlossen zu werden (erfolgreich oder nicht), selbst wenn eine aktuellere Anfrage sie überflüssig macht. Wenn eine neue Anfrage auftritt, werden veraltete Anfragen nicht aus der Warteschlange entfernt, sondern dürfen abgeschlossen werden, bevor die neue Anfrage gestartet wird. Die Begründung dafür liegt darin, dass dieses Modul als generische Datenbanknutzung und nicht als spezifisches Werkzeug für Spielerdaten anwendbar ist, und ist wie folgt:

  1. Es ist schwierig, eine intuitive Regel für den sicheren Entfernung einer Anfrage aus der Warteschlange zu entscheiden. Betrachten Sie die folgende Warteschlange:

    Wert=0, SetAsync(1), GetAsync(), SetAsync(2)

    Das erwartete Verhalten ist, dass GetAsync() 1 zurückgibt, aber wenn wir die SetAsync()-Anfrage aus der Warteschlange entfernen, weil sie durch die aktuellste überflüssig gemacht wurde, würde sie 0 zurückgeben.

    Der logische Fortschritt besteht darin, dass, wenn eine neue Schreibanfrage hinzugefügt wird, nur so viele veraltete Anfragen zurückgenommen werden, wie bis zur letzten Leseanfrage. UpdateAsync() , die bei weitem häufigste Operation (und die einzige, die von diesem System verwendet wird), kann sowohl lesen als auch schreiben, sodass es schwierig wäre, dies innerhalb dieses Entwurfs ohne zusätzliche Komplexität zu lösen.

    DataStoreWrapper könnte von Ihnen verlangen, anzugeben, ob eine UpdateAsync()-Anfrage das Lesen und/oder Schreiben erlaubt hätte, aber das wäre für unser Spielerdaten-System nicht anwendbar, da dies im Voraus aufgrund des Sitzungssperrmechanismus (der später detaillierter behandelt wird) nicht bestimmt werden kann.

  2. Sobald eine Anfrage aus der Warteschlange entfernt wurde, ist es schwierig, eine intuitive Regel dafür zu entscheiden, wie dies zu geschehen hat. Wenn eine DataStoreWrapper-Anfrage gestellt wird, wird der aktuelle Thread bis zu ihrer Vollziehung ausgegeben. Wenn wir veraltete Anfragen aus der Warteschlange entfernen, müssten wir entscheiden, ob wir false, "Aus der Warteschlange entfernt" zurückgeben oder niemals zurückgeben und den aktiven Thread verwerfen. Beide Ansätze haben ihre eigenen Nachteile und belasten die Komplexität für den Verbraucher.

Letztendlich ist unsere Sichtweise, dass der einfache Ansatz (jede Anfrage zu bearbeiten) hier vorzuziehen ist und eine klarere Umgebung schafft, die es zu navigieren gilt, wenn es um komplexe Probleme wie Sitzungssperren geht. Die einzige Ausnahme bilden DataModel:BindToClose(), bei der das Löschen der Warteschlangen notwendig wird, um die Daten aller Benutzer rechtzeitig zu speichern, wobei die Werte, die die einzelnen Funktionsaufrufe zurückgeben, nicht mehr zu einem fortlaufenden Anliegen werden. Um dies zu berücksichtigen, führen wir eine skipAllQueuesToLastEnqueued-Methode ein. Für weitere Informationen siehe Spielerdaten.

Sitzungssperre

Klasse: SessionLockedDataStoreWrapper

Hintergrund

Spielerdaten werden im Speicher auf dem Server gespeichert und nur dann aus den zugrunde liegenden Datenpeichern gelesen und geschrieben, wenn es notwendig ist. Sie können Spielerdaten im Speicher sofort lesen und aktualisieren, ohne Webanfragen tätigen zu müssen, und so die Grenzen von DataStoreService umgehen.

Damit dieses Modell wie vorgesehen funktioniert, ist es zwingend erforderlich, dass nicht mehr als ein Server die Daten eines Spielers gleichzeitig in den Speicher lädt.

Wenn beispielsweise Server A die Daten eines Spielers lädt, kann Server B diese Daten nicht laden, bis Server A die Sperre während einer letzten Speicherung freigibt. Ohne einen Sperrmechanismus könnte Server B veraltete Spielerdaten aus dem Datenstore laden, bevor Server A die Gelegenheit hat, die aktuellere Version, die es im Speicher hat, zu speichern. Wenn Server A seine neueren Daten speichert, nachdem Server B die veralteten Daten geladen hat, würde Server B diese neueren Daten bei seiner nächsten Speicherung überschreiben.

Obwohl Roblox es nur einem Client erlaubt, zeitgleich mit einem Server verbunden zu sein, kann man nicht davon ausgehen, dass die Daten aus einer Sitzung immer gespeichert werden, bevor die nächste Sitzung beginnt. Betrachten Sie die folgenden Szenarien, die auftreten können, wenn ein Spieler Server A verlässt:

  1. Server A führt eine DataStore-Anfrage aus, um die Daten zu speichern, aber die Anfrage schlägt fehl und erfordert mehrere Wiederholungen, um erfolgreich abzuschließen. Während der Wiederholungsphase betritt der Spieler Server B.
  2. Server A führt zu viele UpdateAsync()-Aufrufe für denselben Schlüssel aus und wird throttled. Die letzte Speicheranfrage wird in eine Warteschlange gesetzt. Während die Anfrage sich in der Warteschlange befindet, betritt der Spieler Server B.
  3. Auf Server A wartet ein mit dem Ereignis PlayerRemoving verbundenes Skript, bis die Daten des Spielers gespeichert sind. Bevor dieser Vorgang abgeschlossen ist, betritt der Spieler Server B.
  4. Die Leistung von Server A hat sich so stark verschlechtert, dass die letzte Speicherung verzögert wird, bis der Spieler Server B betritt.

Diese Szenarien sollten selten sein, treten jedoch auf, insbesondere in Situationen, in denen ein Spieler von einem Server zu einem anderen in schneller Folge wechselt (z.B. beim Teleportieren). Einige bösartige Benutzer könnten sogar versuchen, dieses Verhalten auszunutzen, um Aktionen ohne dauerhafte Auswirkungen durchzuführen. Dies kann besonders in Spielen von Bedeutung sein, in denen Spieler handeln können, und ist eine häufige Quelle für Exploits zur Duplizierung von Gegenständen.

Sitzungssperre adressiert diese Verwundbarkeit, indem sichergestellt wird, dass, wenn der Schlüssel DataStore eines Spielers zuerst vom Server gelesen wird, der Server atomar eine Sperre in den Metadaten des Schlüssels im selben Aufruf von UpdateAsync() schreibt. Wenn dieser Sperrwert vorhanden ist, wenn ein anderer Server versucht, den Schlüssel zu lesen oder zu schreiben, fährt der Server nicht fort.

Ansatz

Ein Prozessdiagramm, das das Sitzungssperrensystem veranschaulicht

SessionLockedDataStoreWrapper ist ein Meta-Wraparound um die Klasse DataStoreWrapper. DataStoreWrapper bietet Warteschlangen- und Wiederholungsfunktionen, die SessionLockedDataStoreWrapper mit einer Sitzungssperre ergänzt.

SessionLockedDataStoreWrapper leitet jede DataStore-Anfrage - unabhängig davon, ob es sich um GetAsync, SetAsync oder UpdateAsync handelt - durch UpdateAsync. Dies liegt daran, dass UpdateAsync es ermöglicht, einen Schlüssel atomar zu lesen und zu schreiben. Es ist auch möglich, das Schreiben auf der Grundlage des Wertes abzubrechen, der im Transformations-Callback gelesen wurde, indem nil zurückgegeben wird.

Die Transformationsfunktion, die für jede Anfrage in UpdateAsync übergeben wird, führt die folgenden Operationen durch:

  1. Überprüft, ob der Schlüssel sicher zugänglich ist, und bricht die Operation ab, wenn nicht. "Sicher zugänglich" bedeutet:

    • Das Metadatenobjekt des Schlüssels enthält keinen nicht erkannten LockId-Wert, der vor weniger als der Sperrablauffrist zuletzt aktualisiert wurde. Dies berücksichtigt die Löschung eine Sperre durch einen anderen Server und ignoriert diese Sperre, wenn sie abgelaufen ist.

    • Wenn dieser Server zuvor einen eigenen LockId-Wert in den Metadaten des Schlüssels platziert hat, ist dieser Wert weiterhin in den Metadaten des Schlüssels vorhanden. Dies bezieht sich auf die Situation, in der ein anderer Server die Sperre dieses Servers übernommen hat (durch Ablauf oder Zwang) und sie später freigegeben hat. Alternativ formuliert, selbst wenn LockId nil ist, könnte ein anderer Server weiterhin eine Sperre seitdem ersetzt und entfernt haben.

  2. UpdateAsync führt die DataStore-Operation aus, die der Verbraucher von SessionLockedDataStoreWrapper angefordert hat. Zum Beispiel wird GetAsync() in function(value) return value end übersetzt.

  3. Abhängig von den Parametern, die in die Anfrage übergeben werden, sperrt oder entsperrt UpdateAsync den Schlüssel:

    1. Wenn der Schlüssel gesperrt werden soll, setzt UpdateAsync den LockId in den Metadaten des Schlüssels auf eine GUID. Diese GUID wird im Speicher auf dem Server gespeichert, sodass sie beim nächsten Zugriff auf den Schlüssel verifiziert werden kann. Wenn der Server bereits eine Sperre auf diesem Schlüssel hat, nimmt er keine Änderungen vor. Er plant auch eine Aufgabe, um Sie zu warnen, wenn Sie den Schlüssel nicht erneut aufrufen, um die Sperre innerhalb der Ablauffrist der Sperre aufrechtzuerhalten.

    2. Wenn der Schlüssel entsperrt werden soll, entfernt UpdateAsync den LockId in den Metadaten des Schlüssels.

Ein benutzerdefinierter Wiederholungs-Handler wird an den zugrunde liegenden DataStoreWrapper übergeben, sodass die Operation wiederholt wird, wenn sie bei Schritt 1 aufgrund der Sitzungssperre abgebrochen wurde.

Eine benutzerdefinierte Fehlermeldung wird ebenfalls zurückgegeben, damit das Spielerdaten-System im Falle einer Sitzungssperre eine alternative Fehlermeldung an den Client melden kann.

Warnhinweise

Das Sitzungssperrensystem verlässt sich darauf, dass ein Server seine Sperre auf einen Schlüssel immer löst, wenn er mit ihm fertig ist. Dies sollte immer durch eine Anweisung zum Entsperren des Schlüssels im Rahmen der letzten Speicherung in PlayerRemoving oder BindToClose() erfolgen.

Die Entsperrung kann jedoch in bestimmten Situationen scheitern. Zum Beispiel:

  • Der Server ist abgestürzt oder DataStoreService war für alle Versuche, auf den Schlüssel zuzugreifen, nicht verfügbar.
  • Aufgrund eines logischen Fehlers oder eines ähnlichen Fehlers wurde die Anweisung zur Entsperrung des Schlüssels nicht erteilt.

Um die Sperre auf einem Schlüssel aufrechtzuerhalten, müssen Sie ihn regelmäßig aufrufen, solange er im Speicher geladen ist. Dies würde normalerweise im Rahmen der automatischen Speicher-Schleife geschehen, die im Hintergrund in den meisten Spielerdaten-Systemen ausgeführt wird, aber dieses System stellt auch eine Methode refreshLockAsync zur Verfügung, falls Sie dies manuell durchführen müssen.

Wenn die Ablauffrist der Sperre überschritten wurde, ohne dass die Sperre aktualisiert wurde, kann jeder Server die Sperre übernehmen. Wenn ein anderer Server die Sperre übernimmt, schlagen die Versuche des aktuellen Servers, den Schlüssel zu lesen oder zu schreiben, fehl, es sei denn, er etabliert eine neue Sperre.

Verarbeitung von Entwicklerprodukten

Singleton: ReceiptHandler

Hintergrund

Der Callback ProcessReceipt erfüllt die entscheidende Aufgabe, zu bestimmen, wann ein Kauf finalisiert werden soll. ProcessReceipt wird in sehr spezifischen Szenarien aufgerufen. Für die Set von Garantien siehe MarketplaceService.ProcessReceipt.

Obwohl die Definition von "Handhabung" eines Kaufs zwischen Spielen variieren kann, verwenden wir die folgenden Kriterien:

  1. Der Kauf wurde nicht zuvor bearbeitet.

  2. Der Kauf wird in der aktuellen Sitzung reflektiert.

  3. Der Kauf wurde in einem DataStore gespeichert.

    Jeder Kauf, selbst einmalige Verbrauchsartikel, sollte im DataStore reflektiert werden, sodass die Kaufhistorie der Nutzer in ihren Sitzungsdaten enthalten ist.

Dafür sind die folgenden Operationen erforderlich, bevor PurchaseGranted zurückgegeben wird:

  1. Überprüfen Sie, ob die PurchaseId bereits als bearbeitet vermerkt ist.
  2. Belohnen Sie den Kauf in den im Speicher gehaltenen Spieldaten des Spielers.
  3. Protokollieren Sie die PurchaseId als bearbeitet in den im Speicher gehaltenen Spieldaten des Spielers.
  4. Schreiben Sie die im Speicher gehaltenen Spieldaten des Spielers in den DataStore.

Die Sitzungssperre vereinfacht diesen Ablauf, da Sie sich nicht mehr um die folgenden Szenarien kümmern müssen:

  • Die im Speicher gehaltenen Spieldaten auf dem aktuellen Server sind möglicherweise veraltet, sodass Sie den neuesten Wert aus dem DataStore abrufen müssen, bevor Sie die Kaufhistorie für die PurchaseId verifizieren.
  • Der Callback für denselben Kauf läuft auf einem anderen Server, was Sie dazu zwingt, sowohl die Kaufhistorie für die PurchaseId zu lesen und zu schreiben und die aktualisierten Spieldaten atomar mit dem Kauf zu speichern, um Rennbedingungen zu verhindern.

Die Sitzungssperre garantiert, dass, wenn ein Versuch, in den DataStore des Spielers zu schreiben, erfolgreich ist, kein anderer Server zwischen dem Laden und Speichern der Daten in diesem Server erfolgreich die DataStore des Spielers gelesen oder geschrieben hat. Kurz gesagt, die im Speicher gehaltenen Spieldaten auf diesem Server sind die aktuellste verfügbare Version. Es gibt einige Warnhinweise, aber diese wirken sich nicht auf dieses Verhalten aus.

Ansatz

Die Kommentare in ReceiptProcessor skizzieren den Ansatz:

  1. Überprüfen Sie, ob die Daten des Spielers derzeit auf diesem Server geladen sind und ohne Fehler geladen wurden.

    Da dieses System die Sitzungssperre nutzt, überprüft diese Überprüfung auch, dass die im Speicher gehaltenen Daten die aktuellste Version sind.

    Wenn die Daten des Spielers noch nicht geladen wurden (was zu erwarten ist, wenn ein Spieler einem Spiel beitritt), warten Sie, bis die Daten des Spielers geladen sind. Das System überwacht auch, ob der Spieler das Spiel verlässt, bevor seine Daten geladen sind, da es nicht unbegrenzt warten soll und diesen Callback nicht erneut auf diesem Server für diesen Kauf blockieren soll, falls der Spieler erneut eintritt.

  2. Überprüfen Sie, ob die PurchaseId nicht bereits als bearbeitet in den Spieldaten verzeichnet ist.

    Aufgrund der Sitzungssperre ist das Array der PurchaseIds, das das System im Speicher hat, die aktuellste Version. Wenn die PurchaseId als bearbeitet vermerkt ist und in einem Wert reflektiert wird, der im DataStore geladen oder gespeichert wurde, geben Sie PurchaseGranted zurück. Wenn sie als bearbeitet vermerkt ist, aber nicht im DataStore reflektiert ist, geben Sie NotProcessedYet zurück.

  3. Aktualisieren Sie die Spieldaten lokal auf diesem Server, um den Kauf "zu vergeben".

    ReceiptProcessor verfolgt einen generischen Callback-Ansatz und weist für jede DeveloperProductId einen anderen Callback zu.

  4. Aktualisieren Sie die Spieldaten lokal auf diesem Server, um die PurchaseId zu speichern.

  5. Reichen Sie eine Anfrage ein, um die im Speicher gehaltenen Daten in den DataStore zu speichern, und geben Sie PurchaseGranted zurück, wenn die Anfrage erfolgreich ist. Andernfalls geben Sie NotProcessedYet zurück.

    Wenn diese Speicheranfrage nicht erfolgreich ist, könnte eine spätere Anfrage, die Spieldaten der Sitzung des Spielers zu speichern, dennoch erfolgreich sein. Bei der nächsten ProcessReceipt-Anruf behandelt Schritt 2 diese Situation und gibt PurchaseGranted zurück.

Spieldaten

Singletons: PlayerData.Server, PlayerData.Client

Hintergrund

Module, die eine Schnittstelle bieten, um synchron auf Spielerdaten zuzugreifen und sie zu schreiben, sind in Roblox-Spielen häufig. Dieser Abschnitt behandelt PlayerData.Server und PlayerData.Client.

Ansatz

PlayerData.Server und PlayerData.Client behandeln Folgendes:

  1. Laden der Spielerdaten in den Speicher, einschließlich der Behandlung von Fällen, in denen das Laden fehlschlägt.
  2. Bereitstellung einer Schnittstelle für Servercode, um die Spieldaten abzufragen und zu ändern.
  3. Replikation von Änderungen in den Spieldaten des Spielers an den Client, damit der Client-Code darauf zugreifen kann.
  4. Replikation von Lade- und/oder Speicherfehlern an den Client, damit er Fehlermeldungsdialoge anzeigen kann.
  5. Periodisches Speichern der Spielerdaten, wenn der Spieler das Spiel verlässt, und während der Server herunterfährt.

Spielerdaten laden

Ein Prozessdiagramm, das das Ladesystem illustriert
  1. SessionLockedDataStoreWrapper macht eine getAsync-Anfrage an den Datenspeicher.

    Wenn diese Anfrage fehlschlägt, werden die Standarddaten verwendet, und das Profil wird als "fehlerhaft" markiert, um sicherzustellen, dass es später nicht in den Datenspeicher geschrieben wird.

    Eine alternative Option besteht darin, den Spieler zu verwarnen, aber wir empfehlen, den Spieler mit Standarddaten spielen zu lassen und klare Informationen darüber zu geben, was passiert ist, anstatt ihn aus dem Spiel zu entfernen.

  2. Eine Initial-Payload wird an PlayerDataClient gesendet, die die geladenen Daten und den Status (bei Vorhandensein) enthält.

  3. Alle Threads, die mit waitForDataLoadAsync für den Spieler gewartet haben, werden fortgesetzt.

Schnittstelle für Servercode bereitstellen

  • PlayerDataServer ist ein Singleton, der von jedem Servercode, der in derselben Umgebung ausgeführt wird, abgerufen und zugegriffen werden kann.

  • Spielerdaten sind in einem Wörterbuch von Schlüsseln und Werten organisiert. Sie können diese Werte auf dem Server mithilfe der Methoden setValue, getValue, updateValue und removeValue manipulieren. Diese Methoden arbeiten alle synchron, ohne zu weichen.

  • Die Methoden hasLoaded und waitForDataLoadAsync sind verfügbar, um sicherzustellen, dass die Daten geladen sind, bevor Sie darauf zugreifen. Wir empfehlen, dies einmal während eines Ladescreens zu tun, bevor andere Systeme gestartet werden, um überprüfen zu müssen, ob ein Ladefehler vorliegt, bevor jede Interaktion mit Daten auf dem Client erfolgt.

  • Eine Methode hasErrored kann abfragen, ob das ursprüngliche Laden des Spielers fehlgeschlagen ist und der Spieler daher mit Standarddaten arbeitet. Überprüfen Sie diese Methode, bevor Sie dem Spieler erlauben, Käufe zu tätigen, da Käufe nicht in Daten gespeichert werden können, ohne ein erfolgreiches Laden.

  • Ein Signal playerDataUpdated wird mit dem player, key und value ausgelöst, sobald die Daten eines Spielers geändert werden. Einzelne Systeme können sich dafür anmelden.

Änderungen an den Client replizieren

  • Jede Änderung der Spieldaten in PlayerDataServer wird an PlayerDataClient repliziert, es sei denn, dieser Schlüssel wurde mit setValueAsPrivate als privat markiert.
    • setValueAsPrivate wird verwendet, um Schlüssel zu kennzeichnen, die nicht an den Client gesendet werden sollen.
  • PlayerDataClient enthält eine Methode, um den Wert eines Schlüssels abzurufen (get), und ein Signal, das ausgelöst wird, wenn es aktualisiert wird (updated). Es sind auch eine Methode hasLoaded und ein Signal loaded enthalten, damit der Client auf das Laden der Daten warten und die Replikation vor dem Start seiner Systeme abwarten kann.
  • PlayerDataClient ist ein Singleton, auf den von jedem Clientcode, der in derselben Umgebung ausgeführt wird, zugegriffen werden kann.

Fehler an den Client replizieren

  • Fehlerstatus, die beim Speichern oder Laden von Spieldaten festgestellt werden, werden an PlayerDataClient weitergegeben.
  • Greifen Sie mit den Methoden getLoadError und getSaveError sowie den Signalen loaded und saved auf diese Informationen zu.
  • Es gibt zwei Arten von Fehlern: DataStoreError (die Anfrage an DataStoreService ist fehlgeschlagen) und SessionLocked (siehe Sitzungssperre).
  • Verwenden Sie diese Ereignisse, um die Kaufaufforderungen für den Client zu deaktivieren und Warnmeldungsdialoge zu implementieren. Dieses Bild zeigt ein Beispiel für einen Dialog:
Ein Screenshot eines Beispielwarnungsdialogs, der angezeigt werden könnte, wenn die Spieldaten des Spielers nicht geladen werden konnten.

Spielerdaten speichern

Ein Prozessdiagramm, das das Speichersystem illustriert
  1. Wenn der Spieler das Spiel verlässt, führt das System die folgenden Schritte aus:

    1. Überprüfen Sie, ob es sicher ist, die Spieldaten des Spielers in den Datenspeicher zu schreiben. Szenarien, in denen es unsicher wäre, sind, wenn die Spieldaten des Spielers nicht geladen wurden oder noch geladen werden.
    2. Stellen Sie eine Anfrage über SessionLockedDataStoreWrapper ein, um den aktuellen im Speicher gehaltenen Datenwert in den Datenspeicher zu schreiben und die Sitzungssperre nach Abschluss zu entfernen.
    3. Löschen Sie die Spieldaten des Spielers (und andere Variablen wie Metadaten und Fehlerstatus) aus dem Serverspeicher.
  2. In einer regelmäßigen Schleife schreibt der Server die Daten jedes Spielers in den Datenspeicher (sofern es sicher ist, sie zu speichern). Diese willkommene Redundanz mildert den Verlust im Falle eines Serverabsturzes und ist auch notwendig, um die Sitzungssperre aufrechtzuerhalten.

  3. Wenn eine Anfrage, den Server herunterzufahren, empfangen wird, geschieht Folgendes in einem BindToClose-Callback:

    1. Eine Anfrage werden gemacht, um die Daten jedes Spielers auf dem Server zu speichern, und sie befolgen den Prozess, der normalerweise durchlaufen wird, wenn ein Spieler den Server verlässt. Diese Anfragen werden parallel gestellt, da die BindToClose-Callbacks nur 30 Sekunden Zeit haben, um abzuschließen.
    2. Um die Speicheroperationen zu beschleunigen, werden alle anderen Anfragen in der Warteschlange jedes Schlüssels aus dem zugrunde liegenden DataStoreWrapper gelöscht (siehe Wiederholungen).
    3. Der Callback gibt erst zurück, wenn alle Aufrufe abgeschlossen sind.
©2026 Roblox Corporation. Roblox, das Roblox-Logo und "Powering Imagination" gehören zu unseren eingetragenen und nicht eingetragenen Markenzeichen in den USA und anderen Ländern.