Plik zapisu to obietnica. Ktoś włożył w kampanię czterdzieści godzin, a ty przy każdej nowej funkcji prosisz jego plik, żeby przeżył zmianę, na którą nie wyraził zgody.
Oto jak dotrzymuję tej obietnicy w symulacji, której model danych zmienił się 42 razy, gdy ludzie już w to grali, i cztery zasady, które robią z tego nudę zamiast strachu.
Zasada, która wzięła się z błędu
We wcześniejszym projekcie miałem system zapisu, który wyglądał nieskazitelnie. Każdy plik
niósł numer wersji. Była metoda Sanitize, która chodziła przy wczytaniu i
robiła wszystko poprawnym.
To jest najgorsza klasa awarii, bo nie ma objawu. Nic się nie wywala. Pliki się wczytują. Liczba w pliku zawsze jest poprawna, bo właśnie ją zapisałeś. Dowiadujesz się wtedy, gdy naprawdę stary plik wreszcie wymaga aktualizacji, a maszynerii do tego nie ma.
Cztery zasady
| Zasada | Dlaczego |
|---|---|
| Jeden krok na wersję, w pętli | Pętla nie umie pominąć kroku. Ręcznie pisany łańcuch wywołań umie, i mój to zrobił: zatrzymał się jeden przed najnowszą wersją i zostawił późniejsze pola niezainicjowane. |
| Stare kształty zachowane dosłownie | SaveDataV1 nadal jest w kodzie. Ścieżka aktualizacji czyta prawdziwą historyczną strukturę, a nie twoje wspomnienie o niej. |
| Każdy krok stempluje własną wersję | Nie najnowszą. Krok twierdzący, że produkuje bieżącą wersję, jest pułapką dla wszystkiego, co przeczyta to pole później. |
| Każde podbicie z testem migracji | Test jest jedyną rzeczą, która dowodzi, że krok w ogóle się wykonuje. Bez niego napisałeś krok i masz nadzieję. |
Rekonstrukcja jest deklarowana, nigdy ukrywana
Prędzej czy później migracja musi wymyślić wartość, której stary format nigdy nie przechowywał. Wersja 1 nie zapisywała, kiedy kupiono sprzęt, a wersja 2 tej daty potrzebuje.
Masz trzy opcje i tylko jedna jest uczciwa. Możesz zgadnąć pochlebnie, co po cichu daje starym graczom przewagę. Możesz zgadnąć neutralnie, co nadal jest domysłem, którego nikt nie zweryfikuje. Albo wybierasz najmniej pochlebne założenie, które nadal da się obronić, i zapisujesz, co zrobiłeś.
Ta notatka kosztuje jedno pole tekstowe i sprawia, że gracz, który pyta, czemu jego sprzęt wygląda na starszy, niż pamięta, dostaje odpowiedź zamiast wzruszenia ramionami.
Pułapka: wygląda na wyliczane, a jest przyczynowe
To jest ta, która złapała mnie sześć osobnych razy w tym samym projekcie, i powód, dla którego nie ufam już własnemu osądowi co do tego, co trzeba zapisywać.
Jeśli jutro to czyta, to jest stan
Wczorajsze obciążenie serwerów wygląda na wyliczone. To liczba, którą dałoby się przeliczyć. Ale rynek czyta ją, żeby zdecydować, jak klienci czują się dzisiaj, więc jest przyczynowa: usuń ją z zapisu, a dzień po wczytaniu rozegra się inaczej.
Ten sam kształt, jeszcze pięć razy. Udziały adopcji wersji, bo jutrzejszy dryf czyta dzisiejszy podział. Otwarta kontrola regulatora, której werdykt nie został jeszcze wylosowany, bo jej pominięcie pozwala graczowi wczytać się z każdej kary w grze. Pierwotna planowana data premiery rywala i jego bieżąca zamierzona, które wyglądają prawie identycznie i nie są tym samym: przywrócenie jednej jako drugiej sprawiło, że każdy lab, który zdecydował się poczekać na lepszy sprzęt, zapominał tę decyzję i wypuszczał zgodnie z pierwotnym planem.
Testem nie jest „czy dałbym radę to przeliczyć". Jest nim czy cokolwiek jutro to czyta. Jeśli tak, idzie do pliku, jakkolwiek wyliczalne by wyglądało.
Test, który łapie to wszystko
Jeden test robi tu więcej roboty niż cała reszta zestawu. Rozegraj skryptowaną kampanię do czwartego roku. Zapisz. Wczytaj z powrotem. Rozegraj piąty rok.
Przebieg przerwany musi skończyć się identycznie jak przebieg, którego nikt nie przerwał. Jeśli wczytanie zmienia przyszłość, plik nie jest zapisem. Jest fotografią gry, która już nie istnieje.
Mój padł, a sposób, w jaki go debugowałem, jest tą częścią, którą warto ukraść.
Mierz pierwsze rozejście, nie końcową różnicę
Przez trzy dni mierzyłem, jak daleko od siebie kończyły oba przebiegi. Liczby wyglądały zachęcająco: 73,8, potem 75,9, potem 78,3. Myślałem, że zbliżam się do jednej przyczyny. Nie zbliżałem się. To były osobne błędy w tym samym płaszczu, a jeden z nich sam wprowadziłem, naprawiając inny.
Kiedy zacząłem logować pierwszy tick, w którym oba przebiegi przestały się zgadzać, błędy, które zajmowały wieczory, zaczęły zajmować minuty. Pierwsze rozejście wskazuje przyczynę. Końcowa liczba pokazuje tylko szkodę.
Wartość, która nie przeżyła własnego zapisu
Jedna awaria w tym teście nie miała nic wspólnego z brakującymi polami. Wartość wynosiła
1.0999999999999999 w pamięci i 1.1 po podróży przez plik.
Nikt nigdy by tej różnicy nie zobaczył. Piąty rok zobaczył. Wielkość, która nie przeżywa zapisania i odczytania, nie jest dobrze określonym stanem, tylko plotką o stanie, a naprawą jest zdecydowanie, jaką precyzję naprawdę masz na myśli, i zaokrąglenie do niej przed zapisem, zamiast liczenia na to, że serializator się z tobą zgodzi.
Ile to kosztuje
Mniej więcej dwadzieścia minut na podbicie wersji: jedna metoda, jeden test, jedna linia w notatkach. To cała cena.
Kupuje możliwość zmiany czegokolwiek. Przebudowałem model rynku, dodałem system bezpieczeństwa, rozbiłem zapis na nowe podobiekty i niczego nie przenumerowałem, wszystko w czasie, gdy ludzie mieli otwarte kampanie. Ani razu nie musiałem wybierać między wydaniem funkcji a zachowaniem czyichś czterdziestu godzin.
Wszystko to pochodzi ze Scaling Laws, tycoona o firmie AI zbudowanego w Unity, z systemem zapisu, krokami migracji i testem odtworzenia otwartymi w repozytorium.
Strona gry i kod źródłowy · Poradnik siostrzany: skończone, przetestowane i nieosiągalne
Pytania, które ludzie zadają
Jak zmienić format zapisu, nie psując save'ów?
Jeden krok na wersję, w pętli. Trzymaj stare kształty w kodzie. Niech każdy krok stempluje własną wersję. Dołączaj test migracji do każdego podbicia.
Jaki jest najczęstszy błąd w migracji zapisu?
Pole wersji zapisywane i nigdy nieodczytywane. Działa bez zarzutu i nie robi nic, a dowiadujesz się dopiero, gdy stary plik wreszcie wymaga aktualizacji.
Które pola naprawdę trzeba zapisywać?
Wszystko, co czyta jutro, jakkolwiek wyliczalne by wyglądało. Jeśli wczorajsze obciążenie karmi dzisiejszą reakcję, jest stanem przyczynowym i należy do pliku.
Jak sprawdzić, że wczytanie nie zmienia gry?
Zapisz w połowie kampanii, wczytaj, graj dalej i sprawdź, że przebieg kończy się identycznie jak nieprzerwany. Gdy padnie, loguj pierwszy tick rozejścia.