Zamknięcie karty przeglądarki nie powinno kasować czterech godzin postępu. To wydaje się oczywiste, a jednak wiele gier przeglądarkowych traktuje local storage po macoszemu. Gracz odblokowuje wysoki wynik, dostosowuje ustawienia, wraca następnego dnia i zastaje pustkę. Co gorsza, wraca po aktualizacji, a gra wyrzuca błąd, ponieważ plik zapisu na jego urządzeniu nie pasuje już do kodu, który właśnie wdrożyłeś. Budowanie strzelanki typu survivor w Phaser 4 oznacza walkę z nieustannymi falami przeciwników, ale prawdziwym długofalowym zagrożeniem są twoje własne przyszłe aktualizacje.

Większość deweloperów buduje swój pierwszy system zapisu, biorąc obiekt, przepuszczając go przez JSON.stringify i wrzucając do localStorage. Przy ładowaniu parsują go i przekazują z powrotem do gry w surowej formie. To działa pierwszego dnia. Przestaje działać w momencie, gdy dodasz nowe ustawienie, nową flagę odblokowania lub trzecią warstwę zagnieżdżonej konfiguracji. Jeśli powracający gracz ma stary plik zapisu, w którym brakuje właściwości vignette, a twój nowy kod oczekuje, że ona istnieje, otrzymasz undefined zamiast oczekiwanego boolean. Pomnóż to przez tuzin nowych funkcji i otrzymasz koszmar podczas debugowania, który uderzy najpierw w twoich najbardziej lojalnych graczy.

Zacznij od kontraktu, a nie od surowego obiektu

Zanim w ogóle dotkniesz localStorage, zdefiniuj domyślny schemat zapisu w swoim kodzie. Potraktuj go jak kontrakt, który każdy plik zapisu musi honorować, niezależnie od tego, czy został utworzony pięć minut temu, czy pięć miesięcy temu. Jasny punkt wyjścia może wyglądać następująco:

const defaultSave = {
  highScore: 0,
  settings: {
    screenShake: true,
    vignette: true
  }
};

Ten obiekt znajduje się w twoim kodzie źródłowym. Gdy gra się uruchamia, zawsze masz dostęp do tej struktury. Daje ci to punkt odniesienia. Wymusza to również przemyślenie struktury, zanim cokolwiek zserializujesz. Jeśli pominiesz ten krok i po prostu zapiszesz dowolny obiekt stanu, który będzie wygodny w danym momencie, skończysz z niespójnymi kluczami, brakującymi polami i cichymi błędami, gdy starsze zapisy przestaną być zgodne z twoimi oczekiwaniami.

Defensywne ładowanie za pomocą Try/Catch

Local storage to nie baza danych. To rodzaj szafki na ciągi znaków w przeglądarce i może trafić tam wszystko. Użytkownik mógł ręcznie edytować wartość, operacja zapisu mogła zostać przerwana w połowie lub rozszerzenie przeglądarki mogło wrzucić śmieci do klucza, który zadeklarowałeś. Gdy wyciągasz ten ciąg znaków i podajesz go do JSON.parse, pojedynczy uszkodzony znak spowoduje poważny wyjątek. W grze opartej na Phaser, ten nieobsłużony błąd może zamrozić sekwencję uruchamiania lub wyrzucić gracza do pustego ekranu.

Zawsze opakuj logikę odczytu i parsowania w blok try/catch. W przypadku niepowodzenia, wróć do swojego domyślnego schematu. Cel jest prosty: jeśli plik zapisu jest nieczytelny, potraktuj gracza jak nowego użytkownika, zamiast doprowadzać do awarii całej sesji. Ten jeden nawyk odróżnia projekty hobbystyczne od wersji produkcyjnych. Jego wdrożenie prawie nic nie kosztuje, a chroni cię przed tajemniczymi raportami o błędach, których nie da się powtórzyć.

Łącz stare dane z wartościami domyślnymi

Pomyślne sparsowanie nie oznacza, że jesteś bezpieczny. Nigdy nie zastępuj całkowicie swojego domyślnego obiektu wynikiem parsowania. Stary plik zapisu może nie zawierać twoich najnowszych ustawień. Może przechowywać screenShake, ale nie vignette. Jeśli logika twojej gry zakłada, że vignette istnieje, ponieważ zostało ono dodane w ostatniej aktualizacji, znów będziesz gonić błędy typu undefined.

Zamiast tego połącz wczytane dane z wartościami domyślnymi. Użyj Object.assign, aby nałożyć zapisane wartości na bazowy schemat. Wartości domyślne automatycznie wypełnią każdą brakującą lukę. Nowe właściwości, które dodałeś w wersji drugiej, otrzymają swoje początkowe wartości z obiektu domyślnego. Istniejące właściwości, które gracz faktycznie zmienił, zostaną nadpisane jego zapisanymi preferencjami. Wszyscy wygrywają. Powracający gracz zachowuje swój wysoki wynik, a gra zyskuje dostęp do nowego przełącznika, który dodałeś wczoraj, bez wywalania się.

Pamiętaj, że Object.assign wykonuje płytkie scalanie (shallow merge). Jeśli twój obiekt ustawień z czasem stanie się głęboko zagnieżdżony, możesz potrzebować nieco większej ostrożności przy obsłudze tych wewnętrznych obiektów. Mimo to zasada pozostaje ta sama: dane gracza powinny dekorować twoje wartości domyślne, a nie całkowicie je zastępować.

Wersjonuj swoje klucze

Przeglądarki nie usuwają automatycznie starych wpisów w local storage. Jeśli drastycznie zmienisz strukturę danych, potrzebujesz czystego sposobu na porzucenie starego formatu. Nazwij swój klucz przechowywania z przyrostkiem wersji. bitSurvivorsSave_v1 jest jednoznaczny. Mówi ci dokładnie, który schemat zapisał ten plik. Później, gdy przebudujesz system progresji lub dodasz pełny system ekwipunku, przejdź na bitSurvivorsSave_v2.

Daje to dwie praktyczne korzyści. Po pierwsze, nigdy przypadkowo nie sparsujesz obiektu v1 logiką v2. Po drugie, możesz napisać kod migracji, jeśli zechcesz. Przy uruchamianiu sprawdź, czy istnieje v1. Jeśli istnieje, a v2 nie, przeprowadź migrację starych danych do nowej struktury, zapisz je pod nowym kluczem i idź dalej. Jeśli nie chcesz migrować, stary klucz będzie bezpiecznie spoczywał w pamięci masowej, podczas gdy Twój nowy kod będzie go ignorował. W obu przypadkach wersjonowanie zapobiega cichej korupcji danych.

Spraw, aby zapisywanie było niewidoczne

Trwałość danych powinna być jak oddychanie. Gracz nigdy nie powinien musieć o niej myśleć. Nie dodawaj przycisku „Zastosuj” w menu ustawień. Przyciski „Zastosuj” wprowadzają tarcie i przyzwyczajają użytkowników do martwienia się o to, czy ich wybory faktycznie zostały zapisane. Powodują one również ryzyko utraty danych, gdy gracz przełączy trzy opcje, przeoczy przycisk „Zastosuj” i zamknie kartę.

Zapisuj dane w momencie wystąpienia interakcji. Gdy gracz zaznaczy pole wyboru, aby wyłączyć drżenie ekranu, natychmiast wywołaj funkcję zapisu. Gdy rozgrywka dobiegnie końca, a wynik końcowy zostanie podliczony, zapisz nowy rekord, zanim ekran „game over” skończy animację. Zapisywanie sterowane zdarzeniami sprawia, że architektura jest przewidywalna, ponieważ zapis zawsze znajduje się tuż obok akcji, która zmieniła dane. Nigdy nie musisz szukać centralnej funkcji grupowania ani martwić się o nieaktualny stan.

To podejście upraszcza również Twój model myślowy. Dokładnie wiesz, gdzie następuje zapis: w callbacku obsługującym przełącznik oraz w funkcji obsługującej śmierć. W całym kodzie nie ma żadnych tajemniczych zapisów rozproszonych w różnych miejscach.

Zbuduj przycisk resetowania dla siebie

Podczas rozwoju będziesz uszkadzać własne zapisy. Będziesz zapisywać błędne dane, testować przypadki brzegowe i będziesz potrzebować szybkiego powrotu do czystego stanu. Zaimplementuj przycisk resetowania w menu debugowania lub jako ukrytą kombinację klawiszy. Spraw, aby ten przycisk robił dwie rzeczy w dokładnie tej kolejności: resetował stan w pamięci do domyślnego schematu, a następnie natychmiast wywoływał tę samą funkcję zapisu, która zapisuje dane do local storage.

Jeśli wyczyścisz tylko lokalną zmienną i pominiesz krok zapisu, nie osiągniesz niczego. Kolejne odświeżenie strony wyciągnie stare dane z przeglądarki i je wskrzesi. Reset, który zapomina o utrwaleniu danych, to rodzaj błędu, który potrafi zmarnować całe popołudnie. Dopracuj tę sekwencję raz, a Twoja pętla testowa pozostanie szybka przez resztę projektu.

Najważniejsza lekcja

Zapisywanie nie jest funkcją, którą dokleja się na końcu. To infrastruktura, która decyduje o tym, czy Twoja gra wydaje się trwała i czy szanuje czas gracza. Gra typu survivor shooter w Phaser 4 żyje lub umiera dzięki wielokrotnym podejściom. Jeśli karta przeglądarki jest jak naładowany pistolet wymierzony w postępy gracza, w końcu przestanie on do Ciebie wracać. Stwórz schemat, chroń się przed błędnymi danymi, stosuj scalanie zamiast zastępowania, wersjonuj klucze i zapisuj dane przy każdym istotnym zdarzeniu. Twoje przyszłe „ja” oraz każdy gracz, który wróci po Twojej kolejnej aktualizacji, podziękuje Ci za to.