Warsztat.GDCompo!ProjektyMediaArtykułyQ&AForumOferty pracyPobieranie

Opisz napotkaną sytuację, a redakcja niezwłocznie znajdzie rozwiązanie!

wyślij anuluj

Delphi Save Game

Tekst został importowany z Warsztatowych artykułów. Jego oryginalnym autorem jest Toster. Jeżeli został importowany poprawnie, usuń ten szablon!

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:

{$IfDef Debug} TMyFileStream = Class(TFileStream) public destructor Destroy;override; function Write(const Buffer; Count: Longint): Longint; override; function Read(var Buffer; Count: Longint): Longint; override; procedure LogRChecksum; procedure LogWChecksum; private fRCheckSum: int64; fWCheckSum: int64; end; {$Else} TMyFileStream = TFileStream; {$EndIf}

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ć doadatkowa 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 sie w zwykly TFileStream. Kilka slow o metodach: Write/Read - dziala prawie identycznie jak w TFileStream tyle, że oblicza jeszcze checksuma danych ktore przewijaja sie przez niego. LogRChecksum/ LogWChecksum powoduje zapisanie loggera (jakiegos systemu logowania ktorego uzywamy) aktualnych wartosci zliczonych sum oraz ich wyzerowanie. W destruktorze wsyswietlamy obliczone sumy, koniec teorii czas na kod:

{ TMyFileStream } {$IfDef Debug} destructor TMyFileStream.Destroy; begin Writeln( logger, 'R-CheckSum:'+IntToHex(fRCheckSum,8) ); Writeln( logger, 'W-CheckSum:'+IntToHex(fWCheckSum,8) ); inherited; end; procedure TMyFileStream.LogRChecksum; begin Writeln( logger, 'R-CheckSum:'+IntToHex(fRCheckSum,8) ); fRCheckSum := 0; end; procedure TMyFileStream.LogWChecksum; begin Writeln( logger, 'W-CheckSum:'+IntToHex(fWCheckSum,8) ); fWCheckSum := 0; end; function TMyFileStream.Read(var Buffer; Count: Longint): Longint; var t: integer; b: PByte; begin result := inherited read(Buffer, Count); b := @Buffer; for t := 0 to count-1 do begin fRCheckSum := fRCheckSum + b^; Inc(b); end; end; function TMyFileStream.Write(const Buffer; Count: Integer): Longint; var t: integer; b: PByte; begin result := inherited write(Buffer, Count); b := @Buffer; for t := 0 to count-1 do begin fWCheckSum := fWCheckSum + b^; Inc(b); end; end; {$EndIf}

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

initialization {$IfDef Debug} AssignFile(Logger,'Logger.txt'); rewrite(Logger); {$EndIf} finalization {$IfDef Debug} CloseFile(Logger); {$EndIf} end.

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

TSavableObject = class protected {$IfDef Debug} procedure SaveState(const a:TMyFileStream);virtual; procedure LoadState(const a:TMyFileStream);virtual; {$Else} procedure SaveState(const a:TMyFileStream);virtual;abstract; procedure LoadState(const a:TMyFileStream);virtual;abstract; {$EndIf} end;

Zasada ta sama co wcześniej, w trybie debug chcemy mieć możliwość wyciągnięcia troche 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 srodku:

{ TSavableObject } {$IfDef Debug} procedure TSavableObject.LoadState(const a: TMyFileStream); begin writeln(Logger, ClassName+'[LoadState] filePos:'+IntToHex(a.Position,6)); end; procedure TSavableObject.SaveState(const a: TMyFileStream); begin writeln(Logger, ClassName+'[SaveState] filePos:'+IntToHex(a.Position,6)); end; {$EndIf}

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 wogó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:

type TRealArray = array[0..30] of Real; TMasterSaveBlock = record fInteger: Integer; fAReal: TRealArray; fSlaveCount: Integer; end; TMaster = class(TSavableObject) public constructor Create; overload; constructor Create(const a: TMyFileStream);overload; destructor Destroy;override; private fSlaves: TList; fInteger: Integer; fAReal: TRealArray; protected procedure SaveState(const a:TMyFileStream);override; procedure LoadState(const a:TMyFileStream);override; end;

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 funkcjie zapisu/odczytu, teraz czas na bebechy:

constructor TMaster.Create; var t: integer; begin fInteger := Random(100); fSlaves := TList.Create; for t := 0 to 10 do TSlave.Create(t); for t:= 0 to 30 do fAReal[t] := t*t; end; constructor TMaster.Create(const a: TMyFileStream); begin fSlaves := TList.Create; LoadState(a); end; destructor TMaster.destroy; begin while fSlaves.Count >0 do TSlave(fSlaves[0]).Free; fSlaves.Free; end; procedure TMaster.SaveState(const a:TMyFileStream);override; var data: TMasterSaveBlock; t: integer; begin FillChar( data, SizeOf(data), 0 ); //UWAGA: Wazne nie pomijac data.fInteger := fInteger; data.fAReal := fAReal; data.fSlaveCount := fSlaves.Count; a.Write( data, SizeOf(data) ); //zapis niewolnikow for t := 0 to fSlaves.Count -1 do TSlave( fSlaves[t] ).SaveState(a); end; procedure TMaster.LoadState(const a:TMyFileStream);override; var data: TMasterSaveBlock; t: integer; begin FillChar( data, SizeOf(data), 0 ); //UWAGA: Wazne nie pomijac a.Read( data, SizeOf(data) ); fInteger := data.fInteger; fAReal := data.fAReal; for t := 0 to data.fSlaveCount -1 do TSlave( fSlaves[t] ).Create(a); end;

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 przechowywujemy 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 (najcześciej z pliku). Teraz zerknijmy na TSlave.

type TSlaveSaveBlock = record fName: ShortString; fDynamicCount: Integer; end; TSlave = class(TSavableObject) public constructor Create(Const Master:TMaster;const nr:integer); overload; constructor Create(Const Master:TMaster;const a: TMyFileStream);overload; destructor Destroy;override; private fName: String; fArray: array of integer; fMaster: TMaster; protected procedure SaveState(const a:TMyFileStream);override; procedure LoadState(const a:TMyFileStream);override; end; constructor TSlave.Create(Const Master:TMaster; const nr:integer); var t: integer; begin fName := Format('Slave nr %d',[nr]); SetLength( fArray, Random(100) ); for t := 0 to High(fArray) do fArray[t] := t; Master.fSlaves.Add(self); fMaster := Master; end; constructor TSlave.Create(Const Master:TMaster;const a: TMyFileStream); begin Master.fSlaves.Add(self); fMaster := Master; LoadState(a); end; destructor TSlave.destroy; begin fArray := nil; fMaster.fSlaves.Remove(self); end; procedure TSlave.SaveState(const a:TMyFileStream);override; var data: TSlaveSaveBlock; t: integer; begin FillChar( data, SizeOf(data), 0 ); //UWAGA: Wazne nie pomijac data.fName := fName; //uwaga na dlugosc ciagu ! data.fDynamicCount := Length( fArray ); a.Write( data, SizeOf(data) ); for t := 0 to data.fDynamicCount -1 do a.Write( data[t], SizeOf(Integer) ); end; procedure TSlave.LoadState(const a:TMyFileStream);override; var data: TMasterSaveBlock; t: integer; begin FillChar( data, SizeOf(data), 0 ); //UWAGA: Wazne nie pomijac a.Read( data, SizeOf(data) ); fName := data.fName; SetLength( fArray, data.fDynamicCount); for t := 0 to data.fDynamicCount -1 do a.Read( data[t], SizeOf(Integer) ); end;

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 cos chca zapisać powinny mieć swój rekord, w którym będą trzymały dane i zapisywały go pojedyńczym TStream.Write. należy unikać takich sytuacji:

a.Write( fInteger, SizeOf(Integer); a.Write( fReal, SizeOf(real);

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:

TSlaveSaveBlock = record fName: ShortString; fDynamicCount: Integer; fUnused1: integer; fUnused2: Real; end;

Po 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 ;}

E-Mail:[email protected]
Web:Toster.ps.pl

Toster

Tekst dodał:
Artur Poznański
25.03.2006 21:56

Ostatnia edycja:
Artur Poznański
25.03.2006 21:56

Kategorie:

Aby edytować tekst, musisz się zalogować.

# Edytuj Porównaj Czas Autor Rozmiar
#1 edytuj 25.03.2006 21:56 Artur Poznański 13.38 KB
Zwykły
Do sprawdzenia
Do akceptacji
  • Napisz komentarz:
    Aby dodać swój komentarz, musisz się zalogować.
Licencja Creative Commons

Warsztat używa plików cookies. | Copyright © 2006-2017 Warsztat · Kontakt · Regulamin i polityka prywatności
build #ff080b4740 (Tue Mar 25 11:39:28 CET 2014)