Artykuł ten jak sama nazwa wskazuje będzie traktował o tym jak napisać procedury do zapisu / odczytu save game, w sumie artykuł ten można również uogulnić na zapis dowolnych danych w dowolnym programie. Na początek napiszę co osoba czytająca tego artsa powinna wiedzieć aby nie miała większych problemów ze zrozumieniem tekstu:

# umiejętność programowania obiektowego,
# znajomość / doświadczenie z zakresu dziedziczenia,
# podstawy o klasie TStream.

Ok, no to zaczynamy, to co opisuje jest moją prywatną propozycją podejścia do problemu i nie twierdze, że jest to rozwiązanie jedyne słuszne i najlepsze ale tak właśnie zrobiłem w swojej gierce i może to się komuś innemu przydać. Po pierwsze wszystkie obiekty w grze, które będziemy chcieli zapisywać powinny dziedziczyć po jednej głównej klasie, która nazwiemy TSavableObject ale zanim się nią zajmiemy zadbajmy o klasę którą będziemy dokonywać zapisu i odczytu z dysku, a nazwiemy ją TMyFileStream, i zadeklarujemy ją w następujący sposób:

Kilka objaśnień, otóż każdy od razu widzi że używamy IfDefów gdyż w czasie debugowania często może nam się przydać dodatkowa wiedza o tym co własnie zapisujemy, a gdy wydajemy produkt finalny już ta wiedza nam nie jest potrzebna i w tym momencie nasz obiekt zamienia się w zwykly TFileStream. Kilka slow o metodach: Write/Read – dziala prawie identycznie jak w TFileStream tyle, że oblicza jeszcze checksuma danych które przewijają się przez niego. LogRChecksum/ LogWChecksum powoduje zapisanie loggera (jakiegoś systemu logowania którego ozywamy) aktualnych wartości zliczonych sum oraz ich wyzerowanie. W destruktorze wyświetlamy obliczone sumy, koniec teorii czas na kod:

jak w kodzie widać za logger robi nam zwykła zmienna typy TextFile, która jest tworzona w części inicjującej unita o tak:

Ok, mamy już podstawową klasę, którą będziemy wykorzystywali do operacji wejścia / wyjścia. Teraz zapoznajmy się z TSavableObject:

Zasada ta sama co wcześniej, w trybie debug chcemy mieć możliwość wyciągnięcia trochę większej liczby informacji, a w trybie release nic nas nie interesuje więc robimy to abstrakcyjne. Ok mamy definicje teraz zerknijmy co mamy w środku:

Jak widać na załączonym rysunku w trybie debug nasza klasa będzie logowała operacje zapisu/odczytu oraz podawała w którym miejscu coś zapisała, na pierwszy rzut oka może to się wydawać zbędne ale gdy zapisujemy plik, który ma ~400 kb i na dodatek składa się z informacji o ~100 obiektach może nas czasami interesować czy np. kolejność odczytu jest taka sama lub czy w ogóle czytamy wszystkie dane. W oparciu o takie logowanie można porównać pozycje zapisu i odczytu i już wiemy czy zapisujemy tak jak chcemy i czy czytanie zaczyna się od tych samych miejsc. Ok mamy klasę macierzystą, tak jak na początku wspomniałem wszystkie obiekty które chcą coś zapisać powinny po niej dziedziczyć teraz stwórzmy sobie z dwa takie obiekty. Pierwszy nazwiemy TMaster, a drugi TSlave. Obiekt master będzie miał listę obiektów TSlave i będzie je sobie zapisywał / wczytywał a będzie to wyglądało o tak:

Ok zerknijmy szybko na to co naskrobaliśmy, mamy listę fSlaves, w której będziemy trzymali inne obiekty ponadto mamy dwie dodatkowe zmienne ,które są tylko po to aby zapisać coś jeszcze poza innymi obiektami no i przeciążamy funkcje zapisu/odczytu, teraz czas na bebechy:

Dobra małe tłumaczonko, mamy 2 dodatkowe typy, o których jeszcze nie wspomniałem TRealArray i TMasterSaveBlock pierwszy jest to tablica na jakieś głupoty jest tylko po to aby pokazać jak łatwo i bezstresowo można przechowywać różne typy danych. Drugi typ jest rekordem, w którym przechowujemy wszystkie dane, które obiekt TMaster będzie chciał zapisać. Jak widać mamy dwie funkcje Create, pierwsza jest do tworzenia obiektów gdy np. zaczynamy grę lub gdy po prostu musimy dynamicznie stworzyć obiekt, druga wersja konstruktora tworzy obiekt na podstawie danych otrzymanych ze streamu (najczęściej z pliku). Teraz zerknijmy na TSlave.

Kod w miare prosty nie powinno być problemów ze zrozumieniem. Na koniec kilka przemyśleń, do którch doszedłem podczas pisania swojego save:
1. Wszystkie obiekty, które coś chcą zapisać powinny mieć swój rekord, w którym będą trzymały dane i zapisywały go pojedynczym TStream.Write. należy unikać takich sytuacji:

Zamiast tego pakujemy wszystko do rekordu i zapisujemy za jednym zamachem. Może wydawać się, że kod jest będzie miał takie same działanie jednak nie dajcie się zwieść pozorą czasami bardzo smutne sprawy z tego wynikaja ja straciłem na tym koło 3 h.
2. Warto w rekordach typy TXXXSaveBlock dorzucić kilka nieużywanych pól np:

o co ? Ano czasami zdarza się że macie np definicje jakiegoś przedmiotu, no i wczytujecie 200 przedmiotów z pliku. Po dwóch tygodniach okazuje się że potrzebujecie jeszcze jeden parametr opisujący wszystkie przedmioty no i tu jest problem, albo robicie konwerter z jednego formatu na nowy albo wykorzystujecie pole Unused i wasze stare pliki ładnie się wczytają a po zmianie pola fUnused1 na np fWaga, wszystko ładnie się zapisze. Prosto i elegancko.
3. Nigdy nie zapominajcie o FillChar( data, SizeOf(data), 0 ); bo jak wszyscy wiemy pola są alignowane i w przerwach często siedzi smiecie dlatego warto to wywalić za wczasu, a nie później siedzieć po nocach i szukać czemy sie checkSumy nie zgadzają….
3. W TSlave.SaveBlock pokazałem jak moża zapisywać dynamicznie tworzone tablice nie polecam jednak tego sposbu napisałem go aby pokazać, że też się da, a czasami nawet trzeba…
4. Procka na checksum jest prosta jak drut ale w wielu przypadkach zdaje egzamin jak się komuś nie podoba zawsze może ją przerobić na CRC.

I to tyle, mam nadzieje, że się komuś przyda. Jak ktoś uważa że powinienem temat rozwinąć niech do mnie napisze po otrzymaniu 1e4 maili postaram się stworzyć coś bardziej wyczerpującego ;}

Autor: Toster