Tło
Roblox zapewnia zestaw API do interfejsu z przechowywaniami danych poprzez DataStoreService. Najczęstszym przypadkiem użycia tych API jest zapis, ładowanie i replikacja danych gracza. To jest, dane związane z postępu gracza, zakupów i innych cech sesji, które trwają między poszczególnymi sesjami gry.
Większość doświadczeń na Robloxu używa tych API, aby zaimplementować pewną formę systemu danych gracza. Te implementacje różnią się w swoim podejściu, ale ogólnie szukają rozwiązania tego samego zestawu problemów.
Zwykłe problemy
Poniżej znajdują się niektóre z najczęstszych problemów, które systemy danych graczy próbują rozwiązać:
W dostępie do pamięci: DataStoreService wysyłania prośb o dostęp do sesja, które działają w trybie przepustowości i są podatne na ograniczenia prędkości. To odpowiednie dla początkowego ładowania podczas rozgrywka, ale nie dla wysokiej szybkości czytania i pisania w trakcie
- Początkowe czytanie podczas sesja
- Koniec sesja
- Okresy piszą interwał, aby zmniejszyć ryzyko uszkodzenia końcowego pisania
- Pisze, aby zapewnić, że dane są zapisane podczas przetwarzania kupować
Skuteczne przechowywanie: Przechowywanie wszystkich danych sesji gracza w jednej tabeli umożliwia aktualizację wielu wartości atomowo i obsługę tej samej ilości danych w mniejszej liczbie wniosków. Usuwa również ryzyko niezsynchronizacji wartości i ułatwia zarządzanie tą samą ilością danych w mniejszej liczbie wniosków.
Niektórzy programiści wdrożili również niestandardową serIALizację, aby kompresować duże struktury danych (zwykle zapisywane w grze użytkownik generowany).
Replikacja: Klient potrzebuje regularnego dostępu do danych gracza (na przykład, aby aktualizować interfejs). Standardowy 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. Rozwój często chce opcji selektywnego tego, co jest i nie jest replikowane do klienta.
Handling błędów: Gdy DataStores nie może być dostępny, większość rozwiązań będzie wdrożony mechanizm ponownego wykonania i zapasowy na dane „domyślne”. Specjalna ostrożność jest wymagana, aby upewnić się, że ponowne wykonanie danych nie pisze „prawdziwych” danych, a to jest komunikowane do gracza właściwie.
Próby ponowne: Gdy magazyny danych są niedostępne, w większości rozwiązań wdrożono mechanizm ponownego wykonania i zapasną alternatywę dla danych domyślnych. Bądź szczególnie ostrożny, aby upewnić się, że zapasowe dane nie później ponadpiszą "prawdziwe" dane, i komunikuj sytuację graczowi odpowiednio.
Zablokowanie sesji: Jeśli dane jednego gracza zostaną załadowane i zachowane w pamięci na wielu serwerach, mogą pojawić się problemy z zapisem niezaktualizowanych informacji. Może to prowadzić do utraty danych i powszechnych luków duplikacji przedmiotów.
Przetwarzanie zakupów atomowych: Zweryfikuj, nagraj i zapisz zakupy atomowo, aby zapobiec utracie lub nagrodzeniu przedmiotów wielokrotnie.
Kod przykładowy
Roblox ma kod referencyjny, aby pomóc Ci w projektowaniu i budowaniu systemów danych graczy. Reszta tej strony bada tło, szczegóły implementacji i ogólne ograniczenia.
Po imporcie modelu do Studio powinieneś zobaczyć następującą strukturę katalogu:
Architektura
Ten wysokoziomowy diagram ilustruje systemy kluczowe w próbce i jak one się łączą z kodem w pozostałej części doświadczenia.
Spróbuj ponownie
Klasa: DataStoreWrapper >
Tło
Ponieważ DataStoreService oprócz wszystkich żądań witryny pod maską, jego żądania nie są gwarantowane, aby się udać. Gdy tak się stanie, metody DataStore rzucają błędy, pozwalając Ci na ich poradzenie sobie.
Zwykły "dorzuciłem" może się zdarzyć, jeśli spróbujesz poradzić sobie z błędami przechowywania danych, takimi jak ten:
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
Podczas gdy jest to doskonale ważny mechanizm ponownego wykonania dla funkcji generycznej, nie jest odpowiedni dla DataStoreService żądań, ponieważ nie gwarantuje porządku, w jaki sposób żądania są składane. Przechowywanie porządku żądań jest ważne dla DataStoreService żądań, ponieważ interakcja ze stanem. Rozważ następujący schemat:
- Zapytanie A jest wykonane, aby ustawić wartość klucza K na 1.
- Prośba nie powiodła się, więc zaplanowano ponowne wykonanie w 2 sekundy.
- Przed ponownym wykonaniem wymaga, aby B ustawił wartość K do 2, ale ponowne wykonanie wymaganego wniosku A natychmiastowo ponadpisuje tę wartość i ustawia K na 1.
Mimo że UpdateAsync działa na najnowszej wersji wartości klucza, UpdateAsync wciąż musi być przetwarzany, aby uniknąć nieprawidłowych stanów przejściowych (na przykład, zakup odejmuje monety przed dodaniem monety, co prowadzi do monet negatywnych).
Nasz system danych gracza używa nowej klasy, DataStoreWrapper, która zapewnia wydajne ponowne próby, które są gwarantowane do przetwarzania według klucza.
Podejście
DataStoreWrapper dostarcza metody odpowiadające metodom DataStore : DataStore:GetAsync() , 0> Class.GlobalDataStore:SetAsync()|DataStore:SetAsync
Te metody, gdy są wywołane:
Dodaj wniosek do kolejki. Każdy klucz ma własną kolejkę, w której wnioski są przetwarzane w kolejności i seriami. Wątek wnioskujący wygrywa, dopóki wniosek nie zostanie zakończony.
Ta funkcjonalność opiera się na klasie ThreadQueue, która jest kalkulatorem czasu i limitatorem zasadniczości. Zamiast zwracać obietnicę, ThreadQueue wyświetla bieżący wątek do końca operacji i wyrzuca błąd, jeśli się nie powoduje. To jest bardziej spójne z idiomowymi wątkami Lua.
Jeśli wniosek nie powiódł się, ponownie próbuje się z konfigurowalnymi błędami trybicowymi. Te ponowne próby są częścią zwracanego wezwania do ThreadQueue, więc są one gwarantowane do zakończenia przed następnym wnioskiem w kolejce dla tego klucza.
Gdy wniosek jest zakończony, metoda wniosku success, result zwraca wzór
DataStoreWrapper również wyświetla metody uzyskania długości kolejki dla określonego klucza i usuwania starych żądań. Opcja ta jest szczególnie przydatna w sytuacjach, gdy serwer jest zamykany i nie ma czasu na przetwarzanie żadnych, ale najnowszych żądań.
Ograniczenia
DataStoreWrapper podąża za zasadą, że w każdym wypadku wymaga się zezwolenia na ukończenie (z sukcesem lub inaczej) każdego zapytania o dane, nawet jeśli bardziej nowy zapytanie staje się nieaktualny. Gdy pojawia się nowy zapytanie, stale zapytania nie są usuwane z kolej
Trudno jest zdecydować się na intuicyjny zestaw zasad, gdy wniosek jest bezpieczny do usunięcia z koszyka. Rozważaj następujący koszyk:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Oczekiwanym zachowaniem jest, że GetAsync() powróciłby 1 , ale jeśli usuniemy prośbę SetAsync() z kolejki ze względu na to, że jest ona wykonana z redundantnego źródła, powróci 1> 01> .
Logiczną progressją jest to, że gdy nowy wniosek o zapis dodany, tylko prune stale wnioski, ponieważ najnowszy prośbao czytanie jest tak daleko, jak najnowszy wniosek o czytanie. UpdateAsync() , przez far far najczęstsza operacja (i jedyna używana przez ten system), może zarówno czytać, jak i pisać, więc trudno
DataStoreWrapper może wymagać, abyś określił, czy prośba UpdateAsync() pozwala na czytanie i/lub zapisanie, ale nie będzie miała zastosowania do naszego systemu danych gracza, gdzie to nie może być określone przed czasem ze względu na mechanizm zabezpieczenia sesji (opisany później).
Gdy zostanie usunięty z konsoli, trudno zdecydować się na zasadę intuicyjną dla jak to powinno być załatwione. Gdy wysywany jest wątek DataStoreWrapper, obecny wątek jest wydany, aż do jego zakończenia. Jeśli usuniemy przestarza
Ostatecznie naszym poglądem jest, że prosty podejście (przetwarzanie każdego prośba) jest tutaj preferowane i tworzy wyraźniejszy środowisko do przeglądania, gdy zbliżasz się do złożonych problemów, takich jak zatrzymanie sesji. Jedyne wyjątki do
Zabezpieczanie sesji
Klasa: SessionLockedDataStoreWrapper
Tło
Dane gracza są przechowywane w pamięci na serwerze i są tylko czytane i zapisane w podstawowych magazynach danych, gdy to konieczne. Możesz czytać i aktualizować dane gracza w pamięci natychmiastowo bez potrzeby wymagania web request i uniknąć przekroczenia limitów DataStoreService.
Aby ten model działał tak, jak sobie to wyobrażamy, niezbędne jest, aby nie więcej niż jeden serwer był w stanie załadować dane gracza z pamięci z DataStore w tym samym czasie.
Na przykład, jeśli serwer A ładował dane gracza, serwer B nie może ładować tych danych, dopóki serwer A nie uwolnił swojego zamku podczas ostatecznego zapisu. Bez mechanizmu zabezpieczania, serwer B może ładować nieaktualne dane z magazynu danych, zanim serwer A będzie miał szansę zapisać najnowszą wersję, któr
Mimo że Roblox umożliwia tylko jednemu klientowi połączenie się z jednym serwerem na raz, nie można założyć, że dane z jednej sesji zawsze są zapisywane przed następną sesją. Rozważaj następujące scenariusze, które mogą się zdarzyć, gdy gracz opuści serwer A:
- Serwer A wykonuje DataStore żądanie zapisu ich danych, ale żądanie nie powiodło się i wymaga kilku ponownych prób, aby ukończyć. W okresie ponownego próbowania gracz dołącza do serwera B.
- Serwer A wykonuje zbyt wiele UpdateAsync() wezwaniań do tej samej klucz i jest gwałtownie ograniczony. Prośba ostatecznego zapisu jest umieszczona w kolejce. Podczas gdy prośba jest w kolejce, gracz dołącza do serwera B.
- Na serwerze A, niektóry kod powiązany z wydarzeniem PlayerRemoving wyświetla się przed zapisem danych gracza. Przed ukończeniem tej operacji gracz dołącza do serwera B.
- Wydajność serwera A została pogorszona do tego stopnia, że ostatni zapis zostanie opóźniony do czasu dołączenia gracza 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 połącza się z innym w szybkiej kolejności (na przykład, podczas teleportacji). Niektórzy złowieszczy użytkownicy mogą nawet próbować wykorzystać to zachowanie, aby ukończyć działania bez trwałości. To może być szczególnie skuteczne
Zablokowanie sesji blokuje tę wadę, zapewniając, że gdy klucz gracza DataStore jest najpierw czytany przez serwer, serwer atomowo zapisuje klucz metadanych wewnątrz tego samego UpdateAsync() wezwania. Jeśli ta wartość zablokowania jest obecna, g
Podejście
SessionLockedDataStoreWrapper jest meta-Wraperem wokół klasy DataStoreWrapper. Dostarcza funkcje kłęczenia i ponownego próbowania, które uzupełniają sesję zablokowaną.
SessionLockedDataStoreWrapper przeprowadza każd
Funkcja transformacji przekształciła się w UpdateAsync dla każdego zapytania wykonuje następujące operacje:
Zweryfikuje, czy klucz jest bezpieczny do dostępu, porzucając operację, jeśli nie jest. "Bezpieczny do dostępu" oznacza:
Obiekt metadanych klucza nie zawiera nieznanego wartości LockId , które zostało ostatnio aktualizowane mniej niż czas wygasania zegara. To uwzględnia poszanowanie klucza postawionego przez inny serwer i ignorowanie tego klucza, jeśli wygasł.
Jeśli ten serwer umieścił własną wartość LockId w metadanych klucza wcześniej, to wartość ta nadal jest w metadanych klucza. To kryje sytuację, w której inny serwer przejął klucz tego serwera (przez wygaszenie lub siłą) i później go opublikował. Alternatywnie zapis
Class.GlobalDataStore:UpdateAsync()|UpdateAsync wykonuje operację DataStore proszonego przez konsumenta SessionLockedDataStoreWrapper. Na przykład, 0> Class.GlobalDataStore:GetAsync()|GetAsync()0> tłumaczy się na UpdateAsync3> .
W zależności od przekazanych parametrów w wersji prośba, UpdateAsync zablokowuje lub odblokuje klucz:
Jeśli klucz jest zablokowany, UpdateAsync ustawia LockId w metadanych klucza na GUID. Ten GUID jest zapisywany w pamięci na serwerze, aby można go było sprawdzić następnym razem, gdy uzyska dostęp do kl
Jeśli klucz ma być odblokowany, UpdateAsync usuwa LockId w metadanych klucza.
Dostosowany tryb ponownego wykonania jest przekazany do podstawowego DataStoreWrapper, aby operacja została ponownie wykonana, jeśli zostanie przerwana na kroku 1 z powodu zablokowania sesji.
Dostosowany błąd jest również zwracany konsumentowi, umożliwiając systemowi danych gracza zgłoszenie alternatywnego błędu w przypadku zacięcia sesji na klienta.
Ograniczenia
Tryb zatrzymywania sesji opiera się na serwerze zawsze uwalniającym swój zamek na kluczu, gdy to się stanie z nim. To powinno zawsze następować poprzez instrukcję, aby odblokować klucz jako część ostatecznego zapisu w PlayerRemoving lub BindToClose() .
Jednak odblokowanie może nie działać w niektórych sytuacjach. Na przykład:
- Serwer uległ awarii lub DataStoreService był niedostępny dla wszystkich prób dostępu do klucza.
- Ze względu na błąd w logicznej lub podobnym błędzie nie zostano udzielone instrukcje odblokowania klucza.
Aby utrzymać zamek na kluczu, musisz go regularnie uzyskać, dopóki jest on ładowany w pamięci. To zwykle jest robione jako część automatycznego zapisu pamięci, ale ten system wyświetla również metodę refreshLockAsync, jeśli musisz to zrobić ręcznie.
Jeśli czas wygaszenia zadka zostanie przekroczony bez aktualizacji zadka, to każdy serwer jest wolny do przejęcia zadka. Jeśli inny serwer bierze zamek, próby przez bieżący serwer do czytania lub pisania klucza nie powodują jego utraty, chyba że ustanowi nowy zamek.
Przetwarzanie produktu dla programisty
Singleton: ReceiptHandler >
Tło
Kolega ProcessReceipt wykonuje krytyczną pracę określenia czasu końca kupować. ProcessReceipt jest wzywany w bardzo sprecyzyjnych sytuacjach. Dla jego zestawu gwarancji, zobacz 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 obecnej sesja.
Wymaga to przeprowadzenia następujących operacji przed powrotem PurchaseGranted :
- Zweryfikuj, czy PurchaseId nie został już zapisany jako ustawiony.
- Nagroda za zakup w danych gracza w pamięci.
- Zapisz PurchaseId jako poręcznie w grze w pamięci gracza.
- Napisz dane gracza w pamięci gracza do DataStore .
Zablokowanie sesji upraszcza ten przepływ, ponieważ nie musisz się już martwić o następujące scenariusze:
- Potencjalnie nieaktualne dane gracza w obecnym serwerze wymagające od Ciebie pobrania najnowszej wartości z DataStore przed weryfikacją historii PurchaseId
- Zwrotka akcji dla tej samej transakcji, która działa na innym serwerze, wymagająca, abyś czytał i zapisywał historię PurchaseId i zapisywał aktualizowane dane gracza z zakupem odzwierciedlonym atomowo, aby zapobiec warunkom wyścigu
Zabezpieczenia sesji, które gwarantują, że, jeśli próba pisania na graczu Class.GlobalDataStore|DataStore jest udana, żaden inny serwer nie czyta lub nie zapisuje prawidłowo gracza Class.GlobalDataStore|DataStore między ładowaniem i zapisem tych danych na tym serwerze. W skrócie, dane gracza w pamięci
Podejście
Komentarze w ReceiptProcessor opisują podejście:
Zweryfikuj, że dane gracza są obecnie załadowane na tym serwerze i że załadowano je bez błędów.
Ponieważ ten system używa zautomatyzowanego zamykania sesji, ten czek potwierdza również, że dane w pamięci są najnowszą wersją.
Jeśli dane gracza nie zostały jeszcze załadowane (co zazwyczaj następuje, gdy gracz dołącza do gry), czekaj na załadowanie danych gracza. System słucha również gracza opuszczającego grę, zanim jego dane zostaną załadowane, aby nie powodować niezdefiniowanego zatrzymywania i ponownego uruchomienia tego węzła dla tego zakupu, jeśli gracz dołączy się ponownie do gry.
Zweryfikuj, czy PurchaseId nie jest już zapisany jako przetwarzany w danych gracza.
Ze względu na zabezpieczenie sesji, w przypadku zapisu PurchaseIds w pamięci jest najnowsza wersja. Jeśli zapis PurchaseId z
Aktualizuj dane gracza lokalnie na tym serwerze, aby „nagrodzić” kupować.
ReceiptProcessor przyjmuje ogólny podejście do zwrotów i przypisuje różny zwrot dla każdego DeveloperProductId .
Aktualizuj dane gracza lokalnie na tym serwerze, aby zapisać PurchaseId .
Przesyłaj prośbę o zapisanie danych w pamięci do DataStore, zwracając PurchaseGranted jeśli prośba jest udana. Jeśli nie, zwracaj NotProcessedYet.
Jeśli ta prośba o zapisie nie powiodła się, późniejsza prośba o zapisanie danych sesji gracza w pamięci może nadal zakończyć się sukcesem. Podczas następnego wezwania ProcessReceipt, krok 2 porusza tę sytuację i zwraca PurchaseGranted.
Dane gracza
Singletons: PlayerData.Server > , PlayerData.Client >
Tło
Moduły, które zapewniają interfejs dla kodu gry, aby czytać i zapisać dane sesji gracza w trybie synchronizowanym, są powszechne w doświadczeniach Roblox. Ta sekcja dotyczy PlayerData.Server i PlayerData.Client.
Podejście
PlayerData.Server i PlayerData.Client obsługują obserwuje:
- Ładowanie danych gracza w pamięci, w tym przypadków, w których nie może wczytywać
- Dostarczanie interfejsu dla kodu serwera, aby zapytany i zmieniony danych gracza
- Replikowanie zmian w danymach gracza do klienta, aby kod klienta mógł na niego uzyskać dostęp
- Replikowanie błędów ładowania i/lub zapisywania na klient, aby mógł wyświetlić dialogi błędów
- Zapisywanie danych gracza okresowo, gdy gracz odchodzi i gdy serwer się zamyka
Ładowanie danych gracza
SessionLockedDataStoreWrapper wysyłuje prośbę o getAsync do sklepdanych.
Jeśli ten zapis nie powiódł się, to użyto domyślnych danych, a profil został oznaczony jako "błądliwy", aby upewnić się, że nie zostanie on zapisany w magazynie danych później.
Alternatywą jest wyrzucenie gracza, ale zalecamy pozwolić graczowi grać z domyślnymi danymi i czystym messagingiem, aby ustalić, co się stało, zamiast ich usuwać z doświadczenia.
Początkowa ładowarka jest wysyłana do PlayerDataClient zawierająca ładowane dane i status błędu (jeśli dotyczy).
Wszystkie wątki zakończone za pomocą waitForDataLoadAsync dla gracza są wznowione.
Dostarczanie interfejsu dla kodu serwera
- PlayerDataServer jest jedynikiem, który może być wymagany i uzyskiwany przez dowolny kod serwera działający w tym samym środowisko.
- Dane gracza są zorganizowane w słowniku kluczy i wartości. Możesz manipulować tych wartości na serwerze za pomocą metod setValue, getValue, updateValue i 2>updateValue2>. Wszystkie te metody działają bez zniekształcenia bez zniekształcenia.
- Mетоды hasLoaded i waitForDataLoadAsync dostępne są, aby upewnić się, że dane zostały załadowane, zanim uzyskasz do nich dostęp. Rekomendujemy to zrobić 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 klienta.
- Metoda hasErrored może zapytać, czy pierwotne ładowanie gracza nie powiodło się, powodując, że używa domyślnych danych. Sprawdź tę metodę, zanim pozwolisz graczowi dokonać jakiejkolwiek zakupu, ponieważ zakupy nie można zapisać do danych bez udanego wczytywać.
- Znak playerDataUpdated wysyłany jest z player , key i 2>value2> za każdym razem, gdy dane gracza są zmienione. Systemy indywidualne mogą się do tego subskrybować.
Replikowanie zmian na klientach
- Każda zmiana danych gracza w PlayerDataServer jest replikowana do PlayerDataClient, chyba że ten klucz został oznaczony jako prywatny używając ustawienia ValueAsPrivate
- setValueAsPrivate używany jest do określenia kluczy, które nie powinny być wysyłane do klienta
- PlayerDataClient włącza metodę uzyskania wartości klucza (dostać) i sygnału, który się włącza, gdy jest aktualizowany (aktualizowany). A metoda hasLoaded i sygnał loaded są również włączone, więc klient może czekać na ładowanie danych i replikowanie przed uruchomieniem jego systemów
- PlayerDataClient jest jedynikiem, który może być wymagany i dostępny przez każdy kod klienta uruchomiony w tym samym środowisko
Replikowanie błędów na klienta
- Statusy błędów spotykanych podczas zapisywania lub ładowania danych gracza są replikowane do PlayerDataClient .
- Dostęp do tej informacji za pomocą metod getLoadError i getSaveError oraz sygnałów loaded i 1>saved1>.
- Są dwa rodzaje błędów: DataStoreError (żądanie DataStoreService nie powiodło się) i SessionLocked (zobacz 1> sesja zablokowana1>).
- Użyj tych wydarzeń, aby wyłączyć wątpliwości kupowania klienta i wdrożyć dialogi ostrzeżeń. Ten obraz pokazuje przykładowy dialog:
Zapisywanie danych gracza
Gdy gracz opuści grę, system podąża następujące kroki:
- Sprawdź, czy jest bezpiecznie, aby zapisać dane gracza na magazynie danych. Scenariusze, w których byłoby niebezpiecznie, to dane gracza nie ładową się lub nadal ładowane.
- Użyj SessionLockedDataStoreWrapper żądania, aby zapisać obecną wartość danych w pamięci i usunąć sesję, gdy sesja się zakończy.
- Oczyща dane gracza (i inne zmienne, takie jak metadane i statusy błędów) z pamięci serwera.
Na okresowym pętli, serwer zapisuje dane każdego gracza do magazynu danych (jeśli jest bezpieczne, aby zapisać). Ta witajs redundancji mitigates straty w przypadku awaryjnego serwera i jest również konieczne do utrzymania sesji zamknięcia.
Gdy otrzymano wniosek o zamknięcie serwera, następuje to w BindToClose zwrotnym wezwaniu:
- Żądane zostanie zapisanie danych każdego gracza na serwerze, podążając za procesem, który zwykle kończy się, gdy gracz opuszcza serwer. Te żądania są wykonane równolegle, ponieważ BindToClose zwrotów tylko ma 30 sekund na zakończenie.
- Aby przyspieszyć zapisy, wszystkie inne wnioski w kolejce każdego klucza są usuwane z podstawowego DataStoreWrapper (zobacz Próby ponowne).
- Kwota nie zwraca się do momentu zakończenia wszystkich wniosków.