Spielstände speichern?



  • 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.



  • ne.

    ich rede nicht von config-files, sondern von richtigen savegame.

    z.b.:
    http://www.uesp.net/wiki/Tes5Mod:Save_File_Format

    ich sehe da nirgends irgendeinen separator 🙄



  • Interessant.
    Du redest nicht von Configfiles schickst mir aber einen Link, wo ein Savegameformat beschrieben ist, was wie ein simples Configfile aufgebaut ist. Aha.



  • Nathan schrieb:

    Interessant.
    Du redest nicht von Configfiles schickst mir aber einen Link, wo ein Savegameformat beschrieben ist, was wie ein simples Configfile aufgebaut ist. Aha.

    nö. das format ist binär. da ist nix mit separatoren.



  • Ach so, dann habe ich das missverstanden.
    Weil da steht name und da dachte ich halt, das sähe wie so ein INI File aus.
    Mein Fehler.
    Ja klar, binär sind da keine Seperatoren, da gehts nur nach Größe.



  • Für ein komplexeres Spiel habe ich zuletzt ein Format verwendet, das in mehrere Blöcke aufgeteilt war. Ein Block bestand aus ID, Version, Länge und (Länge) Bytes an Daten. Durch die ID wurde ermittelt, zu welchem Objekt diese Daten gehören und die Daten dann von diesem Objekt interpretiert. Das waren dann zum Teil einfach sequentiell aneinandergereihte Ints und Floats.
    Ein Block beinhaltet auch so eine string/string-map.

    otze schrieb:

    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.

    Ist natürlich auch alles abhängig vom Spiel. In meinem Fall waren das gerademal ein paar Dutzend keys, die aber von Level zu Level unterschiedlich sein können. Durch die Map brauche ich z.B. auch keine Daten mitzuspeichern, die für den aktuellen Level gar nicht relevant sind.
    Daß sich keys ändern ist auch kein Problem. Neue Keys können problemlos hinzugefügt werden, alte werden ggf konvertiert oder einfach ignoriert.

    Sollte es dabei irgendwann um einige tausend Keys gehen (was vielleicht bei einem größeren RPG oder Adventure realistisch ist) wäre eine enum/int map immernoch eine Ausweichmöglichkeit. Wobei ich gerade da die Möglichkeit, über die Strings auch einfach aus einem Skript heraus zugreifen zu können, doch sehr schätzen würde.

    Bei den Structs ist man doch recht eingeschränkt. Neue Variablen hinzufügen geht nur am Ende. Variablen herauslöschen geht auch nicht. Falls doch, funktioniert das ganze einfache Laden über eine Adresse und Länge schon nicht mehr, und dann könnte man auch gleich eine Vernünftige Lösung zum speichern implementieren.


  • Mod

    Hangaman schrieb:

    ...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. ...

    man speichert die farbe nicht weil der speicher knapp ist? lol, wegen dir hab ich mich an meiner morgentlichen coke verschluckt 😃
    GTA1/2 haben auch nicht die ganze welt gespeichert, obwohl es moeglich gewesen waere 😉

    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?

    falls du pro objekt ein savegame machst 😛
    natuerlich nur ein CRC pro _ganzem_ save game 🙂


  • Mod

    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.

    er fragte nicht wie es in deiner welt zu tun waere, sondern mehrmals

    Wie machen das andere Spiele?

    und das war lediglich die antwort drauf, die mehrheit der spiele die ich kenne macht es so.

    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.)

    man muss sich ja nicht extra dumm anstellen. version konvertierungen gehoeren immer dazu.

    Oder man stellt im Nachhinein fest, man braucht nun ein int64 statt eines int32 für XP oder Geld...

    ja, oder wenn du statt watt jetzt volt und ampere seperat speichern moechtest -> versions konvertierung.

    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).

    waere mir viel zu ineffizient, am ende fragt sich ein spieler nicht wie es implementiert ist, sondern "warum muss ich wegen einem scheiss save game 5min warten?" (und sag mir jetzt nicht dass wegen einer std::map<string,string> die welt nicht langsam wird, es geht um das allgemeine denken bei sowas. manche spiele speichern save games waehrend du spielst ohne dass du es ueberhaupt merkst weil es bruchteile einer sekunde sind, andere lassen dich fuer das eine MB mehrere sekunden in einem menue haengen, davon spreche ich).

    und deine loesung deckt nur das wirklich einfachste und primitivste ab, viel haeufiger ist es dass man neue klassen/typen hat, logik aendert die daten anders interpretiert. sich dann soviel overhead aufzuhalsen weil du einen typen aenderst?
    ich nutze viel lieber eine kaskade von versionen ala

    Foo bar_version_002=bar_version_001;
    

    und der ctor regelt jegliche konvertierung, nicht nur simple primitive typen.

    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;
    

    und jetzt kommt "score" hinzu, summe aus XP+Gold, schon musst du logik zum laden und konvertieren verbauen, die std::map hilft hier 0.

    wenn dir deine variante reicht, mach es so, aber sag nicht allgemein gueltig "so sollte man es, so nicht". es gibt spiele die sich auch einfach nur den user input merken und das spiel nochmal abspielen (also nur die logik), weil sich soviel in einem spiel aendert, dass keine unserer varianten im entferntesten funktionieren wuerde. wenn du z.b. ein RPG hast in dem du durch eine riesen welt und 100 quests hast mit einem quest zu anfang dass dir eine scheinbar unbrauchbare goldene nasenklammer gibt, damit du in quest 101 den stinkigen dungeon betreten kannst, das aber erst vor 3 tagen eingebaut wurde, du jedoch ein save game von vor 5tagen debuggen willst, dann musst du ein mechanismuss haben dass dir garantiert dass das savegame nach quest 100 die nasenklammer in deinem inventar hat. da kannst du auch nicht pauschal sagen "jetzt ist die nasenklammer da", denn ein spieler koennte quest 7 uebersprungen haben, das ist oft bei RPGs.


  • Mod

    threadstarter schrieb:

    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.

    schau dir doch den doom3 quellcode an, oder quake, gibt doch genug spiele die open source gemacht wurden. dann siehst du nicht nur das format (was echt nur die halbe arbeit ist), sondern wie man es laedt und mit versionskonflikten umgeht.


  • Mod

    Baldur schrieb:

    Daß sich keys ändern ist auch kein Problem. Neue Keys können problemlos hinzugefügt werden, alte werden ggf konvertiert oder einfach ignoriert.

    statt also daten direkt zu konvertieren, machst du erstmal map magic und am ende konvertierst du doch. das klingt irgendwie redundant.

    Sollte es dabei irgendwann um einige tausend Keys gehen (was vielleicht bei einem größeren RPG oder Adventure realistisch ist) wäre eine enum/int map immernoch eine Ausweichmöglichkeit. Wobei ich gerade da die Möglichkeit, über die Strings auch einfach aus einem Skript heraus zugreifen zu können, doch sehr schätzen würde.

    wuerdest du einen extra pfad machen mit extra lade scripts oder meinst du im allgemeinen spielfluss alle daten immer auf map<string,string> spiegeln?
    beides klingt irgendwie scarry.

    Bei den Structs ist man doch recht eingeschränkt. Neue Variablen hinzufügen geht nur am Ende. Variablen herauslöschen geht auch nicht. Falls doch, funktioniert das ganze einfache Laden über eine Adresse und Länge schon nicht mehr, und dann könnte man auch gleich eine Vernünftige Lösung zum speichern implementieren.

    du konvertierst structs genau so wie es in deinem fall waere. du hast quasi dasselbe nur in komplex und langsam geschaffen. wenn eine variabel einen neuen typen hat z.b. float statt int, dann macht man das oft um den wertebereich zu aendern z.b. auf meter zu gehen statt cm, dann musst du also trotz map den typen umwandeln und seperat behandeln. wenn eine neue variabel eingefuegt wird oder reihenfolgen veraendert werden, ist das auch nicht pauschal mit "initialisieren wir immer auf 0", es kann sein dass der wert errechnet wird, es kann sein dass version5 den wert auf 3.5 setzt und veresion6 anhand dieser neuen variabel eine ganz andere bestimmt und version7 den wert auf 2.5 setzt. schon hast du eine konvertierungsabhaengig von der reihenfolge der initialisierung der variablen. da ist eine kaskade von save game konvertierungen sehr viel flexibler, sicherer, besonders bei grossen teams und langer entwicklungszeit wo du solche fehler sonst erlebst, weil niemand ueberblick wie der ganze code sich gegenseitig ueber die zeit von selbst 2wochen veraendert hat, aber 2wochen lange save games will die QA dennoch laden koennen um einen fix zu verifizieren.


Anmelden zum Antworten