Jak utworzyć plik zapisu dla gry w C ++?

33

Kończę programowanie na kurs programowania gier wideo i chcę wiedzieć, jak utworzyć plik zapisu dla mojej gry, aby użytkownik mógł grać, a następnie wrócić później. Każdy pomysł, jak to się robi, wszystko, co zrobiłem wcześniej, to programy uruchamiane pojedynczo.

Tucker Morgan
źródło
2
Możesz także użyć SQLite
Nick Shvelidze
1
@Shvelo Chociaż możesz to zrobić, wydaje się, że zwiększyłoby to złożoność, która niekoniecznie jest potrzebna.
Nate

Odpowiedzi:

38

Aby zapisać zmienne w pamięci na dysku twardym, należy użyć serializacji . Istnieje wiele rodzajów serializacji, w .NET XML jest powszechnym formatem, chociaż dostępne są serializatory binarne i JSON. Nie jestem zbytnio programistą C ++, ale szybkie wyszukiwanie ujawniło przykład serializacji w C ++:

Istnieją biblioteki, które oferują funkcje serializacji. Niektóre są wymienione w innych odpowiedziach.

Zmienne, które Cię zainteresują, prawdopodobnie będą związane ze stanem gry. Na przykład prawdopodobnie będziesz chciał znać tego rodzaju informacje

  1. Gracz grał na poziomie 3
  2. Gracz był na współrzędnych światowych X, Y.
  3. Gracz ma w plecaku trzy przedmioty
    1. Broń
    2. Zbroja
    3. jedzenie

Naprawdę nie obchodzi cię, jakie tekstury są używane (chyba że twój gracz może zmienić ich wygląd, to szczególny przypadek), ponieważ zwykle są takie same. Musisz skupić się na zapisywaniu ważnych danych gry.

Gdy zaczynasz grę, zaczynasz normalnie grę „nową” (ładuje to tekstury, modele itp.), Ale w odpowiednim czasie ładujesz wartości z pliku zapisu z powrotem do obiektu stanu gry, zastępując „domyślną” nową stan gry. Następnie pozwalasz graczowi wznowić grę.

Znacznie uprościłem to tutaj, ale powinieneś poznać ogólny pomysł. Jeśli masz bardziej szczegółowe pytanie, zadaj tutaj nowe pytanie, a my możemy spróbować Ci pomóc.

Nate
źródło
Rozumiem, co muszę zapisać, ale co chciałbym wiedzieć, jaki jest dokładny sposób, czy zapisujesz go do pliku .txt w projekcie, tych zmodyfikowanych zmiennych lub w inny sposób
Tucker Morgan
Tak, jeśli gra jest prosta, wystarczy plik tekstowy; należy pamiętać, że każdy może edytować plik tekstowy, a tym samym tworzyć własne gry do zapisywania ...
Nate
Zapisywanie plików tekstowych nie służy tylko do prostych gier. Paradox użył strukturalnego formatu tekstowego do zapisywania plików dla wszystkich gier, które stworzyli przy użyciu tego samego silnika, co flagowy silnik Europa Universalis. Szczególnie późna gra, pliki te mogą być ogromne.
Dan Neely,
1
@DanNeely W porządku, nie ma powodu, dla którego nie można używać formatu tekstowego do przechowywania wielu skomplikowanych danych, ale ogólnie mówiąc, gdy dane są tak skomplikowane, zalety innego formatu (binarne, xml itp.) Stają się bardziej wyraźne.
Nate
1
@NateBross uzgodnione. Gry Paradox były bardzo przyjazne dla modów i używały podobnego (identycznego?) Formatu dla danych scenariuszy. Przechowywanie większości danych jako tekstu oznaczało, że nie musieli inwestować w narzędzia do edytowania scenariuszy do użytku publicznego.
Dan Neely,
19

Zazwyczaj jest to specyficzne dla twojej gry. Jestem pewien, że do tej pory nauczyłeś się pisać i odczytywać pliki z twoich zajęć. Podstawową ideą jest:

  1. Wychodząc z gry, zapisz wartości, które chcesz zapisać do pliku.
  2. Podczas ładowania gry sprawdź, czy plik zapisu istnieje, a jeśli tak, załaduj odczytane wartości do swojego programu. Jeśli plik nie istnieje, kontynuuj tak jak teraz i ustaw wartości początkowe / domyślne.

To, co napiszesz, zależy od ciebie. Jednym ze sposobów zapisu jest zapisanie zmiennych w określonej kolejności w postaci strumienia bajtów. Następnie podczas ładowania wczytaj je do swojego programu w tej samej kolejności.

Na przykład (szybkim pseudo kodem):

SaveGame(FileInput file) {
    file.writeInt(playerLevel);
    file.writeInt(playerHealth);
    file.writeInt(gameProgress);
}

LoadGame(FileInput file) {
    if(file.exists()) {
        playerLevel= file.readInt();
        playerHealth = file.readInt();
        gameProgress = file.readInt();
    } else {
        playerLevel = 1;
        playerHealth = 100;
        gameProgress = 0;
    }
}
MichaelHouse
źródło
1
Ta metoda jest ładna i niewielka, choć zalecam wstawienie kilku prostych tagów dla fragmentów danych. W ten sposób, jeśli później będziesz musiał zmienić coś, co zwykle znajduje się w środku pliku, możesz to zrobić, a jedyna „konwersja ze starego”, którą musisz zrobić, znajduje się w tym jednym bloku. Nie jest to tak ważne w przypadku jednorazowego zadania, ale jeśli będziesz kontynuować pracę po tym, jak ludzie zaczną pobierać pliki do zapisania, to trochę koszmar po prostu używając prostych bajtów z pozycją jako jedynym identyfikatorem.
Lunin
1
Tak, to nie generuje bezpiecznych plików zapisywania w przyszłości. Nie działa również w przypadku danych o zmiennych rozmiarach bajtów, takich jak łańcuchy. To ostatnie jest łatwe do naprawienia, pisząc najpierw rozmiar danych, które mają zostać zapisane, a następnie wykorzystując je podczas ładowania, aby odczytać prawidłową liczbę bajtów.
MichaelHouse
6

Prawdopodobnie istnieje wiele sposobów, aby to zrobić, ale najprostszym, który zawsze znajdowałem i którego używałem zarówno osobiście, jak i zawodowo, jest stworzenie struktury zawierającej wszystkie wartości, które chcę zapisać.

struct SaveGameData
{
    int              characterLevel; // Any straight up values from the player
    int              inventoryCount; // Number of items the player has on them or stored or what not
    int[STAT_COUNT]  statistics;     // This is usually a constant size (I am tracking X number of stats)
    // etc
}

struct Item
{
    int itemTypeId;
    int Durability; // also used as a 'uses' count for potions and the like
    int strength;   // damage of a weapon, protection of armor, effectiveness of a potion
    // etc
}

Następnie po prostu zapisuję / fread dane do i z pliku przy użyciu podstawowych wartości we / wy pliku. InventoryCount to liczba struktur elementów zapisanych po głównej strukturze SaveGameData w pliku, więc wiem, ile z nich należy odczytać po pobraniu tych danych. Kluczem tutaj jest to, że gdy chcę zapisać coś nowego, chyba że jest to lista przedmiotów lub tym podobne, wszystko, co muszę zrobić, to dodać wartość do struktury gdzieś. Jeśli jest to lista elementów, będę musiał dodać przepustkę odczytu, jak już sugerowałem dla obiektów Item, licznik w nagłówku głównym, a następnie wpisy.

Ma to wadę polegającą na tym, że różne wersje pliku składowania są ze sobą niezgodne bez specjalnej obsługi (nawet jeśli są to tylko domyślne wartości dla każdego wpisu w głównej strukturze). Ale ogólnie sprawia to, że system można łatwo rozszerzyć, dodając nową wartość danych i wprowadzając wartość w razie potrzeby.

Ponownie, istnieje kilka sposobów, aby to zrobić, a to może prowadzić bardziej do C niż C ++, ale wykonało to zadanie!

James
źródło
1
Warto również zauważyć, że nie jest to niezależne od platformy, nie będzie działać dla łańcuchów C ++, ani dla obiektów, do których odwołują się odnośniki lub wskaźniki, ani żadnych obiektów zawierających dowolne z powyższych!
Kylotan
Dlaczego ta platforma nie jest niezależna? Działa dobrze na PC, PS * i 360. fwrite (pToDataBuffer, sizeof (typ danych), countOfElements, pToFile); działa dla wszystkich tych obiektów, zakładając, że można uzyskać wskaźnik do ich danych, a także rozmiar obiektu, a następnie liczbę obiektów, które chcesz zapisać .. i odczytać pasujące do tego ...
James
To jest niezależny od platformy, tam po prostu nie ma gwarancji, że pliki zapisane na jednej platformie można załadować na inną. Co jest raczej nieistotne dla np. Zapisywania danych gry. Rzeczywiście wskaźnik do danych i rozmiar memcpy może być nieco niezręczny, ale działa.
leftaroundabout
3
W rzeczywistości nie ma gwarancji, że będzie działał dla ciebie na zawsze - co się stanie, jeśli wydasz nową wersję, która jest zbudowana z nowym kompilatorem lub nawet nowe opcje kompilacji, które zmieniają wypełnienie struktury? Zdecydowanie, zdecydowanie odradzam korzystanie z raw-struct fwrite () tylko z tego powodu (nawiasem mówiąc z tego doświadczenia).
puszysty
1
Nie chodzi o „32 bity danych”. Oryginalny plakat po prostu pyta „jak zapisać moje zmienne”. Jeśli napiszesz zmienną bezpośrednio, stracisz informacje na różnych platformach. Jeśli musisz wstępnie przetworzyć przed zapisaniem, pominąłeś najważniejszą część odpowiedzi, tj. jak przetwarzać dane, aby zostały poprawnie zapisane i zawierały tylko trywialny bit, tj. wywołanie fwrite w celu umieszczenia czegoś na dysku.
Kylotan
3

Najpierw musisz zdecydować, jakie dane należy zapisać. Na przykład może to być lokalizacja postaci, jej wynik i liczba monet. Oczywiście twoja gra będzie prawdopodobnie znacznie bardziej złożona, dlatego musisz zapisać dodatkowe dane, takie jak numer poziomu i lista wrogów.

Następnie napisz kod, aby zapisać to w pliku (użyj ofstream). Stosunkowo prosty format, którego można użyć, jest następujący:

x y score coins

Tak więc plik wyglądałby następująco:

14 96 4200 100

Co oznaczałoby, że był na pozycji (14, 96) z wynikiem 4200 i 100 monet.

Musisz także napisać kod, aby załadować ten plik (użyj ifstream).


Ratowanie wrogów można wykonać, włączając ich pozycję do pliku. Możemy użyć tego formatu:

number_of_enemies x1 y1 x2 y2 ...

Najpierw number_of_enemiesjest czytany, a następnie każda pozycja jest czytana za pomocą prostej pętli.

Pubby
źródło
1

Jedno dodanie / sugestia dodałoby poziom szyfrowania do serializacji, aby użytkownicy nie mogli edytować swoich wartości tekstowych na „9999999999999999999”. Jednym z dobrych powodów jest zapobieganie przepełnieniu liczb całkowitych (na przykład).

lokówki
źródło
0

Dla kompletności chcę wspomnieć o bibliotece serializacji c ++, z której osobiście korzystam i której jeszcze nie wymieniono: zbóż .
Jest łatwy w użyciu i ma ładną, czystą składnię do serializacji. Oferuje również wiele rodzajów formatów, w których można zapisać (XML, Json, Binary (w tym wersja przenośna z szacunkiem endianess)). Obsługuje dziedziczenie i jest tylko nagłówkiem,

LukeG
źródło
0

Twoja gra naruszy struktury danych (mam nadzieję?), Które musisz przekształcić w bajty (serializować), aby móc je przechowywać. W przyszłości możesz załadować te bajty z powrotem i przekształcić je z powrotem w oryginalną strukturę (deserializacja). W C ++ nie jest to takie trudne, ponieważ refleksja jest bardzo ograniczona. Ale niektóre biblioteki mogą ci w tym pomóc. Napisałem o tym artykuł: https://rubentorresbonet.wordpress.com/2014/08/25/an-overview-of-data-serialization-techniques-in-c/ Zasadniczo proponuję, abyś spojrzał na biblioteka zbóż, jeśli możesz celować w kompilatory C ++ 11. Nie ma potrzeby tworzenia plików pośrednich, takich jak protobuf, więc zaoszczędzisz tam trochę czasu, jeśli chcesz uzyskać szybkie wyniki. Możesz także wybierać między wersją binarną i JSON, więc może to pomóc w debugowaniu tutaj.

Ponadto, jeśli bezpieczeństwo stanowi problem, możesz zaszyfrować / odszyfrować przechowywane dane, zwłaszcza jeśli używasz formatów czytelnych dla ludzi, takich jak JSON. Pomocne są tutaj algorytmy takie jak AES.

Rubén Torres Bonet
źródło
-5

Musisz użyć fstreamdo plików wejściowych / wyjściowych. Składnia jest prosta EX:

#include <fstream>
// ...
std::ofstream flux ; // to open a file in ouput mode
flux.open("myfile.whatever") ; 

Lub

#include <fstream>
// ...
std::ifstream flux ; // open a file in input mode
flux.open("myfile.whatever") ;

Możliwe są inne działania na pliku: append , binary , trunc itp. Użyłbyś tej samej składni jak powyżej, zamiast tego wstawiamy std :: ios: :( flagi), na przykład:

  • ios::out dla operacji wyjściowej
  • ios::in do operacji wprowadzania
  • ios::binary do binarnej (surowego bajtu) operacji We / Wy, zamiast opartej na znakach
  • ios::app aby zacząć pisać na końcu pliku
  • ios::trunc jeśli plik już istnieje, zastąp starą treść i zastąp nową
  • ios::ate - ustaw wskaźnik pliku „na końcu” dla wejścia / wyjścia

Dawny:

#include <fstream>
// ...
std::ifstream flux ;
flux.open("myfile.whatever" , ios::binary) ;

Oto bardziej kompletny, ale prosty przykład.

#include <iostream>
#include <fstream>

using namespace std ;

int input ;
int New_Apple ;
int Apple_Instock ;
int Eat_Apple ;
int Apple ;

int  main()
{
  bool shouldQuit = false;
  New_Apple = 0 ;
  Apple_Instock = 0 ;
  Eat_Apple = 0 ;

  while( !shouldQuit )
  {
    cout << "------------------------------------- /n";
    cout << "1) add some apple " << endl ;
    cout << "2) check apple in stock " << endl ;
    cout << "3) eat some apple " << endl ;
    cout << "4) quit " << endl ;
    cout << "------------------------------------- /n";
    cin >> input ;

    switch (input)
    {
      case 1 :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << " how much apple do you want to add /n";
        cout << "------------------------------------ /n";      
        cin >> New_Apple ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ; 

        Apple = New_Apple + Apple_Instock ;

        ofstream apple_adder ;
        apple_adder.open("apple.apl") ;
        apple_adder << Apple ;
        apple_adder.close() ;

        cout << "------------------------------------ /n";
        cout << New_Apple << " Apple has been added ! /n";
        cout << "------------------------------------ /n";
        break;
      }

      case 2 :  
      {
        system("cls") ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ;

        cout << "------------------------------------ /n";
        cout << " there is " << Apple_Instock ;
        cout << "apple in stock /n" ;
        cout << "------------------------------------ /n";
        break;
      }

      case 3 :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << "How many apple do you want to eat /n" ;
        cout << "------------------------------------ /n";
        cin >> Eat_Apple ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ;

        Apple = Apple_Instock - Eat_Apple ; 

        ofstream apple_eater ;
        apple_eater.open("apple.apl") ;
        apple_eater << Apple ;
        apple_eater.close() ;

        cout << "----------------------------------- /n";
        cout << Eat_Apple ;
        cout << " Apple has been eated! /n";
        cout << "----------------------------------- /n";
        cout << Apple << " Apple left in stock /n";
        cout << "----------------------------------- /n";
        break;
      }

      case 4 :
      {
        shouldQuit = true;
        break;
      }

      default :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << " invalide choice ! /n";
        cout << "------------------------------------ /n"; 
        break;
      }
    }
  }
  return 0;
}
Francisco Forcier
źródło
4
-1 To bardzo zła odpowiedź. Powinieneś poprawnie sformatować i wyświetlić kod i wyjaśnić, co robisz, nikt nie chce rozszyfrować fragmentu kodu.
Vaillancourt
Dziękuję Katu za komentarz, który masz rację. Powinienem lepiej wyjaśnić mój kod. Czy możesz mi powiedzieć, jak sformatować swoje źródło ze strony internetowej, ponieważ jestem nowy w tego typu
sprawach
Z tej strony, czy z tej strony? Aby uzyskać pomoc w formatowaniu postów, odwiedź stronę pomocy dotyczącą formatowania . Obok nagłówka pola tekstowego używanego do publikowania, aby Ci pomóc, jest wykrzyknik.
Vaillancourt
Spróbuj udokumentować pytania; nie musisz komentować wszystkiego. I nie wyjaśniając, co robisz, w moim komentarzu miałem na myśli, że ogólnie przedstawiasz strategię, którą sugerujesz, przynajmniej w krótkim akapicie. (np. „Jedną z technik jest użycie binarnego formatu pliku z operatorem strumienia. Musisz uważać, aby czytać i pisać w tej samej kolejności, bla bla lba”).
Vaillancourt
2
Za pomocą gotos zlinczujesz się w miejscu publicznym. Nie używaj gotos.
Vaillancourt