Spielstände speichern?



  • ...



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



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


Anmelden zum Antworten