Tło
Roblox udostępnia zestaw interfejsów API do interakcji z magazynami danych za pośrednictwem DataStoreService. Najczęstsze zastosowanie tych interfejsów API polega na zapisywaniu, ładowaniu i replikacji danych gracza. Oznacza to dane związane z postępami gracza, zakupami i innymi cechami sesji, które utrzymują się między poszczególnymi sesjami gry.
Większość gier na Robloxie korzysta z tych interfejsów API, aby wdrożyć jakąś formę systemu danych gracza. Te implementacje różnią się podejściem, ale generalnie dążą do rozwiązania tego samego zestawu problemów.
Powszechne problemy
Poniżej przedstawiono niektóre z najczęstszych problemów, które systemy danych gracza próbują rozwiązać:
Dostęp w pamięci: Żądania DataStoreService wykonują zapytania sieciowe, które działają asynchronicznie i podlegają ograniczeniom prędkości. Jest to odpowiednie dla początkowego załadunku na początku sesji, ale nie dla operacji odczytu i zapisu o wysokiej częstotliwości w trakcie normalnej 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:
- Początkowy odczyt na początku sesji
- Ostateczny zapis na końcu sesji
- Okresowe zapisy w interwałach, aby złagodzić sytuację, w której ostateczny zapis nie powiedzie się
- Zapisy, aby upewnić się, że dane są zapisywane 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ń. Usuwa to również ryzyko desynchronizacji między wartościami i ułatwia rozważanie cofnięć.
Niektórzy deweloperzy wdrażają również niestandardową serializację, aby kompresować duże struktury danych (zazwyczaj w celu zapisania w grze treści generowanej przez użytkowników).
Replikacja: Klient potrzebuje regularnego dostępu do danych gracza (na przykład, aby zaktualizować interfejs użytkownika). Ogólne podejście do replikacji danych gracza do klienta pozwala na przesyłanie tych informacji bez konieczności tworzenia dostosowanych systemów replikacji dla każdego komponentu danych. Deweloperzy często chcą mieć możliwość selektywnego decydowania, co jest, a co nie jest replikowane do klienta.
Obsługa błędów: Gdy magazyny danych nie mogą być dostępne, większość rozwiązań wdraża mechanizm ponawiania prób i zapasowe dane 'domyślne'. Należy szczególnie uważać, aby upewnić się, że dane zapasowe nie nadpiszą później 'prawdziwych' danych i że jest to odpowiednio komunikowane graczowi.
Ponowienia: Gdy magazyny danych są niedostępne, większość rozwiązań wdraża mechanizm ponawiania prób i zapasowe dane domyślne. Należy szczególnie uważać, aby upewnić się, że dane zapasowe nie nadpiszą później "prawdziwych" danych i odpowiednio komunikować sytuację graczowi.
Blokowanie sesji: Jeśli dane jednego gracza są ładowane i przechowywane 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 i powszechnych luk w duplikacji przedmiotów.
Atomowa obsługa zakupów: Weryfikuj, przyznawaj i rejestruj zakupy atomowo, aby zapobiec utracie przedmiotów lub ich przyznawaniu wielokrotnie.
Przykładowy kod
Roblox ma kod referencyjny, który pomoże Ci w projektowaniu i budowaniu systemów danych gracza. Reszta tej strony bada tło, szczegóły implementacji i ogólne zastrzeżenia.
Po zaimportowaniu modelu do Studio powinieneś zobaczyć następującą strukturę folderów:

Architektura
Ten diagram na wysokim poziomie ilustruje kluczowe systemy w próbce i jak interfejsują z kodem w reszcie gry.

Ponowienia
Klasa: DataStoreWrapper
Tło
Ponieważ DataStoreService wykonuje zapytania sieciowe w tle, jego żądania nie są gwarantowane do sukcesu. Gdy to się zdarzy, metody DataStore zgłaszają błędy, co pozwala Ci je obsłużyć.
Typowy "pułapka" może wystąpić, jeśli spróbujesz obsłużyć błędy magazynu danych w ten sposób:
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
endPonawiaj tymczasowe błędy z wykładniczym opóźnieniem i losowym jitterem, aby serwery nie ponawiały prób jednocześnie. Ogranicz opóźnienie i liczbę prób.
Nawet z tym wzorem opóźnienia, ten mechanizm ponawiania nie jest odpowiedni dla żądań DataStoreService, ponieważ nie gwarantuje kolejności, w jakiej żądania są składane. Zachowanie kolejności żądań jest ważne dla żądań DataStoreService, ponieważ interakcji z stanem. Rozważ następujący scenariusz:
- Żądanie A jest składane, aby ustawić wartość klucza K na 1.
- Żądanie nie powiodło się, więc ponowienie jest zaplanowane do uruchomienia po opóźnieniu.
- Zanim ponowienie nastąpi, żądanie B ustawia wartość K na 2, ale ponowienie żądania A natychmiast nadpisuje tę wartość i ustawia K na 1.
Nawet jeśli UpdateAsync działa na najnowszej wersji wartości klucza, żądania UpdateAsync muszą być nadal 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 używa nowej klasy, DataStoreWrapper, która zapewnia ponowienia z gwarancją przetwarzania w kolejności dla każdego klucza.
Podejście

DataStoreWrapper zapewnia metody odpowiadające metodom DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() i 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 w serii. Wątek żądający czeka, aż żądanie zostanie zakończone.
Ta funkcjonalność opiera się na klasie ThreadQueue, która jest opartym na korutynach harmonogramem zadań i ogranicznikiem prędkoś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 Luau.
Jeśli żądanie nie powiedzie się, ponawia z konfigurowanym wykładniczym opóźnieniem. Te ponowienia są częścią wywołania zwrotnego przesłanego do ThreadQueue, więc są gwarantowane do zakończenia przed rozpoczęciem następnego żądania w kolejce dla tego klucza.
Gdy żądanie zostanie zakończone, metoda żądania zwraca wzór success, result.
DataStoreWrapper udostępnia również metody do uzyskania długości kolejki dla danego klucza i usunięcia przestarzałych żądań. Ta ostatnia opcja jest szczególnie przydatna w scenariuszach, gdy serwer jest zamykany i nie ma czasu na przetworzenie żadnych, poza najnowszymi żądaniami.
Zastrzeżenia
DataStoreWrapper przestrzega zasady, że poza ekstremalnymi scenariuszami każde żądanie magazynu danych powinno mieć możliwość zakończenia (pomyślnie lub nie), nawet jeśli nowsze żądanie czyni je zbędnym. Gdy występuje nowe żądanie, przestarzałe żądania nie są usuwane z kolejki, ale są dozwolone do zakończenia przed rozpoczęciem nowego żądania. Uzasadnienie tego wynika z zastosowania tego modułu jako ogólnego narzędzia do magazynowania danych, a nie konkretnego narzędzia do danych gracza, i jest następujące:
Trudno jest zdecydować o intuicyjnym zestawie zasad, kiedy żądanie jest bezpieczne do usunięcia z kolejki. Rozważ następującą kolejkę:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Oczekiwane zachowanie jest takie, że GetAsync() zwróci 1, ale jeśli usuniemy żądanie SetAsync() z kolejki z powodu jego zbędności przez najnowsze, zwróci 0.
Logiczny postęp polega na tym, że gdy dodawane jest nowe żądanie zapisu, należy przyciąć przestarzałe żądania tylko do najnowszego żądania odczytu. UpdateAsync(), zdecydowanie najczęstsza operacja (i jedyna używana przez ten system), może zarówno odczytywać, jak i zapisywać, więc trudno byłoby pogodzić to w tym projekcie bez dodawania dodatkowej złożoności.
DataStoreWrapper mógłby wymagać od Ciebie określenia, czy żądanie UpdateAsync() miało prawo do odczytu i/lub zapisu, ale nie miałoby to zastosowania w naszym systemie danych gracza, gdzie nie można tego określić 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 żądanie DataStoreWrapper jest składane, bieżący wątek jest wstrzymywany, aż zostanie zakończone. Jeśli usuniemy przestarzałe żądania z kolejki, musielibyśmy zdecydować, czy zwrócić false, "Usunięto z kolejki" czy nigdy nie zwracać i odrzucić aktywny wątek. Oba podejścia mają swoje wady i przenoszą dodatkową złożoność na konsumenta.
Ostatecznie naszym zdaniem proste podejście (przetwarzanie każdego żądania) jest tutaj lepsze i tworzy jaśniejsze środowisko do nawigacji w obliczu złożonych problemów, takich jak blokowanie sesji. Jedynym wyjątkiem od tego jest podczas DataModel:BindToClose(), gdzie czyszczenie kolejki staje się konieczne, aby zapisać dane wszystkich użytkowników na czas, a wartość, którą zwracają poszczególne wywołania funkcji, nie jest już bieżącą troską. Aby to uwzględnić, udostępniamy 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 oraz zapisywane do podstawowych magazynów danych tylko wtedy, gdy jest to konieczne. Możesz odczytywać i aktualizować dane gracza w pamięci natychmiast, bez potrzeby wykonywania zapytań sieciowych i unikać przekraczania limitów DataStoreService.
Aby ten model działał zgodnie z zamierzeniem, nie może być więcej niż jeden serwer, który ładuje 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 zablokowania podczas ostatecznego zapisu. Bez mechanizmu blokowania serwer B mógłby załadować nieaktualne dane gracza z magazynu danych, zanim serwer A zdążyłby 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 swojego następnego zapisu.
Nawet jeśli Roblox pozwala, aby klient był połączony tylko z jednym serwerem w danym czasie, nie można 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 opuszcza serwer A:
- Serwer A wykonuje żądanie DataStore, aby zapisać swoje dane, ale żądanie nie powiodło się i wymaga kilku ponowień, aby zakończyć pomyślnie. W trakcie okresu ponawiania gracz dołącza do serwera B.
- Serwer A wykonuje zbyt wiele wywołań UpdateAsync() do tego samego klucza i zostaje ograniczony. Ostateczne żądanie zapisu jest umieszczane w kolejce. Gdy żądanie jest w kolejce, gracz dołącza do serwera B.
- Na serwerze A niektóre kody połączone z wydarzeniem PlayerRemoving wstrzymują się przed zapisaniem danych gracza. Zanim ta operacja się zakończy, gracz dołącza do serwera B.
- Wydajność serwera A spadła do tego stopnia, że ostateczny zapis jest opóźniony 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 jednego serwera 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 wykonać działania, które nie będą się utrzymywać. Może to mieć szczególny wpływ w grach, które pozwalają graczom na handel i jest powszechnym źródłem exploitów duplikacji przedmiotów.
Blokowanie sesji rozwiązuje tę lukę, zapewniając, że gdy klucz DataStore gracza jest po raz pierwszy odczytywany przez serwer, serwer atomowo zapisuje blokadę w metadanych klucza w tym samym wywołaniu UpdateAsync(). Jeśli ta wartość blokady jest obecna, gdy jakikolwiek inny serwer próbuje odczytać lub zapisać klucz, serwer nie kontynuuje.
Podejście

SessionLockedDataStoreWrapper jest meta-opakowaniem wokół klasy DataStoreWrapper. DataStoreWrapper zapewnia funkcjonalność kolejkowania i ponawiania, którą SessionLockedDataStoreWrapper uzupełnia o blokowanie 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 pozwala na atomowe odczytywanie i zapisywanie klucza. Możliwe jest również porzucenie zapisu na podstawie odczytanej wartości, zwracając nil w funkcji transformacji.
Funkcja transformacji przekazywana 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 jest. "Bezpieczny do uzyskania dostępu" oznacza:
Obiekt metadanych klucza nie zawiera nieznanej wartości LockId, która została ostatnio zaktualizowana mniej niż czas wygaśnięcia blokady temu. To uwzględnia poszanowanie blokady nałożonej przez inny serwer i ignorowanie tej blokady, jeśli wygasła.
Jeśli ten serwer wcześniej umieścił własną wartość LockId w metadanych klucza, to wartość ta nadal znajduje się w metadanych klucza. To uwzględnia sytuację, w której inny serwer przejął blokadę tego serwera (przez wygaśnięcie lub siłę) i później ją zwolnił. Alternatywnie, nawet jeśli LockId jest nil, inny serwer mógłby nadal zastąpić i usunąć blokadę w czasie, gdy zablokowałeś klucz.
UpdateAsync wykonuje operację DataStore, o którą prosił konsument SessionLockedDataStoreWrapper. 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ć przy następnym dostępie do klucza. Jeśli serwer już ma blokadę na tym kluczu, nie wprowadza żadnych zmian. Planowane jest również zadanie, aby ostrzec Cię, jeśli nie uzyskasz ponownie dostępu do klucza, aby utrzymać blokadę w czasie wygaśnięcia blokady.
Jeśli klucz ma być odblokowany, UpdateAsync usuwa LockId w metadanych klucza.
Niestandardowy handler ponawiania jest przekazywany do podstawowego DataStoreWrapper, aby operacja była ponawiana, jeśli została przerwana na kroku 1 z powodu zablokowanej sesji.
Niestandardowa wiadomość o błędzie jest również zwracana do konsumenta, co pozwala systemowi danych gracza zgłosić alternatywny błąd w przypadku blokady sesji do klienta.
Zastrzeżenia
Reżim blokowania sesji polega na tym, że serwer zawsze zwalnia swoją blokadę na klucz, gdy skończy z nim pracować. Powinno to zawsze następować poprzez instrukcję odblokowania klucza jako część ostatecznego zapisu w PlayerRemoving lub BindToClose().
Jednak odblokowanie może nie powieść się w pewnych sytuacjach. Na przykład:
- Serwer uległ awarii lub DataStoreService był nieoperacyjny podczas wszystkich prób uzyskania dostępu do klucza.
- Z powodu błędu w logice lub podobnego błędu instrukcja odblokowania klucza nie została wydana.
Aby utrzymać blokadę na kluczu, musisz regularnie uzyskiwać do niego dostęp tak długo, jak jest załadowany w pamięci. Zwykle odbywa się to jako część pętli automatycznego zapisu działającej w tle w większości systemów danych gracza, ale ten system również udostępnia metodę refreshLockAsync, jeśli musisz to zrobić ręcznie.
Jeśli czas wygaśnięcia blokady został przekroczony bez aktualizacji blokady, to każdy serwer ma prawo przejąć blokadę. Jeśli inny serwer przejmuje blokadę, próby bieżącego serwera odczytu lub zapisu klucza kończą się niepowodzeniem, chyba że ustanowi nową blokadę.
Przetwarzanie produktów dewelopera
Singleton: ReceiptHandler
Tło
Wywołanie zwrotne ProcessReceipt pełni kluczową rolę w określaniu, kiedy sfinalizować zakup. ProcessReceipt jest wywoływane w bardzo specyficznych scenariuszach. Aby uzyskać zestaw gwarancji, zobacz MarketplaceService.ProcessReceipt.
Chociaż definicja "obsługi" zakupu może różnić się w zależności od gier, używamy następujących kryteriów:
Zakup nie został wcześniej obsłużony.
Zakup jest odzwierciedlony w bieżącej sesji.
To wymaga przeprowadzenia następujących operacji przed zwróceniem PurchaseGranted:
- Weryfikacja, czy PurchaseId nie został już zarejestrowany jako obsłużony.
- Przyznanie zakupu w danych gracza przechowywanych w pamięci.
- Zarejestrowanie PurchaseId jako obsłużonego w danych gracza przechowywanych w pamięci.
- Zapisanie danych gracza przechowywanych w pamięci do DataStore.
Blokowanie sesji upraszcza ten proces, ponieważ nie musisz już martwić się o następujące scenariusze:
- Dane gracza w pamięci na bieżącym serwerze mogą być nieaktualne, co wymagałoby pobrania najnowszej wartości z DataStore przed weryfikacją historii PurchaseId.
- Wywołanie zwrotne dla tego samego zakupu działające na innym serwerze, co wymagałoby zarówno odczytu, jak i zapisu historii PurchaseId oraz zapisu zaktualizowanych danych gracza z odzwierciedlonym zakupem atomowo, aby zapobiec warunkom wyścigu.
Blokowanie sesji gwarantuje, że jeśli próba zapisu do DataStore gracza jest udana, żaden inny serwer nie odczytał ani nie zapisał pomyślnie do DataStore gracza między załadowaniem danych a zapisaniem ich na tym serwerze. Krótko mówiąc, dane gracza w pamięci na tym serwerze są najnowszą dostępną wersją. Istnieją pewne zastrzeżenia, ale nie wpływają one 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ż ten system korzysta z blokowania sesji, ta kontrola również weryfikuje, ż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), poczekaj na załadowanie danych gracza. System nasłuchuje również na opuszczenie gry przez gracza przed załadowaniem jego danych, ponieważ nie powinien wstrzymywać się w nieskończoność i blokować ponownego wywołania tego wywołania zwrotnego na tym serwerze dla tego zakupu, jeśli gracz ponownie dołączy.
Weryfikacja, że PurchaseId nie jest już zarejestrowany jako przetworzony w danych gracza.
Dzięki blokowaniu sesji tablica PurchaseIds, którą system ma w pamięci, jest najnowszą wersją. Jeśli PurchaseId jest zarejestrowany jako przetworzony i odzwierciedlony w wartości, która została załadowana lub zapisana do DataStore, zwróć PurchaseGranted. Jeśli jest zarejestrowany jako przetworzony, ale nie odzwierciedlony w DataStore, zwróć NotProcessedYet.
Zaktualizuj dane gracza lokalnie na tym serwerze, aby "przyznać" zakup.
ReceiptProcessor przyjmuje ogólne podejście do wywołań zwrotnych i przypisuje różne wywołania zwrotne dla każdego DeveloperProductId.
Zaktualizuj dane gracza lokalnie na tym serwerze, aby przechować PurchaseId.
Złóż żądanie zapisu danych w pamięci do DataStore, zwracając PurchaseGranted, jeśli żądanie jest udane. Jeśli nie, zwróć NotProcessedYet.
Jeśli to żądanie zapisu nie jest udane, późniejsze żądanie zapisu danych sesji gracza może nadal zakończyć się sukcesem. 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 Roblox. 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
- Replikacja zmian w danych gracza do klienta, aby kod klienta mógł uzyskać do nich dostęp
- Replikacja błędów ładowania i/lub zapisywania do klienta, aby mógł wyświetlać okna dialogowe z błędami
- Okresowe zapisywanie danych gracza, gdy gracz opuszcza grę i gdy serwer się zamyka
Ładowanie danych gracza

SessionLockedDataStoreWrapper wykonuje żądanie getAsync do magazynu danych.
Jeśli to żądanie nie powiedzie się, używane są dane domyślne, a profil jest oznaczany jako "błędny", aby upewnić się, że nie zostanie zapisany w magazynie danych później.
Alternatywną opcją jest wyrzucenie gracza, ale zalecamy pozwolenie graczowi na grę z danymi domyślnymi i jasne komunikowanie, co się wydarzyło, zamiast usuwania go z gry.
Początkowy ładunek jest wysyłany do PlayerDataClient, zawierający załadowane dane i status błędu (jeśli występuje).
Wszelkie wątki wstrzymane za pomocą waitForDataLoadAsync dla gracza są wznawiane.
Zapewnienie interfejsu dla kodu serwera
- PlayerDataServer jest singletonem, 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łownik kluczy i wartości. Możesz manipulować tymi wartościami na serwerze za pomocą metod setValue, getValue, updateValue i removeValue. Te metody działają synchronizacyjnie, bez wstrzymywania.
- Metody hasLoaded i waitForDataLoadAsync są dostępne, aby upewnić się, że dane zostały załadowane przed ich uzyskaniem. Zalecamy zrobienie tego raz podczas ekranu ładowania przed uruchomieniem innych systemów, aby uniknąć konieczności sprawdzania błędów ładowania przed każdą interakcją z danymi na kliencie.
- Metoda hasErrored może zapytać, czy początkowe ładowanie gracza nie powiodło się, co spowodowało użycie danych domyślnych. Sprawdź tę metodę przed zezwoleniem graczowi na dokonywanie jakichkolwiek zakupów, ponieważ zakupy nie mogą być zapisywane w danych bez pomyślnego załadowania.
- Sygnalizator playerDataUpdated wyzwala się z player, key i value za każdym razem, gdy dane gracza są zmieniane. Poszczególne systemy mogą subskrybować to.
Replikacja zmian do klienta
- Każda zmiana 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ę do uzyskania wartości klucza (get) oraz sygnalizator, który wyzwala się, gdy jest aktualizowany (updated). Zawiera również metodę hasLoaded i sygnalizator loaded, aby klient mógł czekać na załadowanie danych i replikację przed rozpoczęciem swoich systemów
- PlayerDataClient jest singletonem, który może być wymagany i dostępny przez dowolny kod klienta działający w tym samym środowisku
Replikacja 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 sygnalizatorami loaded i saved.
- Istnieją dwa rodzaje błędów: DataStoreError (żądanie DataStoreService nie powiodło się) i SessionLocked (zobacz Blokowanie sesji).
- Użyj tych zdarzeń, aby wyłączyć monity o zakupy w kliencie i wdrożyć okna dialogowe ostrzegawcze. Ten obrazek pokazuje przykład okna dialogowego:

Zapisywanie danych gracza

Gdy gracz opuszcza grę, system wykonuje następujące kroki:
- Sprawdza, czy bezpiecznie jest zapisać dane gracza w magazynie danych. Scenariusze, w których byłoby to niebezpieczne, obejmują niepowodzenie ładowania danych gracza lub nadal trwające ładowanie.
- Składa żądanie przez SessionLockedDataStoreWrapper, aby zapisać bieżącą wartość danych w pamięci do magazynu danych i usunąć blokadę sesji po zakończeniu.
- Czyści 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 to bezpieczne do zapisania). Ta mile widziana redundancja łagodzi utratę w przypadku awarii serwera i jest również konieczna do utrzymania blokady sesji.
Próbka uruchamia jedną wspólną pętlę po AUTO_SAVE_INTERVAL sekundach (domyślnie 180) i następnie zapisuje każdego załadowanego gracza równolegle. Ta pętla nie przesuwa serwerów ani graczy, więc serwery, które zaczynają w podobnym czasie, mogą flushować razem.
Przesuń pierwszy zapis każdego gracza o losowy czas w obrębie interwału, aby na żywo serwery nie zapisywały wszystkich w tym samym czasie:
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)endGdy otrzymane zostanie żądanie zamknięcia serwera, następuje to w wywołaniu zwrotnym BindToClose:
- Składane jest żądanie zapisu danych każdego gracza na serwerze, zgodnie z procesem, który normalnie przechodzi, gdy gracz opuszcza serwer. Te żądania są składane 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 podstawowego DataStoreWrapper (zobacz Ponowienia).
- Wywołanie zwrotne nie zwraca, dopóki wszystkie żądania nie zostaną zakończone.