Spielstände speichern?
-
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).
-
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_Formatich 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.
-
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 waerebau 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
-
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 alaFoo 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.
-
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.
-
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.
-
Wow, mal nicht so schnell
Ich hab ja oben geschrieben, daß mein Savegame aus mehreren Datenblöcken besteht und jedes Objekt seinen eigenen Block hat, bzw. selbst dafür verantwortlich ist sich selbst zu speichern und zu laden. Wäre also auch Team-kompatibel.
Bei der Map ging es wirklich nur um irgendwelche Gamestates, Questflags, etc. und bei dem Skript bezog ich mich auch nur auf Skripte, die Questmechaniken, etc steuern. Da ists dann auch einfach ein "getVar" und "setVar" für die Skripte zu implementieren als jede Variable einzeln rüber zu hieven.
An der Map selbst musste ich noch garnichts konvertieren, da hinzufügen von Keys ja eh ohne Probleme geht. Konvertierungen gibts dafür in den Objekt-Daten, die dort von den Objekten selbst behandelt werden.
Das Ganze läuft übrigens auf Android und das Autosave ist fast nicht spürbar im Spiel
Wie läuft denn die Konvertierung wenn du structs verwendest?
struct SaveGame_V1 { uint32_t hp; uint32_t xp; }; struct Savegame_V2 { uint32_t hp; uint64_t xp; } switch(version) { case 1: { SaveGame_V1 sg1; SaveGame_V2 sg2; read(&sg1); sg2.hp = sg1.hp; sg2.xp = sg1.xp; ... break; } }
Wird doch auch schnell unübersichtlich und fehleranfällig.
Da doch eher gleich auf das Memory-Dump verzichten und die Variablen einzeln lesen.
SaveGame sg; sg.hp = readUInt32(); switch(version) { case 1: sg.xp = readUInt32(); break; case 2: sg.xp = readUInt64(); break; }
(ich weise darauf hin daß der Code als Pseudocode zu lesen ist)