Hintergrund
Roblox bietet eine Reihe von APIs, um über DataStoreService mit Datenspeichern zu interagieren. 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 bleiben.
Die meisten Spiele auf Roblox verwenden diese APIs, um eine Form eines Spielerdaten-Systems zu implementieren. Diese Implementierungen unterscheiden sich in ihrem Ansatz, zielen jedoch im Allgemeinen darauf ab, die gleichen 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: DataStoreService-Anfragen führen Webanfragen durch, die asynchron arbeiten und Rate-Limits unterliegen. Dies ist für einen ersten Ladevorgang zu Beginn der Sitzung angemessen, jedoch nicht für häufige Lese- und Schreibvorgänge während des normalen Spielverlaufs. Die Spielerdaten-Systeme der meisten Entwickler speichern diese Daten im Speicher auf dem Roblox-Server und beschränken die DataStoreService-Anfragen auf die folgenden Szenarien:
- Erster Lesevorgang zu Beginn einer Sitzung
- Letzter Schreibvorgang am Ende der Sitzung
- Periodische Schreibvorgänge in einem Intervall, um das Szenario zu mildern, in dem der letzte Schreibvorgang fehlschlägt
- Schreibvorgänge, um sicherzustellen, dass Daten während der Verarbeitung eines Kaufs gespeichert werden
Effiziente Speicherung: Das Speichern aller Sitzungsdaten eines Spielers in einer einzigen Tabelle ermöglicht es, mehrere Werte atomar zu aktualisieren und die gleiche Datenmenge mit weniger Anfragen zu verarbeiten. Es beseitigt auch das Risiko der Desynchronisation zwischen Werten und erleichtert Rollbacks.
Einige Entwickler implementieren auch eine benutzerdefinierte Serialisierung, um große Datenstrukturen zu komprimieren (typischerweise, 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 Spielerdaten an den Client ermöglicht es, diese Informationen zu übertragen, ohne maßgeschneiderte Replikationssysteme für jede Datenkomponente erstellen zu müssen. Entwickler möchten oft die Möglichkeit haben, selektiv zu entscheiden, was an den Client repliziert wird und was nicht.
Fehlerbehandlung: Wenn auf Datenspeicher nicht zugegriffen werden kann, implementieren die meisten Lösungen einen Mechanismus zum Wiederholen und einen Fallback auf 'Standard'-Daten. Besondere Sorgfalt ist erforderlich, um sicherzustellen, dass Fallback-Daten später keine 'echten' Daten überschreiben und dass dies dem Spieler angemessen kommuniziert wird.
Wiederholungen: Wenn Datenspeicher nicht zugänglich sind, implementieren die meisten Lösungen einen Mechanismus zum Wiederholen und einen Fallback auf Standarddaten. Achten Sie darauf, dass Fallback-Daten später keine "echten" Daten überschreiben, und kommunizieren Sie die Situation angemessen an den Spieler.
Sitzungssperrung: Wenn die Daten eines einzelnen Spielers auf mehreren Servern geladen und im Speicher sind, 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: Überprüfen, gewähren 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 Hintergrund, Implementierungsdetails und allgemeine Vorbehalte.
Nachdem Sie das Modell in Studio importiert haben, sollten Sie die folgende Ordnerstruktur sehen:

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

Wiederholungen
Klasse: DataStoreWrapper
Hintergrund
Da DataStoreService im Hintergrund Webanfragen durchführt, ist nicht garantiert, dass seine Anfragen erfolgreich sind. Wenn dies geschieht, werfen die Methoden DataStore Fehler, die Sie behandeln können.
Ein häufiges "Problem" kann auftreten, wenn Sie versuchen, Fehler bei Datenspeichern wie folgt zu behandeln:
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
endWiederholen Sie vorübergehende Fehler mit exponentiellem Backoff und zufälligem Jitter, damit Server nicht gleichzeitig wiederholen. Begrenzen Sie die Verzögerung und die Anzahl der Versuche.
Selbst mit diesem Verzögerungsmuster ist dieser Wiederholungsmechanismus für DataStoreService-Anfragen nicht geeignet, da er nicht die Reihenfolge garantiert, in der Anfragen gestellt werden. Die Beibehaltung der Reihenfolge von Anfragen ist wichtig für DataStoreService-Anfragen, da sie mit dem Zustand interagieren. Betrachten Sie das folgende Szenario:
- Anfrage A wird gestellt, um den Wert des Schlüssels K auf 1 zu setzen.
- Die Anfrage schlägt fehl, sodass ein Wiederholungsversuch nach der Backoff-Verzögerung geplant wird.
- 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.
Obwohl UpdateAsync auf der neuesten Version des Wertes des Schlüssels arbeitet, müssen UpdateAsync-Anfragen dennoch in der Reihenfolge verarbeitet werden, um ungültige vorübergehende Zustände zu vermeiden (zum Beispiel, dass ein Kauf Münzen abzieht, bevor eine Münzaddition verarbeitet wird, was zu negativen Münzen führt).
Unser Spielerdaten-System verwendet eine neue Klasse, DataStoreWrapper, die wartende Wiederholungen bereitstellt, die garantiert in der Reihenfolge pro Schlüssel verarbeitet werden.
Ansatz

DataStoreWrapper bietet Methoden, die den Methoden DataStore entsprechen: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() und DataStore:RemoveAsync().
Diese Methoden, wenn sie aufgerufen werden:
Fügen die Anfrage einer Warteschlange hinzu. Jeder Schlüssel hat seine eigene Warteschlange, in der Anfragen in der Reihenfolge und in Serie verarbeitet werden. Der anfordernde Thread wartet, bis die Anfrage abgeschlossen ist.
Diese Funktionalität basiert auf der ThreadQueue-Klasse, die ein coroutine-basiertes Aufgabenplanungs- und Ratenbegrenzungssystem ist. Anstatt ein Versprechen zurückzugeben, wartet ThreadQueue auf den aktuellen Thread, bis die Operation abgeschlossen ist, und wirft einen Fehler, wenn sie fehlschlägt. Dies ist konsistenter mit idiomatischen asynchronen Luau-Mustern.
Wenn eine Anfrage fehlschlägt, wird sie mit einem konfigurierbaren exponentiellen Backoff wiederholt. Diese Wiederholungen sind Teil des Rückrufs, der an die ThreadQueue übergeben wird, sodass sie garantiert abgeschlossen werden, bevor die nächste Anfrage in der Warteschlange für diesen Schlüssel beginnt.
Wenn eine Anfrage abgeschlossen ist, gibt die Anfrage-Methode das Muster success, result zurück.
DataStoreWrapper bietet auch Methoden, um die Warteschlangenlänge für einen bestimmten Schlüssel abzurufen und veraltete Anfragen zu löschen. Letztere Option ist besonders nützlich in Szenarien, in denen der Server heruntergefahren wird und keine Zeit bleibt, um mehr als die aktuellsten Anfragen zu verarbeiten.
Vorbehalte
DataStoreWrapper folgt dem Prinzip, dass außerhalb extremer Szenarien jede Datenspeicheranfrage abgeschlossen werden sollte (erfolgreich oder nicht), selbst wenn eine neuere Anfrage sie überflüssig macht. Wenn eine neue Anfrage erfolgt, werden veraltete Anfragen nicht aus der Warteschlange entfernt, sondern dürfen abgeschlossen werden, bevor die neue Anfrage gestartet wird. Die Begründung dafür ist in der Anwendbarkeit dieses Moduls als generisches Datenspeicher-Utility und nicht als spezifisches Werkzeug für Spielerdaten verwurzelt und lautet wie folgt:
Es ist schwierig, eine intuitive Regelmenge zu entscheiden, wann eine Anfrage sicher aus der Warteschlange entfernt werden kann. Betrachten Sie die folgende Warteschlange:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Das erwartete Verhalten ist, dass GetAsync() 1 zurückgibt, aber wenn wir die SetAsync()-Anfrage aufgrund ihrer Überflüssigkeit durch die neueste entfernen, würde sie 0 zurückgeben.
Der logische Fortschritt besteht darin, dass, wenn eine neue Schreibanfrage hinzugefügt wird, nur veraltete Anfragen bis zur letzten Leseanfrage entfernt werden. 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 Designs zu reconciliieren, ohne zusätzliche Komplexität hinzuzufügen.
DataStoreWrapper könnte erfordern, dass Sie angeben, ob eine UpdateAsync()-Anfrage lesen und/oder schreiben durfte, aber dies hätte keine Anwendbarkeit für unser Spielerdaten-System, wo dies aufgrund des Sitzungssperrmechanismus (der später ausführlicher behandelt wird) nicht im Voraus bestimmt werden kann.
Sobald sie aus der Warteschlange entfernt wurden, ist es schwierig, eine intuitive Regel zu entscheiden, wie dies behandelt werden sollte. Wenn eine DataStoreWrapper-Anfrage gestellt wird, wird der aktuelle Thread bis zu ihrem Abschluss gewartet. 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 laden zusätzliche Komplexität auf den Verbraucher.
Letztendlich ist unsere Ansicht, dass der einfache Ansatz (jede Anfrage zu verarbeiten) hier vorzuziehen ist und eine klarere Umgebung schafft, um komplexe Probleme wie Sitzungssperrung anzugehen. Die einzige Ausnahme hiervon ist während DataModel:BindToClose(), wo das Löschen der Warteschlange notwendig wird, um die Daten aller Benutzer rechtzeitig zu speichern, und der Wert, den einzelne Funktionsaufrufe zurückgeben, kein fortlaufendes Anliegen mehr ist. Um dies zu berücksichtigen, stellen wir eine Methode skipAllQueuesToLastEnqueued zur Verfügung. Für weitere Informationen siehe Spielerdaten.
Sitzungssperrung
Klasse: SessionLockedDataStoreWrapper
Hintergrund
Spielerdaten werden im Speicher auf dem Server gespeichert und nur dann aus den zugrunde liegenden Datenspeichern gelesen und geschrieben, wenn es notwendig ist. Sie können die im Speicher befindlichen Spielerdaten sofort lesen und aktualisieren, ohne Webanfragen durchführen zu müssen, und vermeiden, die Limits von DataStoreService zu überschreiten.
Damit dieses Modell wie beabsichtigt funktioniert, ist es unerlässlich, dass nicht mehr als ein Server die Daten eines Spielers gleichzeitig aus dem DataStore in den Speicher laden kann.
Wenn beispielsweise Server A die Daten eines Spielers lädt, kann Server B diese Daten nicht laden, bis Server A seine Sperre während eines letzten Speichervorgangs freigibt. Ohne einen Sperrmechanismus könnte Server B veraltete Spielerdaten aus dem Datenspeicher laden, bevor Server A die neuere Version, die es im Speicher hat, speichern kann. Wenn Server A seine neueren Daten speichert, nachdem Server B die veralteten Daten geladen hat, würde Server B diese neueren Daten während seines nächsten Speichervorgangs überschreiben.
Obwohl Roblox nur zulässt, dass ein Client zu einem Zeitpunkt mit einem Server verbunden ist, können Sie 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:
- Server A führt eine DataStore-Anfrage durch, um seine Daten zu speichern, aber die Anfrage schlägt fehl und erfordert mehrere Wiederholungen, um erfolgreich abzuschließen. Während des Wiederholungszeitraums tritt der Spieler Server B bei.
- Server A führt zu viele UpdateAsync()-Aufrufe für denselben Schlüssel durch und wird gedrosselt. Die letzte Speicheranfrage wird in eine Warteschlange gestellt. Während die Anfrage in der Warteschlange ist, tritt der Spieler Server B bei.
- Auf Server A wartet ein Code, der mit dem Ereignis PlayerRemoving verbunden ist, bevor die Daten des Spielers gespeichert werden. Bevor dieser Vorgang abgeschlossen ist, tritt der Spieler Server B bei.
- Die Leistung von Server A hat sich so weit verschlechtert, dass der letzte Speicher bis nach dem Beitritt des Spielers zu Server B verzögert wird.
Diese Szenarien sollten selten sein, treten jedoch auf, insbesondere in Situationen, in denen ein Spieler schnell von einem Server zu einem anderen wechselt (zum Beispiel beim Teleportieren). Einige böswillige Benutzer könnten sogar versuchen, dieses Verhalten auszunutzen, um Aktionen abzuschließen, ohne dass sie bestehen bleiben. Dies kann besonders in Spielen, die den Spielern den Handel ermöglichen, Auswirkungen haben und ist eine häufige Quelle für Exploits zur Duplizierung von Gegenständen.
Die Sitzungssperrung adressiert diese Verwundbarkeit, indem sichergestellt wird, dass, wenn der Schlüssel des Spielers DataStore zuerst vom Server gelesen wird, der Server atomar einen Lock in den Metadaten des Schlüssels innerhalb des gleichen UpdateAsync()-Aufrufs 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

SessionLockedDataStoreWrapper ist ein Meta-Wrap um die DataStoreWrapper-Klasse. DataStoreWrapper bietet Warteschlangen- und Wiederholungsfunktionen, die SessionLockedDataStoreWrapper mit Sitzungssperrung 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, den Schreibvorgang basierend auf dem gelesenen Wert abzubrechen, indem nil im Transformationsrückruf zurückgegeben wird.
Die Transformationsfunktion, die für jede Anfrage in UpdateAsync übergeben wird, führt die folgenden Operationen aus:
Überprüft, ob der Schlüssel sicher zuzugreifen ist, und bricht die Operation ab, wenn dies nicht der Fall ist. "Sicher zuzugreifen" bedeutet:
Das Metadatenobjekt des Schlüssels enthält keinen nicht erkannten LockId-Wert, der zuletzt vor weniger als der Sperrablaufzeit aktualisiert wurde. Dies berücksichtigt die Beachtung eines von einem anderen Server gesetzten Locks und das Ignorieren dieses Locks, wenn er abgelaufen ist.
Wenn dieser Server zuvor seinen eigenen LockId-Wert in den Metadaten des Schlüssels gesetzt hat, ist dieser Wert weiterhin in den Metadaten des Schlüssels. Dies berücksichtigt die Situation, in der ein anderer Server den Lock dieses Servers übernommen hat (durch Ablauf oder durch Zwang) und ihn später freigegeben hat. Alternativ ausgedrückt, selbst wenn LockId nil ist, könnte ein anderer Server immer noch einen Lock ersetzt und entfernt haben, seit Sie den Schlüssel gesperrt haben.
UpdateAsync führt die DataStore-Operation aus, die der Verbraucher von SessionLockedDataStoreWrapper angefordert hat. Zum Beispiel wird GetAsync() zu function(value) return value end.
Abhängig von den Parametern, die in die Anfrage übergeben werden, sperrt oder entsperrt UpdateAsync den Schlüssel:
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, damit sie beim nächsten Zugriff auf den Schlüssel überprüft werden kann. Wenn der Server bereits einen Lock auf diesen Schlüssel hat, werden keine Änderungen vorgenommen. Es wird auch eine Aufgabe geplant, um Sie zu warnen, wenn Sie den Schlüssel nicht erneut zugreifen, um den Lock innerhalb der Ablaufzeit des Locks aufrechtzuerhalten.
Wenn der Schlüssel entsperrt werden soll, entfernt UpdateAsync den LockId in den Metadaten des Schlüssels.
Ein benutzerdefinierter Wiederholungsbehandler wird in den zugrunde liegenden DataStoreWrapper übergeben, sodass die Operation wiederholt wird, wenn sie in Schritt 1 aufgrund der Sitzungssperrung abgebrochen wurde.
Eine benutzerdefinierte Fehlermeldung wird ebenfalls an den Verbraucher zurückgegeben, sodass das Spielerdaten-System im Falle einer Sitzungssperrung einen alternativen Fehler an den Client melden kann.
Vorbehalte
Das Sitzungssperrsystem ist darauf angewiesen, dass ein Server seinen Lock auf einen Schlüssel immer freigibt, wenn er mit ihm fertig ist. Dies sollte immer durch eine Anweisung zum Entsperren des Schlüssels als Teil des letzten Schreibvorgangs in PlayerRemoving oder BindToClose() geschehen.
Der Unlock kann jedoch in bestimmten Situationen fehlschlagen. Zum Beispiel:
- Der Server ist abgestürzt oder DataStoreService war für alle Versuche, auf den Schlüssel zuzugreifen, nicht funktionsfähig.
- Aufgrund eines Logikfehlers oder eines ähnlichen Fehlers wurde die Anweisung zum Entsperren des Schlüssels nicht erteilt.
Um den Lock auf einem Schlüssel aufrechtzuerhalten, müssen Sie regelmäßig darauf zugreifen, solange er im Speicher geladen ist. Dies würde normalerweise als Teil der automatischen Speicher-Schleife geschehen, die im Hintergrund in den meisten Spielerdaten-Systemen läuft, aber dieses System stellt auch eine Methode refreshLockAsync zur Verfügung, wenn Sie dies manuell tun müssen.
Wenn die Ablaufzeit des Locks überschritten wurde, ohne dass der Lock aktualisiert wurde, kann jeder Server den Lock übernehmen. Wenn ein anderer Server den Lock übernimmt, schlagen Versuche des aktuellen Servers, den Schlüssel zu lesen oder zu schreiben, fehl, es sei denn, er stellt einen neuen Lock her.
Verarbeitung von Entwicklerprodukten
Singleton: ReceiptHandler
Hintergrund
Der ProcessReceipt-Rückruf erfüllt die entscheidende Aufgabe, zu bestimmen, wann ein Kauf abgeschlossen werden soll. ProcessReceipt wird in sehr spezifischen Szenarien aufgerufen. Für seine Garantien siehe MarketplaceService.ProcessReceipt.
Obwohl die Definition von "Behandlung" eines Kaufs zwischen Spielen variieren kann, verwenden wir die folgenden Kriterien:
Der Kauf wurde nicht zuvor behandelt.
Der Kauf wird in der aktuellen Sitzung reflektiert.
Dies erfordert die Durchführung der folgenden Operationen, bevor PurchaseGranted zurückgegeben wird:
- Überprüfen Sie, ob die PurchaseId nicht bereits als behandelt aufgezeichnet wurde.
- Gewähren Sie den Kauf in den im Speicher befindlichen Spielerdaten des Spielers.
- Protokollieren Sie die PurchaseId als behandelt in den im Speicher befindlichen Spielerdaten des Spielers.
- Schreiben Sie die im Speicher befindlichen Spielerdaten des Spielers in den DataStore.
Die Sitzungssperrung vereinfacht diesen Ablauf, da Sie sich nicht mehr um die folgenden Szenarien kümmern müssen:
- Die im Speicher befindlichen Spielerdaten auf dem aktuellen Server könnten veraltet sein, was erfordert, dass Sie den neuesten Wert aus dem DataStore abrufen, bevor Sie die PurchaseId-Historie überprüfen.
- Der Rückruf für denselben Kauf wird auf einem anderen Server ausgeführt, was erfordert, dass Sie sowohl die PurchaseId-Historie lesen und schreiben und die aktualisierten Spielerdaten mit dem Kauf atomar speichern, um Rennbedingungen zu verhindern.
Die Sitzungssperrung garantiert, dass, wenn ein Versuch, in den DataStore des Spielers zu schreiben, erfolgreich ist, kein anderer Server erfolgreich auf den DataStore des Spielers gelesen oder geschrieben hat, während die Daten in diesem Server geladen und gespeichert wurden. Kurz gesagt, die im Speicher befindlichen Spielerdaten auf diesem Server sind die aktuellste verfügbare Version. Es gibt einige Vorbehalte, aber diese beeinflussen dieses Verhalten nicht.
Ansatz
Die Kommentare in ReceiptProcessor umreißen den Ansatz:
Überprüfen Sie, ob die Daten des Spielers derzeit auf diesem Server geladen sind und ob sie ohne Fehler geladen wurden.
Da dieses System die Sitzungssperrung verwendet, überprüft diese Überprüfung auch, ob die im Speicher befindlichen 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 hört auch auf, dass der Spieler das Spiel verlässt, bevor seine Daten geladen werden, da es nicht unbegrenzt warten und diesen Rückruf auf diesem Server für diesen Kauf blockieren sollte, wenn der Spieler erneut beitritt.
Überprüfen Sie, ob die PurchaseId nicht bereits als verarbeitet in den Spielerdaten aufgezeichnet ist.
Aufgrund der Sitzungssperrung ist das Array der PurchaseIds, das das System im Speicher hat, die aktuellste Version. Wenn die PurchaseId als verarbeitet aufgezeichnet ist und in einem Wert reflektiert wird, der in den DataStore geladen oder gespeichert wurde, geben Sie PurchaseGranted zurück. Wenn sie als verarbeitet aufgezeichnet ist, aber nicht im DataStore reflektiert wird, geben Sie NotProcessedYet zurück.
Aktualisieren Sie die Spielerdaten lokal auf diesem Server, um den Kauf "zu gewähren".
ReceiptProcessor verwendet einen generischen Rückrufansatz und weist für jede DeveloperProductId einen anderen Rückruf zu.
Aktualisieren Sie die Spielerdaten lokal auf diesem Server, um die PurchaseId zu speichern.
Stellen Sie eine Anfrage, um die im Speicher befindlichen 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 die im Speicher befindlichen Sitzungsdaten des Spielers speichert, dennoch erfolgreich sein. Während des nächsten ProcessReceipt-Aufrufs behandelt Schritt 2 diese Situation und gibt PurchaseGranted zurück.
Spielerdaten
Singletons: PlayerData.Server, PlayerData.Client
Hintergrund
Module, die eine Schnittstelle für den synchronen Lese- und Schreibzugriff auf die Sitzungsdaten der Spieler bereitstellen, sind in Roblox-Spielen üblich. Dieser Abschnitt behandelt PlayerData.Server und PlayerData.Client.
Ansatz
PlayerData.Server und PlayerData.Client behandeln Folgendes:
- Laden der Daten des Spielers in den Speicher, einschließlich der Handhabung von Fällen, in denen das Laden fehlschlägt
- Bereitstellung einer Schnittstelle für Servercode, um die Spielerdaten abzufragen und zu ändern
- Replikation von Änderungen in den Spielerdaten an den Client, damit der Clientcode darauf zugreifen kann
- Replikation von Lade- und/oder Speicherfehlern an den Client, damit dieser Fehlermeldungen anzeigen kann
- Periodisches Speichern der Spielerdaten des Spielers, wenn der Spieler das Spiel verlässt und wenn der Server heruntergefahren wird
Spielerdaten laden

SessionLockedDataStoreWrapper führt eine getAsync-Anfrage an den Datenspeicher durch.
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 kicken, aber wir empfehlen, den Spieler mit Standarddaten und klarer Kommunikation darüber, was passiert ist, spielen zu lassen, anstatt ihn aus dem Spiel zu entfernen.
Eine anfängliche Nutzlast wird an PlayerDataClient gesendet, die die geladenen Daten und den Fehlerstatus (falls vorhanden) enthält.
Alle Threads, die mit waitForDataLoadAsync für den Spieler gewartet haben, werden fortgesetzt.
Bereitstellung einer Schnittstelle für Servercode
- PlayerDataServer ist ein Singleton, das von jedem Servercode, der in derselben Umgebung ausgeführt wird, benötigt und zugegriffen werden kann.
- Die Spielerdaten sind in einem Wörterbuch von Schlüsseln und Werten organisiert. Sie können diese Werte auf dem Server mit den Methoden setValue, getValue, updateValue und removeValue manipulieren. Diese Methoden arbeiten alle synchron, ohne zu warten.
- Die Methoden hasLoaded und waitForDataLoadAsync sind verfügbar, um sicherzustellen, dass die Daten geladen wurden, bevor Sie darauf zugreifen. Wir empfehlen, dies einmal während eines Ladebildschirms zu tun, bevor andere Systeme gestartet werden, um zu vermeiden, dass vor jeder Interaktion mit Daten auf dem Client auf Ladefehler überprüft werden muss.
- Eine hasErrored-Methode kann abfragen, ob das anfängliche Laden des Spielers fehlgeschlagen ist, was dazu führt, dass er Standarddaten verwendet. Ü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 dass ein erfolgreiches Laden erfolgt.
- Ein playerDataUpdated-Signal wird mit dem player, key und value ausgelöst, wann immer die Daten eines Spielers geändert werden. Einzelne Systeme können sich dafür anmelden.
Änderungen an den Client replizieren
- Jede Änderung an den Spielerdaten 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 sollten.
- 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). Eine hasLoaded-Methode und ein loaded-Signal sind ebenfalls enthalten, sodass der Client auf das Laden und die Replikation von Daten warten kann, bevor er seine Systeme startet.
- PlayerDataClient ist ein Singleton, das von jedem Clientcode, der in derselben Umgebung ausgeführt wird, benötigt und zugegriffen werden kann.
Fehler an den Client replizieren
- Fehlerstatus, die beim Speichern oder Laden von Spielerdaten auftreten, werden an PlayerDataClient repliziert.
- Greifen Sie mit den Methoden getLoadError und getSaveError sowie den loaded- und saved-Signalen auf diese Informationen zu.
- Es gibt zwei Arten von Fehlern: DataStoreError (die Anfrage an DataStoreService ist fehlgeschlagen) und SessionLocked (siehe Sitzungssperrung).
- Verwenden Sie diese Ereignisse, um die Kaufaufforderungen des Clients zu deaktivieren und Warnmeldungen zu implementieren. Dieses Bild zeigt ein Beispiel für ein Dialogfeld:

Spielerdaten speichern

Wenn der Spieler das Spiel verlässt, führt das System die folgenden Schritte aus:
- Überprüfen Sie, ob es sicher ist, die Spielerdaten in den Datenspeicher zu schreiben. Szenarien, in denen es unsicher wäre, umfassen, dass die Spielerdaten des Spielers nicht geladen werden konnten oder noch geladen werden.
- Stellen Sie eine Anfrage über den SessionLockedDataStoreWrapper, um den aktuellen im Speicher befindlichen Datenwert in den Datenspeicher zu schreiben und die Sitzungssperre nach Abschluss zu entfernen.
- Löschen Sie die Spielerdaten des Spielers (und andere Variablen wie Metadaten und Fehlerstatus) aus dem Serverspeicher.
In einer periodischen Schleife schreibt der Server die Daten jedes Spielers in den Datenspeicher (vorausgesetzt, es ist sicher zu speichern). Diese willkommene Redundanz mildert den Verlust im Falle eines Serverabsturzes und ist auch notwendig, um die Sitzungssperre aufrechtzuerhalten.
Das Beispiel startet eine gemeinsame Schleife nach AUTO_SAVE_INTERVAL Sekunden (standardmäßig 180) und speichert dann jeden geladenen Spieler parallel. Diese Schleife versetzt keine Server oder Spieler, sodass Server, die zu ähnlichen Zeiten starten, zusammen flushen können.
Versetzen Sie den ersten Speicher jedes Spielers um eine zufällige Dauer innerhalb des Intervalls, damit Live-Server nicht alle gleichzeitig schreiben:
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)endWenn eine Anfrage zum Herunterfahren des Servers empfangen wird, geschieht Folgendes in einem BindToClose-Rückruf:
- Eine Anfrage wird gestellt, um die Daten jedes Spielers auf dem Server zu speichern, wobei der Prozess durchlaufen wird, der normalerweise erfolgt, wenn ein Spieler den Server verlässt. Diese Anfragen werden parallel gestellt, da BindToClose-Rückrufe nur 30 Sekunden Zeit haben, um abzuschließen.
- Um die Speicher zu beschleunigen, werden alle anderen Anfragen in der Warteschlange jedes Schlüssels aus dem zugrunde liegenden DataStoreWrapper gelöscht (siehe Wiederholungen).
- Der Rückruf gibt erst zurück, wenn alle Anfragen abgeschlossen sind.