Tło
Roblox dostarcza zestaw interfejsów do komunikacji z magazynami danych za pośrednictwem DataStoreService .Najczęstszym przypadkiem użycia dla tych API jest przechowywanie, ładowanie i replikowanie danych gracza.Oznacza to dane powiązane z postępem gracza, zakupami i innymi cechami sesji, które utrzymują się między poszczególnymi sesjami gry.
Większość doświadczeń na Roblox wykorzystuje te API do wdrożenia jakiejś formy systemu danych gracza.Te implementacje różnią się w swoim podejściu, ale ogólnie starają się rozwiązać ten sam zestaw problemów.
Wspólne problemy
Poniżej znajdują się niektóre z najczęstszych problemów, które systemy danych gracza próbują rozwiązać:
W dostępie do pamięci: DataStoreService żądania wykonują żądania sieciowe, które działają asynergicznie i podlegają ograniczeniom prędkości.Jest to odpowiednie dla początkowego obciążenia na początku sesja, ale nie dla częstych operacji odczytu i zapisu podczas normalnego przebiegu rozgrywka.Systemy danych graczy większości programistów przechowują te dane w pamięci na serwerze Roblox, ograniczając żądania DataStoreService do następujących scenariuszy:
- Początkowe odczytanie na początku sesja
- Ostateczne zapisanie na końcu sesja
- Okresowe pisanie w odstępie czasu, aby złagodzić scenariusz, w którym ostateczne pisanie nie powodzi się
- Pisze, aby zapewnić zapisanie danych podczas przetwarzania kupować
Wydajne przechowywanie: Przechowywanie całych danych sesji gracza w jednej tabeli pozwala na aktualizację wielu wartości atomowo i obsługę tej samej ilości danych w mniejszej liczbie żądań.Zmniejsza to również ryzyko dezsynchronizacji między wartościami i ułatwia odwracanie decyzji.
Niektórzy programiści wdrażają również niestandardową serializację do skompresowania dużych struktur danych (zwykle aby zapisać generowaną przez użytkownika zawartość w grze).
Replikacja: Klient potrzebuje regularnego dostępu do danych gracza (na przykład do aktualizacji interfejsu użytkownika).Ogólne podejście do replikowania danych gracza do klienta pozwala przesyłać te informacje bez konieczności tworzenia niestandardowych systemów replikacji dla każdej części danych.Programiści często chcą, aby opcja była selektywna w kwestii tego, co jest i czego nie jest replikowane do klienta.
Obsługa błędów: Gdy nie można uzyskać dostępu do magazynów danych, większość rozwiązań wdroży mechanizm ponownego wykorzystania i powrotu do danych "domyślnych".Potrzebna jest szczególna pielęgnacja, aby zapewnić, że dane awaryjne nie zostaną później nadpisane "prawdziwymi" danymi i że to zostanie odpowiednio przekazane graczowi.
Odnowienia: Gdy sklepy danych są niedostępne, większość rozwiązań implementuje mechanizm odnowienia i sposób powrotu do domyślnych danych.Zwróć szczególną uwagę na to, aby dane awaryjne nie zastąpiły później "prawdziwych" danych i odpowiednio przekazały sytuację graczowi.
Blokowanie sesji: Jeśli dane pojedynczego gracza zostaną załadowane i będą w pamięci na wielu serwerach, mogą wystąpić problemy, w których jeden serwer zapisze przeterminowane informacje.Może to prowadzić do utraty danych i wspólnych luk w duplikacji przedmiotów.
Przetwarzanie zakupów atomowych: Zweryfikuj, nagrodz i zapisz zakupy atomowo, aby zapobiec utracie przedmiotów lub przyznaniu ich wielokrotnie.
Próbny kod
Roblox ma kod referencyjny, który pomaga Ci w projektowaniu i budowaniu systemów danych gracza.Pozostała część tej strony analizuje tło, szczegóły wdrożenia i ogólne ostrzeżenia.
Po zaimportowaniu modelu do Studio powinieneś zobaczyć następną strukturę katalogu:

Architektura
Ta wysokopoziomowa diagram pokazuje kluczowe systemy w próbce i sposób, w jaki komunikują się z kodem w pozostałej części doświadczenia.

Ponowne próby
Klasa: DataStoreWrapper >
Tło
Ponieważ DataStoreService wysyła żądania sieciowe pod maską, jego żądania nie są gwarantowane do powodzenia.Kiedy tak się stanie, metody DataStore rzucają błędy, co pozwala ci je obsłużyć.
Wspólny "gotcha" może wystąpić, jeśli próbujesz obsługiwać awarie przechowywania danych w ten sposób:
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
Chociaż jest to doskonale prawidłowy mechanizm odnowienia dla ogólnej funkcji, nie jest odpowiedni do żądań DataStoreService ze względu na to, że nie gwarantuje kolejności, w jakiej żądania są składane.Zachowanie kolejności żądań jest ważne dla żądań DataStoreService ze względu na to, że wchodzą w interakcję ze stanem.Rozważ następujący scenariusz:
- Prośba A jest wykonywana, aby ustawić wartość klucza K na 1.
- Żądanie nie powiodło się, więc ponowne wykonanie jest planowane w ciągu 2 sekund.
- Zanim nastąpi ponowne wypróbowanie, wniosek B ustawia wartość K na 2, ale ponowne wypróbowanie wniosku A natychmiast zastępuje tę wartość i ustawia K na 1.
Chociaż UpdateAsync działa na najnowszej wersji wartości klucza, UpdateAsync wciąż muszą być przetwarzane żądania, aby uniknąć nieprawidłowych stanów przejściowych (na przykład zakup odlicza monety przed dodaniem monety, co prowadzi do negatywnych monet).
Nasz system danych odtwarzacza używa nowej klasy, DataStoreWrapper, która zapewnia odpłatne próby ponowne, które są gwarantowane do przetwarzania według klucza.
Podejście

DataStoreWrapper dostarcza metody odpowiadające metodom DataStore : DataStore:GetAsync() , DataStore:SetAsync() , DataStore:UpdateAsync() i DataStore:RemoveAsync() .
Te metody, gdy są wzywane:
Dodaj żądanie do kolejki.Każdy klucz ma swoją własną kolejkę, w której wnioski są przetwarzane w kolejności i w kolejności.Wątek żądający zatrzymuje się, dopóki wniosek nie zostanie zrealizowany.
Funkcjonalność ta opiera się na klasie ThreadQueue, która jest harmonogramem zadań i ogranicznikiem prędkości opartym na koryncie.Zamiast zwracać obiecane, ThreadQueue zwraca obecny wątek, aż operacja zostanie zakończona i wyświetla błąd, jeśli nie powiedzie się.Jest to bardziej zgodne z idiomatycznymi wzorcami asynchronicznymi Luau.
Jeśli wniosek nie powiedzie się, ponownie próbuje z wykładniczym odrzuceniem konfigurowalnymTe ponowne próby są częścią powrotu przesłanego do ThreadQueue, więc są gwarantowane do zakończenia, zanim rozpocznie się następna próba w kolejce dla tego klucza.
Gdy wniosek zostanie zakończony, metoda wniosku wraca z wzorem success, result
DataStoreWrapper również ujawnia metody, aby uzyskać dług kolejki dla danego klucza i wyczyścić stare żądania.Ostatnia opcja jest szczególnie przydatna w scenariuszach, gdy serwer się wyłącza i nie ma czasu na przetworzenie żadnych, ale najnowszych żądań.
Ostrzeżenia
DataStoreWrapper podąża za zasadą, że poza ekstremalnymi scenariuszami każde żądanie przechowywania danych powinno być dozwolone do ukończenia (powodzenie lub inaczej), nawet jeśli nowsze żądanie sprawia, że jest zbędne.Kiedy pojawia się nowe żądanie, stare żądania nie są usuwane z kolejki, ale zamiast tego mogą zostać ukończone przed rozpoczęciem nowego żądania.Powód tego wynika z zastosowania tego modułu jako uniwersalnej usługi przechowywania danych, a nie konkretnego narzędzia dla danych gracza, i wygląda następująco:
Trudno jest wybrać intuicyjny zestaw zasad dla sytuacji, w której żądanie jest bezpieczne do usunięcia z kolejki. Rozważ następną kolejkę:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Oczekiwane zachowanie jest takie, że GetAsync() powinno zwrócić 1, ale jeśli usuniemy żądanie SetAsync() z kolejki ze względu na to, że zostało uznane za zbędne przez najnowsze, zwróci 0 .
Logiczny postęp polega na tym, że gdy dodano nową prośbę o napisanie, tylko oczyszczono stare prośby tak daleko, jak najnowsza prośba o odczyt.UpdateAsync() , zdecydowanie najczęstsza operacja (i jedyna stosowana przez ten system) może zarówno czytać, jak pisać, więc trudno byłoby pogodzić to w tym projekcie bez dodawania dodatkowej złożoności.
DataStoreWrapper mógł wymagać, aby określić, czy wniosek UpdateAsync() został zatwierdzony do odczytu i/lub zapisu, ale nie miałby zastosowania do naszego systemu danych gracza, gdzie nie można tego określić przed czasem ze względu na mechanizm blokowania sesji (omówiony szczegółowo później).
Po usunięciu z kolejki trudno jest podjąć decyzję o intuicyjnej zasadzie dla jak powinno to być obsługiwane.Kiedy zostanie złożone żądanie DataStoreWrapper, obecny wątek jest przekazywany, dopóki nie zostanie zakończony.Jeśli usunęlibyśmy stare żądania z kolejki, musielibyśmy zdecydować, czy mają zostać zwrócone false, "Removed from queue" lub nigdy nie wrócić i odrzucić aktywny wątek.Oba podejścia mają swoje wady i przenoszą dodatkową złożoność na konsumenta.
Ostatecznie naszym zdaniem prostsze podejście (przetwarzanie każdego prośba) jest tutaj preferowane i tworzy bardziej przejrzyste środowisko do nawigacji, gdy zbliżamy się do skomplikowanych problemów, takich jak blokowanie sesji.Jedynym wyjątkiem od tego jest podczas DataModel:BindToClose(), gdzie oczyszczenie kolejki staje się konieczne, aby zapisać wszystkie dane użytkowników na czas, a wartość powrotu pojedynczej funkcji nie jest już bieżącym problemem.Aby to zrównoważyć, wystawiamy metodę skipAllQueuesToLastEnqueued.Aby uzyskać więcej kontekstu, zobacz Dane gracza.
Blokowanie sesji
Klasa: SessionLockedDataStoreWrapper >
Tło
Dane gracza są przechowywane w pamięci na serwerze i są odczytywane i zapisywane do podstawowych magazynów danych tylko w razie potrzeby.Możesz odczytywać i aktualizować dane gracza w pamięci natychmiast bez potrzeby żądań sieciowych i unikać przekroczenia limitów DataStoreService.
Aby ten model działał zgodnie z przeznaczeniem, niezbędne jest, aby nie więcej niż jeden serwer był w stanie załadować dane gracza do pamięci z DataStore w tym samym czasie.
Na przykład, jeśli serwer A ładować dane gracza, serwer B nie może załadować tych danych, dopóki serwer A nie uwolni ich podczas ostatecznego zapisu.Bez mechanizmu blokowania serwer B mógłby pobrać przeterminowane dane odtwarzacza z magazynu danych, zanim serwer A będzie miał szansę zapisać najnowszą wersję, którą ma w pamięci.Następnie, jeśli serwer A zapisze swoje nowsze dane po załadowaniu przez serwer B przestarzałych danych, serwer B zastąpi te nowsze dane podczas następnego zapisu.
Chociaż Roblox pozwala tylko jednemu klientowi być połączonym z jednym serwerem na raz, nie możesz zakładać, że dane z jednej sesji są zawsze zapisywane przed rozpoczęciem następnej sesji.Rozważ następujące scenariusze, które mogą wystąpić, gdy gracz opuści serwer A:
- Serwer A wysyła prośbę DataStore o zapisanie danych, ale prośba nie powodzi się i wymaga kilku prób, aby zakończyć pomyślnie.Podczas okresu ponownego wypróbowania gracz dołącza do serwera B.
- Serwer A wykonuje zbyt wiele wezwań UpdateAsync() do tego samego klucza i jest ograniczony.Ostatnia prośba o zapisanie jest umieszczana w kolejce.Gdy wniosek jest w kolejce, gracz dołącza do serwera B.
- Na serwerze A niektóry kod połączony z wydarzeniem PlayerRemoving zostanie wyświetlony przed zapisaniem danych gracza.Zanim ta operacja się zakończy, gracz dołącza do serwera B.
- Wydajność serwera A spadła do punktu, w którym ostateczne zapisanie jest opóźniane do czasu, aż gracz dołączy do serwera B.
Te scenariusze powinny być rzadkie, ale zdarzają występować, szczególnie w sytuacjach, w których gracz odłącza się od jednego serwera i łączy się z innym w szybkiej kolejności (na przykład, podczas teleportacji).Niektórzy złośliwi użytkownicy mogą nawet próbować nadużywać tego zachowania, aby ukończyć działania bez ich trwania.Może to być szczególnie uciążliwe w grach, które pozwalają graczom na wymianę i jest powszechnym źródłem exploitów duplikacji przedmiotów.
Blokowanie sesji rozwiązuje tę podatność, zapewniając, że gdy klucz gracza DataStore zostanie najpierw odczytany przez serwer, serwer atomowo zapisuje blokadę do metadanych klucza w tym samym wezwaniu UpdateAsync().Jeśli wartość blokady jest obecna, gdy którykolwiek inny serwer próbuje odczytać lub zapisać klucz, serwer nie kontynuuje.
Podejście

SessionLockedDataStoreWrapper jest meta-pakietem wokół klasy DataStoreWrapper. zapewnia funkcjonalność kolekcjonowania i ponownego wykonywania, które uzupełnia blokowanie sesji.
SessionLockedDataStoreWrapper przechodzi każdą prośbę DataStore w każdym momencie — niezależnie od tego, czy jest to GetAsync , SetAsync lub UpdateAsync —przez UpdateAsync .Dzieje się tak, ponieważ UpdateAsync pozwala na czytanie i pisanie klucza do atomu.Można również zrezygnować z pisania w oparciu o wartość odczytaną przez powrót nil w powrocie transformacji.
Funkcja transformacji przekazana do UpdateAsync dla każdego żądania wykonuje następujące operacje:
Sprawdza, czy klucz jest bezpieczny do dostępu, rezygnując z operacji, jeśli nie jest. "Bezpieczny do dostępu" oznacza:
Obiekt metadanych klucza nie zawiera nieznanej wartości LockId aktualizowanej mniej niż czas wygaśnięcia blokady.To tłumaczy respektowanie zamka umieszczonego przez inny serwer i ignorowanie tego zamka, jeśli wygasł.
Jeśli ten serwer umieścił własną wartość LockId w metadanych klucza wcześniej, wartość ta nadal znajduje się w metadanych klucza.To wyjaśnia sytuację, w której inny serwer przejął blok tego serwera (przez wygaśnięcie lub siłą) i później go uwolnił.Alternatywnie sformułowane, nawet jeśli LockId jest nil, inny serwer nadal mógł zastąpić i usunąć blok w czasie od momentu, w którym zamknąłeś klucz.
UpdateAsync wykonuje operację DataStore wymaganą przez konsumenta SessionLockedDataStoreWrapper. Na przykład GetAsync() tłumaczy na function(value) return value end.
W zależności od parametrów przekazanych do prośba, UpdateAsync blokuje lub odblokowuje klucz:
Jeśli klucz ma być zablokowany, UpdateAsync ustawia LockId w metadanych klucza na GUID.Ta identyfikator jest przechowywana w pamięci na serwerze, aby następnym razem można ją zweryfikować, gdy uzyska dostęp do klucza.Jeśli serwer ma już blokadę na tym kluczu, nie dokonuje żadnych zmian.Planuje również zadanie, aby ostrzec Cię, jeśli nie uzyskasz ponownie dostępu do klucza, aby utrzymać blokadę w czasie wygaśnięcia bloku.
Jeśli klucz ma być odblokowany, UpdateAsync usuwa LockId w metadanych klucza.
Przesłany jest niestandardowy tryb odnowienia do podstawowego DataStoreWrapper, tak aby operacja została ponownie wykonana, jeśli została przerwana w kroku 1 ze względu na zablokowanie sesji.
Wysyłany jest również niestandardowy komunikat o błędzie do konsumenta, umożliwiający systemowi danych gracza zgłoszenie alternatywnego błędu w przypadku blokowania sesji dla klienta.
Ostrzeżenia
Reżim blokowania sesji polega na serwerze zawsze uwalniającym swój blok na kluczu, gdy skończy z nim.Powinno to zawsze nastąpić za pomocą instrukcji odblokowania klucza jako część ostatecznego zapisu w PlayerRemoving lub BindToClose().
Jednak odblokowanie może zawieść w pewnych sytuacjach. Na przykład:
- Serwer uległ awarii lub DataStoreService nie był dostępny dla wszystkich prób uzyskania dostępu do klucza.
- Z powodu błędu w logice lub podobnego błędu instrukcja odblokowania klucza nie została wykonana.
Aby utrzymać blokadę na kluczu, musisz regularnie uzyskiwać do niego dostęp tak długo, jak jest on przechowywany w pamięci.Zazwyczaj byłoby to zrobione jako część pętla automatycznego zapisu uruchamianego w tle w większości systemów danych graczy, ale ten system ujawnia również metodę refreshLockAsync, jeśli musisz to zrobić ręcznie.
Jeśli czas wygaśnięcia bloku został przekroczony bez aktualizacji bloku, każdy serwer jest wolny, aby przejąć blok.Jeśli inny serwer zablokuje, próby dokonane przez obecny serwer odczytania lub zapisania klucza nie powiodą się, chyba że ustanowi nowy blok.
Przetwarzanie produktu dla programistów
Singleton: ReceiptHandler
Tło
Wezwanie ProcessReceipt wykonuje krytyczną pracę określania, kiedy należy zakończyć kupować.ProcessReceipt jest wzywany w bardzo specyficznych scenariuszach.Dla swojego zestawu gwarancji patrz MarketplaceService.ProcessReceipt .
Chociaż definicja "obsługi" zakupu może się różnić między doświadczeniami, używamy następujących kryteriów
Zakup nie został wcześniej przetworzony.
Zakup jest odzwierciedlony w bieżącej sesja.
Wymaga to wykonania następujących operacji przed powrotem PurchaseGranted :
- Zweryfikuj, czy PurchaseId nie został już zarejestrowany jako rozwiązany.
- Nagrodz zakup w danych gracza w pamięci.
- Zapisz PurchaseId jako przetworzone w pamięci gracza dane gracza.
- Napisz dane o graczu w pamięci gracza do DataStore.
Blokowanie sesji uprości to przepływ, ponieważ nie musisz już martwić się o następujące scenariusze:
- Dane o graczu w pamięci na obecnym serwerze mogą być przestarzałe, wymagając od ciebie pobrania najnowszej wartości z DataStore przed weryfikacją historii PurchaseId
- Wezwanie do tej samej transakcji wykonywane na innym serwerze, wymagające od obu odczytania i zapisania historii PurchaseId oraz zapisania aktualizowanych danych odtwarzacza za pomocą transakcji odzwierciedlonej atomowo, aby zapobiec warunkom wyścigu
Blokowanie sesji gwarantuje, że jeśli próba napisania do gracza DataStore zakończy się sukcesem, żaden inny serwer nie przeczytał lub nie napisał do gracza DataStore między danymi, które zostały załadowane i zapisane na tym serwerze.Krótko mówiąc, dane o graczu w pamięci na tym serwerze są najnowszą dostępną wersją.Istnieją pewne ograniczenia, ale nie wpływają one na to zachowanie.
Podejście
Komentarze w ReceiptProcessor objaśniają podejście:
Zweryfikuj, czy dane gracza są obecnie przeładowane na tym serwerze i czy zostały wczytane bez błędów.
Ponieważ ten system wykorzystuje blokowanie sesji, ten test sprawdza również, czy dane w pamięci są najnowszą wersją.
Jeśli dane gracza jeszcze nie załadowały się (co oczekuje się, gdy gracz dołącza do gry), poczekaj, aż dane gracza zostaną wczytywać.System słucha również gracza, który opuszcza grę przed załadowaniem danych, ponieważ nie powinien być nieskończenie dostępny i blokować ponowne wezwanie tego powrotu na tym serwerze dla tej transakcji, jeśli gracz dołączy ponownie.
Zweryfikuj, czy PurchaseId nie został już zarejestrowany jako przetworzony w danych gracza.
Ze względu na blokowanie sesji, w pamięci systemu jest najnowsza wersja arraya PurchaseIds.Jeśli PurchaseId zostanie zarejestrowany jako przetworzony i odzwierciedlony w wartości, która została wczytana lub zapisana do DataStore, zwróć PurchaseGranted .Jeśli zostanie zapisane jako przetworzone, ale nie odzwierciedlone w DataStore , zwróć NotProcessedYet .
Aktualizuj dane o graczu lokalnie na tym serwerze, aby "nagrodzić" kupować.
ReceiptProcessor przyjmuje ogólne podejście do powrotu i przypisuje inny powrót dla każdego DeveloperProductId.
Aktualizuj dane odtwarzacza lokalnie na tym serwerze, aby przechować PurchaseId.
Prześlij żądanie, aby zapisać dane w pamięci do DataStore, zwracając PurchaseGranted , jeśli żądanie zakończy się sukcesem. W przeciwnym razie zwróć NotProcessedYet .
Jeśli ta prośba o zapis nie powiedzie się, późniejsza prośba o zapisanie danych sesji w pamięci gracza nadal może powieść sukces.Podczas następnego wezwania ProcessReceipt krok 2 radzi sobie z tą sytuacją i zwraca PurchaseGranted .
Dane gracza
Singletony: PlayerData.Server , PlayerData.Client
Tło
Moduły, które zapewniają interfejs dla kodu gry do synchronicznego odczytywania i zapisywania danych sesji gracza, są powszechne w doświadczeniach Roblox.Ta sekcja obejmuje PlayerData.Server i PlayerData.Client.
Podejście
PlayerData.Server i PlayerData.Client obsługują obserwuje:
- Ładowanie danych gracza do pamięci, w tym obsługa przypadków, w których nie uda się je wczytywać
- Zapewnienie interfejsu dla kodu serwera do zapytania i zmiany danych odtwarzacza
- Replikowanie zmian w danych gracza do klienta, aby kod klienta mógł do nich uzyskać dostęp
- Replikowanie błędów ładowania i/lub zapisywania do klienta, aby mógł wyświetlać dialogi o błędach
- Okresowe zapisywanie danych gracza, gdy gracz odchodzi, oraz gdy serwer się zamyka
Załaduj dane gracza

SessionLockedDataStoreWrapper wysyła prośbę getAsync do sklepdanych.
Jeśli ta prośba nie powiedzie się, domyślne dane zostaną wykorzystane, a profil zostanie oznaczony jako "błędny", aby upewnić się, że nie zostanie zapisany w magazynie danych później.
Alternatywną opcją jest wyrzucenie gracza, ale polecamy pozwolić graczowi grać z domyślnymi danymi i wyczyścić komunikację, co się wydarzyło, zamiast usuwać go z doświadczenia.
Początkowy ładunek jest wysyłany do PlayerDataClient zawierający załadowane dane i stan błędu (jeśli istnieje).
Wszystkie wątki wygenerowane za pomocą waitForDataLoadAsync dla gracza są wznowione.
Zapewnij interfejs dla kodu serwera
- PlayerDataServer jest jedynym, który może być wymagany i uzyskiwany przez dowolny kod serwera uruchamiany w tym samym środowisko.
- Dane gracza są zorganizowane w słowniku kluczy i wartości.Możesz manipulować tymi wartościami na serwerze za pomocą metod setValue, getValue, updateValue i removeValue.Wszystkie te metody działają synchronicznie bez zatrzymywania się.
- Metody hasLoaded i waitForDataLoadAsync są dostępne, aby zapewnić, że dane zostały załadowane przed uzyskaniem do nich dostępu.Zalecamy zrobienie tego raz podczas ekranu ładowania, zanim zostaną uruchomione inne systemy, aby uniknąć konieczności sprawdzania błędów ładowania przed każdą interakcją z danymi na klientze.
- Metoda hasErrored może zapytać, czy pierwotne obciążenie gracza nie powiodło się, spowodowane tym, że używają domyślnych danych.Sprawdź tę metodę przed zezwoleniem graczowi na dokonanie jakichkolwiek zakupów, ponieważ zakupy nie mogą być zapisane w danych bez udanego wczytywać.
- Sygnał playerDataUpdated uruchamia się z player, key i value za każdym razem, gdy zmieniane są dane gracza.Poszczególne systemy mogą się do tego zapisać.
Odwróć zmiany w kliencie
- Każda zmiana danych gracza w PlayerDataServer jest replikowana do PlayerDataClient, chyba że klucz został oznaczony jako prywatny za pomocą setValueAsPrivate
- setValueAsPrivate jest używany do oznaczania kluczy, które nie powinny być wysyłane do klienta
- PlayerDataClient obejmuje metodę uzyskania wartości klucza (zdobycie) oraz sygnał, który wysyłany jest, gdy zostanie zaktualizowany (zaktualizowany).Zawiera się również metoda hasLoaded i sygnał loaded, dzięki czemu klient może poczekać na załadowanie i replikację danych przed uruchomieniem swoich systemów
- PlayerDataClient jest jedynym, który może być wymagany i uzyskiwany przez dowolny kod klienta uruchamiany w tym samym środowisko
Powtórz błędy do klienta
- Stany błędów występujące podczas zapisywania lub ładowania danych gracza są replikowane do PlayerDataClient.
- Uzyskaj dostęp do tych informacji za pomocą metod getLoadError i getSaveError wraz z sygnałami loaded i saved.
- Istnieją dwa rodzaje błędów: DataStoreError (żądanie DataStoreService nie powiodło się) i SessionLocked (patrz Blokowanie sesji).
- Użyj tych wydarzeń, aby wyłączyć monity o zakup klienta i wdrożyć dialog ostrzeżeń. Poniższy obraz pokazuje przykładowy dialog:

Zapisz dane gracza

Kiedy gracz opuści grę, system wykona następujące kroki:
- Sprawdź, czy bezpiecznie jest zapisać dane gracza do sklepdanych.Scenariusze, w których nie byłoby bezpiecznie, obejmują nieudane ładowanie danych gracza lub nadal się ładowanie.
- Wyślij żądanie za pośrednictwem SessionLockedDataStoreWrapper , aby zapisać obecną wartość danych w pamięci do przechowywania danych i usunąć blokadę sesji po zakończeniu.
- Oczyszcza dane gracza (i inne zmienne, takie jak metadane i statusy błędów) z pamięci serwera.
Na cyklicznym pętlu serwer zapisuje dane każdego gracza do magazynu danych (pod warunkiem, że jest bezpieczny do zapisania).Ta powitalna redundancja łagodzi utratę w przypadku awarii serwera i jest również niezbędna do utrzymania blokady sesji.
Gdy otrzymano wniosek o wyłączenie serwera, następuje to w powrocie BindToClose:
- Wysyłana jest prośba o zapisanie danych każdego gracza na serwerze, po procesie, który zazwyczaj przechodzi, gdy gracz opuszcza serwer.Te żądania są wysyłane równolegle, ponieważ wezwania BindToClose powrotne mają tylko 30 sekund na ukończenie.
- Aby przyspieszyć zapisywanie, wszystkie inne żądania w kolejce każdego klucza są usuwane z podstawowego DataStoreWrapper (patrz Próby odnowienia).
- Wezwanie powrotne nie zostanie wywołane, dopóki wszystkie żądania nie zostaną zakończone.