Tło
Roblox udostępnia zestaw interfejsów API do komunikacji z magazynami danych za pośrednictwem DataStoreService. Najczęstszym zastosowaniem tych interfejsów API jest zapisywanie, ładowanie i replikowanie danych gracza. Oznacza to dane związane z postępem gracza, zakupami i innymi cechami sesji, które utrzymują się pomiędzy poszczególnymi sesjami gry.
Większość gier na Robloxie wykorzystuje te interfejsy API do wdrażania jakiejś formy systemu danych gracza. Te wdrożenia różnią się pod względem podejścia, ale generalnie dążą do rozwiązania tych samych problemów.
Powszechne problemy
Poniżej znajdują się niektóre z najczęstszych problemów, które systemy danych gracza starają się rozwiązać:
Dostęp w pamięci: Żądania DataStoreService wykonują asynchroniczne żądania sieciowe, które są subject to rate limits. Jest to odpowiednie do inicjalnego załadunku na początku sesji, ale nie dla operacji odczytu i zapisu o wysokiej częstotliwości w normalnym toku rozgrywki. Większość systemów danych gracza przechowuje te dane w pamięci na serwerze Roblox, ograniczając żądania DataStoreService do następujących scenariuszy:
- Wstępny odczyt na początku sesji
- Ostatni zapis na końcu sesji
- Okresowe zapisy w odstępach, aby złagodzić scenariusz, w którym ostatni zapis się nie powiódł
- Zapisy, aby upewnić się, że dane są zachowane podczas przetwarzania zakupu
Efektywne przechowywanie: Przechowywanie wszystkich danych sesji gracza w jednej tabeli pozwala na atomowe aktualizowanie wielu wartości i obsługiwanie tej samej ilości danych w mniejszej liczbie żądań. Eliminuje to także ryzyko desynchronizacji między wartościami i ułatwia proces cofania.
Niektórzy deweloperzy wdrażają również niestandardową serializację, aby skompresować duże struktury danych (typowo, aby zaoszczędzić wytworzone przez użytkowników treści w grze).
Replikacja: Klient potrzebuje regularnego dostępu do danych gracza (na przykład, aby zaktualizować interfejs użytkownika). Ogólny sposób replikowania danych gracza do klienta pozwala na transmisję tych informacji bez potrzeby tworzenia specjalnych systemów replikacji dla każdego składnika danych. Deweloperzy często chcą mieć możliwość selektywnego określenia, co jest a co 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ń wprowadza mechanizm ponawiania i zapasowe dane 'domyślne'. Należy szczególnie uważać, aby upewnić się, że zapasowe dane nie nadpiszą później 'prawdziwych' danych oraz aby odpowiednio komunikować to graczowi.
Ponawianie: Gdy magazyny danych są niedostępne, większość rozwiązań wdraża mechanizm ponawiania i zapasowe dane domyślne. Należy szczególnie dbać, aby zapasowe dane nie nadpisały później 'prawdziwych' danych i aby odpowiednio komunikować tę sytuację graczowi.
Blokowanie sesji: Jeżeli dane pojedynczego gracza zostaną załadowane i są w pamięci na wielu serwerach, mogą wystąpić problemy, w których jeden serwer zapisuje nieaktualne informacje. Może to prowadzić do utraty danych oraz typowych luk w duplikacji przedmiotów.
Atomowe przetwarzanie zakupów: Weryfikuj, nagradzaj i rejestruj zakupy atomowo, aby zapobiec utracie przedmiotów lub ich przyznaniu wielokrotnie.
Przykładowy kod
Roblox ma kod referencyjny, aby pomóc Ci w projektowaniu i budowaniu systemów danych gracza. Reszta tej strony bada tło, szczegóły implementacji i ogólne pułapki.
Po zaimportowaniu modelu do Studia powinieneś zobaczyć następującą strukturę folderów:

Architektura
Ten diagram na wysokim poziomie ilustruje kluczowe systemy w przykładzie oraz ich interakcje z kodem w reszcie gry.

Ponawianie
Klasa: DataStoreWrapper
Tło
Ponieważ DataStoreService wykonuje wewnętrzne żądania sieciowe, jego żądania nie są gwarantowane na sukces. Gdy to się zdarzy, metody DataStore zgłaszają błędy, co pozwala Ci je obsługiwać.
Typowym "pułapką" może być próba obsługi awarii magazynu 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ż to jest całkowicie poprawny mechanizm ponawiania dla ogólnej funkcji, nie jest odpowiedni dla żądań DataStoreService, ponieważ nie gwarantuje kolejności, w jakiej są wykonywane żądania. Zachowanie kolejności żądań jest ważne dla żądań DataStoreService, ponieważ współdziałają one z stanem. Rozważ następujący scenariusz:
- Żądanie A jest wysyłane w celu ustawienia wartości klucza K na 1.
- Żądanie kończy się niepowodzeniem, więc planowane jest ponowienie po 2 sekundach.
- Zanim nastąpi ponowienie, żądanie B ustawia wartość K na 2, ale ponowienie żądania A natychmiast nadpisuje tę wartość i ustawia K na 1.
Chociaż UpdateAsync działa na najnowszej wersji wartości klucza, żądania UpdateAsync muszą nadal być przetwarzane w kolejności, aby uniknąć nieprawidłowych stanów przejściowych (na przykład, zakup odejmuje monety przed przetworzeniem dodania monet, co skutkuje ujemnymi monetami).
Nasz system danych gracza wykorzystuje nową klasę, DataStoreWrapper, która zapewnia ponawianie żądań z wstrzymywaniem, które są gwarantowane do przetwarzania w kolejności dla każdego klucza.
Podejście

DataStoreWrapper udostępnia metody odpowiadające metodom DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() oraz DataStore:RemoveAsync().
Te metody, gdy są wywoływane:
Dodają żądanie do kolejki. Każdy klucz ma swoją własną kolejkę, w której żądania są przetwarzane w kolejności i seryjnie. Wątek żądający wstrzymuje się, aż żądanie zostanie zakończone.
Ta funkcjonalność opiera się na klasie ThreadQueue, która jest harmonogramem zadań opartym na korutynach i ogranicznikiem szybkości. Zamiast zwracać obietnicę, ThreadQueue wstrzymuje bieżący wątek, aż operacja zostanie zakończona i zgłasza błąd, jeśli się nie powiedzie. Jest to bardziej zgodne z idiomatycznymi wzorcami asynchronicznymi w Luau.
Jeśli żądanie nie powiedzie się, jest ponawiane z konfigurowanym eksponencjalnym opóźnieniem. Te ponowienia stanowią część zwrotnego wywołania przekazanego do ThreadQueue, więc są zapewnione do zakończenia przed rozpoczęciem następnego żądania w kolejce dla tego klucza.
Kiedy żądanie jest zakończone, metoda żądania zwraca wartość w formacie success, result.
DataStoreWrapper udostępnia również metody, aby uzyskać długość kolejki dla danego klucza i wyczyścić przestarzałe żądania. Ostatnia opcja jest szczególnie przydatna w scenariuszach, gdy serwer jest wyłączany i nie ma czasu na przetworzenie żadnych, z wyjątkiem najnowszych żądań.
Pułapki
DataStoreWrapper podąża za zasadą, że, z wyjątkiem skrajnych scenariuszy, każde żądanie magazynu danych powinno zostać umożliwione do zakończenia (szczęśliwie lub w przeciwnym razie), nawet jeśli bardziej niedawne żądanie czyni je zbędnym. Kiedy występuje nowe żądanie, przestarzałe żądania nie są usuwane z kolejki, ale pozwala się im na zakończenie przed rozpoczęciem nowego żądania. Uzasadnienie tego podejścia opiera się na zastosowaniu tego modułu jako ogólnego narzędzia do magazynu danych, a nie konkretnego narzędzia do danych gracza, i obejmuje następujące kwestie:
Trudno jest zdecydować o intuicyjnym zestawie zasad dotyczących tego, kiedy żądanie jest bezpieczne do usunięcia z kolejki. Rozważ następującą kolejkę:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Oczekiwane zachowanie to, że GetAsync() zwróci 1, ale jeśli usuniemy żądanie SetAsync() z kolejki z powodu jego nadmiarowości na skutek najnowszego, zwróci 0.
Logiczny postęp polega na tym, że gdy dodawane jest nowe żądanie zapisu, należy usuwać przestarzałe żądania tylko do miejsca, w którym znajduje się najnowsze żądanie odczytu. UpdateAsync(), zdecydowanie najczęstsza operacja (a jedyna używana przez ten system), może zarówno odczytywać, jak i zapisywać, więc byłoby trudno pogodzić to w tym projekcie bez dodawania dodatkowej złożoności.
DataStoreWrapper mógłby wymagać określenia, czy żądanie UpdateAsync() miało prawo odczytywać i/lub zapisywać, ale nie miałoby to zastosowania w naszym systemie danych gracza, gdzie nie można tego ustalić z wyprzedzeniem z powodu mechanizmu blokowania sesji (omówionego bardziej szczegółowo później).
Po usunięciu z kolejki trudno jest zdecydować o intuicyjnej zasadzie, jak to powinno być obsługiwane. Gdy składane jest żądanie DataStoreWrapper, bieżący wątek jest wstrzymywany, aż zostanie zakończone. Jeśli usunięto by przestarzałe żądania z kolejki, musielibyśmy zdecydować, czy zwrócić false, "Usunięto z kolejki" czy nigdy nie zwracać i zignorować aktywny wątek. Oba podejścia mają swoje wady i nakładają dodatkową złożoność na konsumenta.
Ostatecznie uważamy, że proste podejście (przetwarzanie każdego żądania) jest tu lepsze i stwarza jaśniejsze otoczenie do poruszania się w obliczu złożonych problemów, takich jak blokowanie sesji. Jedynym wyjątkiem od tej zasady jest podczas DataModel:BindToClose(), gdzie oczyszczenie kolejki staje się konieczne, aby zapisać dane wszystkich użytkowników na czas, a wartość, jaką zwracają poszczególne wywołania funkcji, przestaje być bieżącą kwestią. Aby to ułatwić, udostępniamy metodę skipAllQueuesToLastEnqueued. Dla dodatkowych kontekstów, zobacz Dane gracza.
Blokowanie sesji
Klasa: SessionLockedDataStoreWrapper
Tło
Dane gracza są przechowywane w pamięci na serwerze i są odczytywane i zapisywane z podstawowych magazynów danych tylko wtedy, gdy to konieczne. Możesz odczytywać i aktualizować dane gracza w pamięci natychmiast, bez potrzeby wysyłania żądań sieciowych, co pozwala uniknąć przekraczania limitów DataStoreService.
Aby ten model działał zgodnie z zamierzeniem, istotne 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 ładuje dane gracza, serwer B nie może załadować tych danych, dopóki serwer A nie zwolni swojego locku podczas ostatniego zapisu. Bez mechanizmu blokowania serwer B mógłby załadować nieaktualne dane gracza z magazynu danych, zanim serwer A będzie miał szansę zapisać nowszą wersję, którą ma w pamięci. Następnie, jeśli serwer A zapisuje swoje nowsze dane po tym, jak serwer B załadował nieaktualne dane, serwer B nadpisze te nowsze dane podczas następnego zapisu.
Nawet jeśli Roblox pozwala tylko jednemu klientowi na połączenie z jednym serwerem w tym samym czasie, nie możesz założyć, ż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 opuszcza serwer A:
- Serwer A wykonuje żądanie DataStore, aby zapisać swoje dane, ale żądanie kończy się niepowodzeniem i wymaga wielu ponowień, aby pomyślnie zakończyć. W czasie okresu ponawiania gracz dołącza do serwera B.
- Serwer A wykonuje zbyt wiele wywołań UpdateAsync() do tego samego klucza i jest throttlowany. Ostatnie żądanie zapisu zostaje umieszczone w kolejce. Gdy żądanie jest w kolejce, gracz dołącza do serwera B.
- Na serwerze A, kod powiązany z wydarzeniem PlayerRemoving wstrzymuje się przed zapisaniem danych gracza. Przed zakończeniem tej operacji gracz dołącza do serwera B.
- Wydajność serwera A uległa znacznemu pogorszeniu na tyle, że ostateczny zapis jest opóźniony aż do momentu, gdy gracz dołącza do serwera B.
Te scenariusze powinny być rzadkie, ale występują, szczególnie w sytuacjach, gdy gracz rozłącza się z jednym serwerem i łączy z innym w szybkim tempie (na przykład podczas teleportacji). Niektórzy złośliwi użytkownicy mogą nawet próbować wykorzystać to zachowanie, aby zakończyć działania bez ich zachowania. Może to mieć szczególny wpływ na gry, które pozwalają graczom na handel i jest częstym źródłem exploitów duplikacji przedmiotów.
Blokowanie sesji adresuje tę lukę, zapewniając, że gdy klucz DataStore gracza jest po raz pierwszy odczytywany przez serwer, serwer atomowo zapisuje lock w metadanych klucza za pomocą tego samego wywołania UpdateAsync(). Jeśli ta wartość locka jest obecna, gdy jakikolwiek inny serwer próbuje odczytać lub zapisać klucz, serwer nie wykonuje dalszych działań.
Podejście

SessionLockedDataStoreWrapper jest meta-opakowaniem wokół klasy DataStoreWrapper. DataStoreWrapper zapewnia funkcjonalności kolejkowania i ponawiania, które SessionLockedDataStoreWrapper uzupełnia blokowaniem sesji.
SessionLockedDataStoreWrapper przekazuje każde żądanie DataStore — niezależnie od tego, czy jest to GetAsync, SetAsync czy UpdateAsync — przez UpdateAsync. Dzieje się tak, ponieważ UpdateAsync umożliwia zarówno odczyt, jak i zapis klucza atomowo. Możliwe jest także porzucenie zapisu na podstawie odczytanej wartości, zwracając nil w funkcji transformacji.
Funkcja transformacji przekazana do UpdateAsync dla każdego żądania wykonuje następujące operacje:
Weryfikuje, czy klucz jest bezpieczny do uzyskania dostępu, porzucając operację, jeśli nie. "Bezpieczny do uzyskania dostępu" oznacza:
Metadane klucza nie zawierają nieznanej wartości LockId, która została ostatnio zaktualizowana mniej niż czas wygaśnięcia locka temu. Uwzględnia to poszanowanie locka nałożonego przez inny serwer oraz ignorowanie go, jeśli wygasł.
Jeśli ten serwer wcześniej umieścił własną wartość LockId w metadanych klucza, ta wartość nadal znajduje się w metadanych klucza. Uwzględnia to sytuację, w której inny serwer przejął lock tego serwera (na skutek wygaśnięcia lub siłą) i później go zwolnił. Alternatywnie, nawet jeśli LockId jest nil, inny serwer mógłby nadal zastąpić i usunąć lock w czasie od czasu, kiedy zablokowałeś klucz.
UpdateAsync przeprowadza operację DataStore, której konsument SessionLockedDataStoreWrapper żądał. Na przykład, GetAsync() tłumaczy się na function(value) return value end.
W zależności od parametrów przekazanych w żądaniu, UpdateAsync albo blokuje, albo odblokowuje klucz:
Jeśli klucz ma być zablokowany, UpdateAsync ustawia LockId w metadanych klucza na GUID. Ten GUID jest przechowywany w pamięci na serwerze, aby można go było zweryfikować następnym razem, gdy uzyskuje dostęp do klucza. Jeśli serwer już ma lock na tym kluczu, nie wprowadza żadnych zmian. Zaplanowane zostaje także zadanie, które ostrzegawca, jeśli nie uzyskasz ponownie dostępu do klucza, aby mantenować lock w czasie wygaśnięcia.
Jeśli klucz ma być odblokowany, UpdateAsync usuwa LockId w metadanych klucza.
Do wewnętrznego DataStoreWrapper przekazywany jest niestandardowy handler ponawiania, aby operacja była ponawiana, jeśli została przerwana na kroku 1 z powodu blokady sesji.
Konsumentowi zwracana jest także niestandardowa wiadomość o błędzie, co pozwala systemowi danych gracza raportować alternatywny błąd w przypadku blokady sesji do klienta.
Pułapki
System blokowania sesji opiera się na tym, że serwer zawsze zwalnia swój lock na klucz, gdy zakończy jego obsługę. To powinno zawsze następować na skutek instrukcji odblokowania klucza jako część ostatecznego zapisu w PlayerRemoving lub BindToClose().
Jednakże odbloka może nie powieść się w niektórych sytuacjach. Na przykład:
- Serwer uległ awarii lub DataStoreService był niesprawny podczas wszystkich prób dostępu do klucza.
- Z powodu błędu w logice lub podobnego błędu instrukcja odblokowania klucza nie została wydana.
Aby utrzymać lock na kluczu, musisz regularnie uzyskiwać do niego dostęp tak długo, jak jest załadowany w pamięci. Zazwyczaj byłoby to wykonywane jako część pętli automatycznego zapisu działającej w tle w większości systemów danych gracza, ale ten system także udostępnia metodę refreshLockAsync, jeśli musisz to zrobić ręcznie.
Jeśli czas wygaśnięcia locka został przekroczony, a lock nie został zaktualizowany, wtedy każdy serwer ma prawo przejąć lock. Jeśli inny serwer przejmuje lock, próby aktualnego serwera na odczyt lub zapis klucza kończą się niepowodzeniem, chyba że ustanowi nowy lock.
Przetwarzanie produktu dewelopera
Singleton: ReceiptHandler
Tło
Callback ProcessReceipt pełni krytyczną rolę w decydowaniu, kiedy sfinalizować zakup. ProcessReceipt jest wywoływany w bardzo specyficznych scenariuszach. Dla jego zestawu gwarancji, zobacz MarketplaceService.ProcessReceipt.
Chociaż definicja „obsługi” zakupu może różnić się pomiędzy grami, stosujemy następujące kryteria:
Zakup nie został wcześniej obsłużony.
Zakup jest odzwierciedlony w bieżącej sesji.
Wymaga to przeprowadzenia następujących operacji przed zwróceniem PurchaseGranted:
- Weryfikacja, że PurchaseId nie został już zapisany jako obsłużony.
- Nagradza zakup w danych gracza w pamięci.
- Rejestruje PurchaseId jako obsłużony w danych gracza w pamięci.
- Zapisuje dane gracza w pamięci do DataStore.
Blokowanie sesji upraszcza ten proces, ponieważ nie musisz się już martwić o następujące scenariusze:
- Lokalne dane gracza w bieżącym serwerze potencjalnie są nieaktualne, co wymagałoby pobrania najnowszej wartości z DataStore przed weryfikacją historii PurchaseId.
- Callback dla tego samego zakupu działa w innym serwerze, co wymagałoby zarówno odczytu, jak i zapisu historii PurchaseId oraz zapisania zaktualizowanych danych gracza z odzwierciedlonym zakupem atomowo, aby zapobiec warunkom wyścigu.
Blokowanie sesji gwarantuje, że jeśli próba zapisu do DataStore gracza powiedzie się, żaden inny serwer nie przeczytał ani nie zapisał danych gracza do DataStore między załadowanymi a zapisanymi danymi w tym serwerze. Krótko mówiąc, lokalne dane gracza w tym serwerze są najnowszą dostępną wersją. Istnieją pewne pułapki, ale nie wpływają na to zachowanie.
Podejście
Komentarze w ReceiptProcessor opisują podejście:
Weryfikacja, że dane gracza są obecnie załadowane na tym serwerze i że załadowano je bez błędów.
Ponieważ system ten używa blokowania sesji, ta kontrola również potwierdza, że dane w pamięci są najnowszą wersją.
Jeśli dane gracza jeszcze się nie załadowały (co jest oczekiwane, gdy gracz dołącza do gry), czeka na załadowanie danych gracza. System nasłuchuje również, gdy gracz opuszcza grę przed załadowaniem ich danych, ponieważ nie powinien wstrzymywać się w nieskończoność i blokować ponowne wywołanie tego callbacku w tym serwerze dla tego zakupu, jeśli gracz dołączy ponownie.
Weryfikacja, że PurchaseId nie jest już zapisany jako przetworzony w danych gracza.
Dzięki blokowaniu sesji, tablica PurchaseIds, jaką system ma w pamięci, to najnowsza wersja. Jeśli PurchaseId jest zapisany jako przetworzony i odzwierciedlony w wartości, która została załadowana lub zapisana w DataStore, zwróć PurchaseGranted. Jeśli jest zapisany jako przetworzony, ale nie odzwierciedlony w DataStore, zwróć NotProcessedYet.
Zaktualizuj dane gracza lokalnie w tym serwerze, aby "nagradzać" zakup.
ReceiptProcessor stosuje ogólne podejście do callbacków i przypisuje inny callback dla każdego DeveloperProductId.
Zaktualizuj dane gracza lokalnie в tym serwerze, aby przechować PurchaseId.
Złóż żądanie zapisania danych w pamięci do DataStore, zwracając PurchaseGranted, jeśli żądanie zakończy się sukcesem. W przeciwnym razie zwróć NotProcessedYet.
Jeśli to żądanie zapisu nie zakończy się sukcesem, późniejsze żądanie, aby zapisać dane sesyjne gracza w pamięci, mogłoby nadal się powieść. Podczas następnego wywołania ProcessReceipt krok 2 obsługuje tę sytuację i zwraca PurchaseGranted.
Dane gracza
Singletony: PlayerData.Server, PlayerData.Client
Tło
Moduły, które zapewniają interfejs do synchronizacyjnego odczytu i zapisu danych sesji gracza, są powszechne w grach na Robloxie. Ta sekcja dotyczy PlayerData.Server i PlayerData.Client.
Podejście
PlayerData.Server i PlayerData.Client obsługują następujące:
- Ładowanie danych gracza do pamięci, w tym obsługę przypadków, w których ładowanie nie powiodło się.
- Zapewnienie interfejsu dla kodu serwera do zapytania i zmiany danych gracza.
- Replikowanie zmian w danych gracza do klienta, aby kod kliencki mógł uzyskać do nich dostęp.
- Replikowanie błędów ładowania i/lub zapisywania do klienta, aby mógł wyświetlać okna dialogowe błędów.
- O okresowe zapisy danych gracza, gdy gracz opuszcza grę oraz gdy serwer się wyłącza.
Ładowanie danych gracza

SessionLockedDataStoreWrapper składa żądanie getAsync do magazynu danych.
Jeśli to żądanie nie powiedzie się, używane są dane domyślne, a profil jest oznaczony jako "błędny", aby upewnić się, że nie będą one zapisane w magazynie danych później.
Alternatywną opcją jest wyrzucenie gracza, ale rekomendujemy pozwolenie graczowi na grę z danymi domyślnymi i jasnymi komunikatami wskazującymi na to, co się wydarzyło, zamiast usuwania ich z gry.
Wstępny ładunek jest wysyłany do PlayerDataClient, zawierający załadowane dane oraz status błędu (jeśli występuje).
Wszelkie wątki wstrzymane przy użyciu waitForDataLoadAsync dla gracza są wznawiane.
Zapewnienie interfejsu dla kodu serwera
- PlayerDataServer to singleton, który może być wymagany i dostępny przez dowolny kod serwera działający w tym samym środowisku.
- 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 oraz removeValue. Wszystkie te metody działają synchronously bez wstrzymywania.
- Metody hasLoaded i waitForDataLoadAsync są dostępne, aby upewnić się, że dane zostały załadowane przed uzyskaniem do nich dostępu. Rekomendujemy wykonanie tego raz, podczas ekranu ładowania, zanim inne systemy zostaną uruchomione, aby uniknąć konieczności sprawdzania błędów ładowania przed każdą interakcją z danymi po stronie klienta.
- Metoda hasErrored może sprawdzić, czy wstępne ładowanie gracza nie powiodło się, co zmusiło go do korzystania z danych domyślnych. Sprawdź tę metodę, zanim pozwolisz graczowi na dokonywanie jakichkolwiek zakupów, ponieważ zakupy nie mogą być zapisywane w danych bez udanego załadunku.
- Sygnalizacja playerDataUpdated wyzwala gracza, klucz i wartość za każdym razem, gdy dane gracza są zmieniane. Poszczególne systemy mogą się na to zapisać.
Replikowanie zmian do klienta
- Każda zmiana w danych gracza w PlayerDataServer jest replikowana do PlayerDataClient, chyba że ten klucz został oznaczony jako prywatny za pomocą setValueAsPrivate.
- setValueAsPrivate jest używane do oznaczania kluczy, które nie powinny być wysyłane do klienta.
- PlayerDataClient zawiera metodę uzyskiwania wartości klucza (get) oraz sygnał, który wyzwala, gdy zostaje zaktualizowany (updated). Wartości metody hasLoaded oraz sygnał loaded są dostępne, aby klient mógł czekać na załadowanie danych i zreplikowanie ich zanim rozpocznie swoje systemy.
- PlayerDataClient to singleton, który może być wymagany i dostępny przez dowolny kod klienta działający w tym samym środowisku.
Replikowanie błędów do klienta
- Statusy błędów napotkane podczas zapisywania lub ładowania danych gracza są replikowane do PlayerDataClient.
- Uzyskaj te informacje 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ę) oraz SessionLocked (zobacz Blokowanie sesji).
- Użyj tych zdarzeń, aby wyłączyć komunikaty o zakupach po stronie klienta i wdrożyć okna dialogowe ostrzegawcze. Ten obrazek pokazuje przykładowe okno dialogowe:

Zapisywanie danych gracza

Gdy gracz opuszcza grę, system wykonuje następujące kroki:
- Sprawdza, czy bezpiecznie jest zapisać dane gracza do magazynu danych. Scenariusze, w których byłoby to niebezpieczne, obejmują niepowodzenie ładowania danych gracza lub wciąż przetwarzane ładowanie.
- Składa żądanie za pośrednictwem SessionLockedDataStoreWrapper, aby zapisać bieżącą wartość danych w pamięci do magazynu danych i usunąć blokadę sesji, gdy zakończone.
- Opróżnia dane gracza (i inne zmienne, takie jak metadane i statusy błędów) z pamięci serwera.
W okresowej pętli serwer zapisuje dane każdego gracza do magazynu danych (pod warunkiem, że jest bezpieczne do zapisania). Taka redundancja zapobiega utracie danych w przypadku awarii serwera i jest również niezbędna do utrzymania blokady sesji.
Gdy otrzymane zostaje żądanie wyłączenia serwera, następuje to w wywołaniu zwrotnym BindToClose:
- Żądanie jest składane w celu zapisania danych każdego gracza na serwerze, zgodnie z procesem normalnie stosowanym, gdy gracz opuszcza serwer. Te żądania są realizowane równolegle, ponieważ wywołania zwrotne BindToClose mają tylko 30 sekund na zakończenie.
- Aby przyspieszyć zapisy, wszystkie inne żądania w kolejce każdego klucza są usuwane z wewnętrznego DataStoreWrapper (zobacz Ponawianie).
- Wywołanie zwrotne nie zwraca, dopóki wszystkie żądania nie zostaną zakończone.