Spielstände speichern?



  • Hallo,

    wie funktioniert das mit der Spielstandspeicherung?

    Ich habe da bisher keine Idee, eben nur, dass man für jedes Objekt einen State speichert. Das erscheint mir aber irgendwie zu aufwändig.

    Wie machen das andere Spiele?



  • Das kommt stark auf das Spiel drauf an.

    Du musst dir überlegen, welche Daten du brauchst, um den aktuellen Spielstand wiederherzustellen.

    Nehmen wir zum Beispiel ein Jump&Run:
    Die Welt ansich muss man für gewöhnlich nicht speichern, sofern der Spieler keine Möglichkeit hat auf diese Einfluss zu nehmen. Falls doch werden das eher spezielle Objekte sein (Info ob eine Mauer zerstört ist oder nicht).

    Dann bleiben immernoch der Spieler-Charakter, Gegner und interagierbare Objekte übrig, die gespeichert werden sollen. Bei einem Jump&Run ist es aber eher üblich mit Checkpoints zu arbeiten, d.h. du speicherst lediglich die Information ab, daß der Spieler Checkpoint 7 in Level 13 erreicht hat, und startest einfach den Level an genau dieser Stelle neu. Im Idealfall reichen also genau diese beiden Werte.

    Ein Gegenbeispiel wäre z.b. ein RPG oder Strategiespiel, wo man zu jedem beliebigen Zeitpunkt speichern und fortsetzen können will. Dort müsstest du wirklich jede einzelne Information zu jedem Gegner, Gebäude, etc abspeichern, was sehr komplex werden kann.

    Ich würde auch empfehlen, von Anfang an eine Versionsnummer in dein Dateiformat mit zu speichern. Falls du irgendwann etwas am Format ändern musst (z.B. bekommen in einem Update alle Gebäude noch einen Level für Upgrades) musst du wissen ob du gerade ein altes oder ein neues Savegame lädst.



  • Also ich habe ein Buch, da steht drin, man solle das ganze mit "Katalogen" und memorydumps machen.

    Also, sofern ich das richtig verstanden habe:
    1. Ich gehe davon aus, dass jedes Objekt im Spiel in einem allokierten speicher ladent.
    also für jedes objekt den Pointer rausfinden und in eine liste speichern.

    2. dann den ram dumpen.

    beim spielstand laden:
    1. gedumptes ram-image laden
    2. pointerliste laden und an geladenes ram-image anpassen.

    was hältst du davon?

    Wie machen es denn "richtige" spiele? man muss ja auch logik usw. mitspeichern.



  • apfel-esser schrieb:

    was hältst du davon?

    Nix!
    "gedumptes ram-image" ... in welchem Buch liest Du denn so einen Müll?

    Du speicherst das was wichtig ist. Dabei ist der exakte Zustand oft nichtmal relevant, nur der grobe Fortschritt in der Spielwelt.



  • asdqweyxc schrieb:

    apfel-esser schrieb:

    was hältst du davon?

    Nix!
    "gedumptes ram-image" ... in welchem Buch liest Du denn so einen Müll?

    Du speicherst das was wichtig ist. Dabei ist der exakte Zustand oft nichtmal relevant, nur der grobe Fortschritt in der Spielwelt.

    Ja, aber nehmen wir mal an, ich habe ein open-World-Spiel, dann muss ich jeden Fligenschiss einzeln speichern!

    Und wie willst du die Logik eines Spiels am besten speichern?

    Nochmal die Frage: wie machen das Computerspiele?



  • Ja, du musst halt jeden Fliegenschiss einzeln speichern.
    Die großen Computerspiele werden so etwas wie einen Ausgabeoperator für jedes Gameobjekt definieren, was die relevanten Daten speichert. Und dann so etwas wie einen Eingabeoperator, was die Daten wieder einliest und die nicht-relevanten auf einen hardcoded Wert setzt.
    Beispiel:
    Die Position, die Geschwindigkeit und das Leben ist wichtig. Nicht aber, in welchem Frame die Animation gerade ist - die kann ruhig wieder mit 0 anfangen.

    Edit: Was genau meinst du mit Logik?



  • ...



  • Nathan schrieb:

    Ja, du musst halt jeden Fliegenschiss einzeln speichern.
    Die großen Computerspiele werden so etwas wie einen Ausgabeoperator für jedes Gameobjekt definieren, was die relevanten Daten speichert. Und dann so etwas wie einen Eingabeoperator, was die Daten wieder einliest und die nicht-relevanten auf einen hardcoded Wert setzt.
    Beispiel:
    Die Position, die Geschwindigkeit und das Leben ist wichtig. Nicht aber, in welchem Frame die Animation gerade ist - die kann ruhig wieder mit 0 anfangen.

    Edit: Was genau meinst du mit Logik?

    Z.b. dass sich eine tür erst dann öffnet, wenn der spielcharacter den endboss besiegt und mit charakter xyz gesprochen hat, und dabei die richtige dialog-option angeklickt hat.



  • Das ist hardcoded oder in nicht veränderbaren ConfigFiles gespeichert.



  • Memory-Dumps sind eine ganz, ganz, ganz, (...) GANZ schlechte Idee.
    - Du speicherst haufenweise unnötige Daten mit.
    - Du speicherst Daten, die nach Neustart des Spiels andere Werte haben (z.B. Pointer auf andere Objekte)
    - Du berücksichtigst nicht, daß sich die Datenstruktur eines Objekts seitdem das Savegame erstellt wurde, geändert haben kann. Sobald dein Charakter einen zusätzlichen Wert bekommt, hast du verloren. Darum auch auf jeden Fall dein Savegame mit einer Versionsnummer ausstatten.

    Bei meinem letzten Spiel hatte ich eine Liste von Spiel-Objekten (z.B. Gebäude, Charaktere, ...), die alle von einer gemeinsamen Basis-Klasse abgeleitet wurden. Jedes Objekt bekam eine eindeutige Nummer, die anstelle des Pointers gespeichert wurde, wenn ein Objekt auf ein anderes verwiesen hatte.
    Danach wurde für jedes Objekt eine "serialize" Funktion aufgerufen, so daß jedes Objekt für sich selbst einen Datenblock erstellt hatte, in dem seine Daten lagen.

    Beim Einlesen hatte ich dann wieder eine Liste von Datenblöcken. Über eine ID wurde bestimmt, von welchem Typ der Block erstellt wurde. Dann wurden erst einmal alle Objekte wieder erstellt und erst im zweiten Schritt jedem Objekt sein Datenblock übergeben, damit es selbst wieder seine Informationen aus dem Datenblock gelesen hatte. Falls dabei irgendwo ein Verweis auf ein Objekt X vorkam, konnte das Objekt sich den aktuellen Pointer auf dieses Objekt holen, da ja bereits alle Objekte schon im ersten Schritt erstellt wurden.

    Bei deiner Tür müsstest du speichern, ob sie zum aktuellen Zeitpunkt offen oder geschlossen war. Die Spiel-Logik ist zu dem Zeitpunkt ja schon abgelaufen. Ggf brauchst du zusätzlich noch irgendwelche Variablen, in der du Quest-States mit abspeicherst (hat mit NPC x gesprochen, hat richtige Dialog-Option ausgewählt, etc) Das wäre dann einfach eine Liste von Variablen, die zusätzlich zu den Objekten mit abgespeichert werden.



  • apfel-esser schrieb:

    Also ich habe ein Buch, da steht drin, man solle das ganze mit "Katalogen" und memorydumps machen.

    Memorydumps sind ganz schlecht, das dümmste was man machen kann.

    Wenn du da ein Speicherleck oder Bug in der Software hast, dann überträgt sich der Fehler von jedem Speicherstand fort in den nächsten und der Fehler akkumuliert sich, ich schlimmsten Fall kommst du dann einen Punkt, wo man das Spiel zwar laden kann, es aber ständig an der gleichen Stelle abstürzt.

    So ein Versagen sieht man z.B. sehr gut an der Total War Reihe.
    Also Rome Total War und Medieval 2.

    Bei denen ist das so und dort wird der Speicherstand mit der Zeit korrupiert und man kann irgendwann nicht mehr weiterspielen.
    Mir ist das sowohl bei Rom, als auch bei Medieval 2 passiert und das ist sehr ärgerlich, wenn man in jedes einzelne Spiel vorher > 80 h Spielzeit reingesteckt hat.

    Deswegen kann ich nur davon warnen, einen Memorydump zu machen.
    Mach das nicht.
    Stattdessen sichere die Werte einzeln, was du sichern mußt.

    Bei einem RPG kann man z.B. Protokoll führen in welchem Levelabschnitt der Spieler war. Z.b. in Sektoren einteilen.
    Und nur dort wo er war, dafür macht man dann eine Speicherung.
    Damit läßt sich Speicherzeit erheblich reduzieren.

    Wenn du ganz schlau bist, dann speicherst du temporär sobald er einen Sektor verläßt und in den nächsten eintretet.
    Wenn der SPieler dann abspeichert, dann mußt du die temporären Sektorspeicherstände nur noch in den normalen Spielstand kopieren. Also eine einfache Kopie auf der Platte.
    Sollte der Spieler sterben oder neu laden, dann muss der temporäre Speicherstand natürlich gelöscht werden, das ist klar.

    Die Finger würd eich aber auf alle Fälle von diesen Speicherdumps machen.
    Sie erschweren es leider auch erheblich Bugs in einem Spiel wiederzufinden, weil man ständig unterschiedliche Savegames hat.



  • Man kann aber auch nur Memorydumps machen von Objekten, in denen eben diese relevanten Daten liegen. Also wenn Position und Ausrichtung wichtig ist, der Rest aber nicht, dann speichere ich Position und Ausrichtung in einem extra Speicherblock (in C/C++ z.b. in dem ich das in eine extra struct packe). Beim speichern mache ich dann Dumps von allen Instanzen dieser Speicherbloecke. Letztendlich sind das ja alles Methoden um das gleiche zu erreichen, ob ich nun binaer oder als Text oder sonstwas speichere ist doch egal.

    Es muss das gemacht wurden, was am Anfang gesagt wurde, jedes Objekt muss seinen Zustand speichern (ausser er ist nicht relevant fuer das Spiel).


  • Mod

    auch die logik states sind daten und nicht code, entsprechend kannst du auch die in eine datei schreiben bzw aus einer datei lesen.

    bool KilledEndBoss
    

    in einem open world spiel musst du nicht jeden fliegenschiss speichern, denn das interesiert niemanden. du wirst bei spielen wie GTA3/4.. auch sehen, dass nach jedem laden viele fuer das spiel nicht kritische daten komplett anders sind, fahrzeuge haben andere farben etc.
    entsprechend kannst du den logik state sauber in einem datenblock verwalten, z.b.

    struct GameState
    {
    ...
    uint8_t KilledEndBoss;
    ...
    };
    

    und diesen einfach z.b. per

    fwrite(&myGameState,1,sizeof(GameState),...);
    

    rausschreiben.

    vergiss nicht eine veresionsnummer einzufuehren, oft will man alte game states reinladen, da solltest du zumindestens wissen, ob du sie konvertieren kannst oder sie invalide sind. bau auch ein simples hash ein, z.B. CRC32 am ende ein, passiert schonmal dass was korrupt ist und du willst nicht ein savegame debuggen was an sich schon kaputt war.



  • Nathan schrieb:

    Die großen Computerspiele werden so etwas wie einen Ausgabeoperator für jedes Gameobjekt definieren, was die relevanten Daten speichert.

    Hast du da vielleicht ein Beispiel?



  • suche beispielcode schrieb:

    Nathan schrieb:

    Die großen Computerspiele werden so etwas wie einen Ausgabeoperator für jedes Gameobjekt definieren, was die relevanten Daten speichert.

    Hast du da vielleicht ein Beispiel?

    Hat rapso doch grad gemacht?



  • rapso schrieb:

    in einem open world spiel musst du nicht jeden fliegenschiss speichern, denn das interesiert niemanden. du wirst bei spielen wie GTA3/4.. auch sehen, dass nach jedem laden viele fuer das spiel nicht kritische daten komplett anders sind, fahrzeuge haben andere farben etc.

    Farben in der Garage haben die gleichen Farben und alle anderen Fahrzeuge werden sowieso nicht gespeichert.
    Da kann es schon sein, dass die Fahrzeuge fehlen wenn man sich nur um 180° umdreht.

    Ich finde das allerdings trotzdem als Mangel und Spielkritisch, denn das ist schon doof wenn man sich nur umdrehen muss und alles ist weg oder anders.
    Mir wäre es lieber, wenn die Daten vorhanden bleiben würden, aber das geht natürlich nicht, wenn man ein Spiel auch für die sch*** Konsolenwelt mit ihrem knappen Speicher entwickeln möchte.
    Konsolen halten also mal wieder den Fortschritt auf, wie immer.

    bau auch ein simples hash ein, z.B. CRC32 am ende ein, passiert schonmal dass was korrupt ist und du willst nicht ein savegame debuggen was an sich schon kaputt war.

    Ehrlich?
    Hashes du wirklich jedes Objekt?



  • rapso schrieb:

    entsprechend kannst du den logik state sauber in einem datenblock verwalten, z.b.

    struct GameState 
    { 
     ... 
    uint8_t KilledEndBoss; 
     ... 
    };
    

    und diesen einfach z.b. per

    fwrite(&myGameState,1,sizeof(GameState),...);
    

    rausschreiben.

    Eben sowas sollte man nicht tun.
    Man bekommt hier schon Probleme, wenn man zu der Struct eine Variable hinzufügt (das Spiel versucht plötzlich mehr Daten aus dem File zu lesen, als dort hinein gespeichert wurden. Dabei gehören die folgenden Daten bereits zu einem anderen Objekt.)
    Oder man stellt im Nachhinein fest, man braucht nun ein int64 statt eines int32 für XP oder Geld...

    Ich verwende für sowas lieber eine std::map<std::string, std::string>.
    Man kriegt alle primitiven Typen in einem String unter, ist flexibler, wenn man etwas hinzufügen / ändern muss (man will ja auch nicht während der Entwicklung alle Nase lang einen Savegame-Konvertor schreiben, weil sonst alle Savegames flöten gehen).
    Der Nachteil dabei ist ein etwas höherer Speicher-Bedarf durch die Strings, den finde ich allerdings heutzutage auch vernachlässigbar. Sollte der Speicherbedarf dennoch eine Rolle spielen, kann man statt des string-Keys auch einen enum verwenden.

    enum GameVariable 
         XP, 
         Gold, 
         KilledBoss 
    };
    

    std::map<GameVariable, int32_t> game_variables;

    [edit]rapso, entschuldigung, ich habe statt zitieren aus versehen edit geklickt, ich habe deinen post wieder in den ursprungszustand gebracht.



  • Das führt aber nur zu a) langen Speicherzeiten und b) großen Savegames. Dein Argument lässt sich auch einfach dadurch entkräften, dass du am Anfang des Savegames einfach eine Versionsnummer einbaust. Wenn ein veralteter Speicherstand gelesen wird, kann der dann einfach in das neue Format umgewandelt werden. In deinem Szenario "was ist, wenn sich das Spiel ändert" muss man so etwas eh berücksichtigen. Ist ja in deinem Fall nicht anders: "was ist, wenn sich die Keys ändern"?

    Man kann sowas auch grundsätzlich abfangen, indem man zum Beispiel immer im größtmöglichen Datentyp speichert.



  • Naja man kann die struct theoretisch für jede Versionsnummer anders casten. Kommt halt immer drauf an, wenn man nur ein kleines Spiel hat kann das durchaus sinnvoll sein. Für ein Spiel das sich noch sehr stark ändern wird wäre diese Technik absolut nicht zu empfehlen.

    Ich würde auch jedem Objekt eine De-/Serialisierungsmethode mitgeben und dann für jedes Objekt für alle wichtigen Member jeweils deren Id (über eine enum) und dann die Daten abspeichern. Eventuell davor noch die Länge des Blocks damit man wirklich gute Abwärts- und Aufwärtskompatibilität hat.

    Spiel auch für die sch*** Konsolenwelt mit ihrem knappen Speicher entwickeln möchte

    Kommt immer drauf an welche Konsole. Außerdem bezweifel ich mal dass das an den Technischen Limits liegt dass die Daten nicht gespeichert werden. Soo schnell verbraucht sich Speicher eigentlich auch nicht wenn mann nicht gerade Bilder/Models/Sound/... speichert. 4 Kilobyte sind immerhin ca. 1024 floats/integer, da lässt sich einiges speichern...



  • Ich wüsste gerne, wie so ein Format für ein gespeichertes Spiel aussehen könnte.

    Habt ihr mal einen Link zu einer Spezifikation/Beschreibung von so einem Format?

    Das Spiel ist egal.



  • In der Regel Werte, von irgendwelchen Seperatoren getrennt.
    Da gibts eigentlich nichts großes zu zeigen.


Anmelden zum Antworten