Slashdot Mirror


Object-Oriented 'Save Game' Techniques?

GreyArtist asks: "I took a course in C++ a year ago in which the instructor claimed that global (file-scope or inter-file-scope) variables were antiquated and not to be used under any circumstances. I immediately thought of a counter argument that involved the method I use for saving game data. The games (and many of the other programs) I write use not only global variables, but consecutive global variables declared in their own separate module. To save the game (or user settings) to file, I simply save a single large segment of data that contains all the necessary information. How do other coders do it? Would they create a 'MyObject.savemyself()' method for every object in their game? Do they save all the game code along with the data? Either way, it seems like a horrid case of code (or data) bloat. What do you die-hard object-oriented fanatics have to say about this, and what method they would you use for saving games?"

18 of 229 comments (clear)

  1. Re:how about.. by danielrose · · Score: 2, Informative

    If you are initiating a save by pressing save, you may as well save what your variables are at NOW.

    You don't need to log changes as they occur, like a journalled fs does, unless you wanted to eliminate the need to press save, and have everything save automagically. This may be impractical, depending on the number of variables and states you are saving in your log.

    --
    i hate pansy republicans
  2. eh? mygame.savemethod()?? by torpor · · Score: 5, Informative

    Would they create a 'MyObject.savemyself()' method for every object in their game?

    isn't the purpose of 'object oriented programming' that you don't have to think like this? you just call the one big 'Game Object' save method, and .. all other derived/related objects, do their big save?

    seems a bit wonky to me.

    "game saves" is not just a game problem, of course. there are many, many parallels in other types of application .. embedded data loggers, for example, getting a warning that the shack is about to flood for winter, need to save their state too ..

    for me, the 'global context save and restore' is a 'built-in' to the design. i'm rather fond of libs and services which provide persistence natively .. though i think that some would argue that mmap's to flash RAM are cheating ... ;)

    --
    ; -- the corruption of government starts with its secrets. a truly free people keep no secrets. --
  3. Re:Uhhh.. by Anonymous Coward · · Score: 2, Informative

    You mean the memento pattern, right?

  4. Object-based approach... by genneth · · Score: 3, Informative

    I'm not a CompSci student -- so I don't know the strict definitions of things, but I think this, below, counts as more of an object based approach as opposed to true OOP.

    The basic idea is that the thing you're trying to do, ie. have saved game state, ought be a first class thing. So have a global singleton that manages this, and have objects register themselves to that class, then most of the boilerplate can be collected in the global object.

    In C++, a better approach would be something like that taken by Boost.Serialization, which provide a template (STL style) framework, so that you can plug in different ways to marshal data as well as different output formats, etc.

  5. Re:how about.. by vinsci · · Score: 4, Informative

    You mean object prevalence like in Prevayler? See also here for a general presentation (intro here: Object Prevalence in C++).

    --

    Trusted Computing FAQ | Free Dawit Isaak!
  6. Beware of Memory Dumps. by Jason+Pollock · · Score: 4, Informative

    If you use a memory dump save (as it sounds to me) you will eventually notice several things:

    1) The files aren't easily loaded between versions of the software.
    2) The files aren't platform independent.
    3) The files are very fragile, and very dependent on compiler options.

    This is one of the complaints about Word document files - they can contain memory dumps. :)

    However, for simple ease of implementation, nothing beats getting a pointer and writing a block of memory to disk.

    Jason

  7. Serialization by SteveX · · Score: 5, Informative

    It's called serialization, and most OO frameworks support it in some way or another.

    Usually it's a way for an object to render itself to a stream, and reconstitute itself from a stream.

    That way you can save the objects to disk, or send them over the network, or whatever else you need to do with them.

    Every object serializes itself, and all of it's immediate children. Once every object does this, you can save the whole tree of objects with one call.

    1. Re:Serialization by Anonymous Coward · · Score: 1, Informative
      (Posted anonymously because these are the claims of me, not my company.)

      I have to respectfully disagree with your tone here, although you raise a valid point.

      On my previous project at work, we heavily rely on serialization for managing save states and shuttling data across the network -- not in a game application, but in a database application. We find the serialization pattern to be really powerful in its simplicity, and maybe other /. readers will, too.

      The serialization methods on your objects should not just do a dumb memcpy() of each of its members into a buffer. Yes, if you do this, you're locking yourself into a particular set of members.

      The solution to this problem is so simple that I can't believe you dismissed the entire serialization design pattern because of it. Serialize a one byte version number to the stream at the beginning of each object where a future change is conceivable. Then, if you need to add changes in the future, bump the version numbers of only the structures that have changed.

      Your deserializer code now looks very simple (pseudo C++):

      ver = deserialize<int>(stream);
      if (ver==2)
      {
      . // standard deserialization code
      }
      else if (ver==1)
      {
      . // Code that reads an old stream and builds a new object
      . // in a backwards-compatible way.
      }

      The code in the ver==1 case can do whatever logic is necessary to construct a new-version object with an old-version state. Most of the time, this just means setting some new member in your object to a reasonable default value which preserves the old behavior. Or you might just be changing the stream format for efficiency, in which case the ver==1 case can retain the original deserialize code in its entirety.

      This solution does not give you forward compatibility. A lack of forward compatibility is probably fine for file formats, but generally not okay for network protocols, since you want old and new versions to be able to communicate in both directions.

      Our solution to this is that our network communication objects have slightly more robust serialization/deserialization methods. They start off with a version byte as before (actually a two-byte word of flags, but it's the same idea), but this time preceeded by a four-byte size field. This lets us add fields that older versions will safely ignore.

      Even in version 1, every object calculates how much space it will require to serialize itself, and it writes this number to the stream. Then it serializes as before. The deserialization code uses this number for a simple trick. It reads the size and remembers it, then deserializes the object exactly as any other deserialize method would. After completion of the operation, it compares the number of bytes read to the number of bytes claimed in the stream.

      Now in version 1, these two numbers should always be equal, or else we have a bug. But nevertheless, if these numbers don't match, we read enough extra bytes off the stream to get them to match, ignoring whatever values they might contain.

      So when we decide to update one of our network messages, by adding a field to a network object in version 2, we ask ourselves, "does this network message still make sense to a client who doesn't know about this extra field?" If the answer is no, then we make another network message entirely. Sometimes new version request messages won't be understood by old versions, and we'd rather handle these errors in the network message dispatch code than the serialization code.

      If the answer is yes, then things are beautiful. We bump the version number (or add a flag) to indicate some special version 2 data is on the stream. Then we serialize all the version 1 fields in the same format as before, followed by any version 2 data. So our new deserializer now looks like (pseudo C++):

      size = deserialize<int>(

  8. boost.serialization by ville · · Score: 3, Informative

    Boost has boost.serialization which takes care of such things as pointers. Check it out.

    // ville

  9. That's not how you implement singleton by Chemisor · · Score: 2, Informative
    No, the proper way to implement the singleton pattern is:
    class Game {
    private:
    Game (void) {}
    public:
    static Game* Instance (void)
    {
    static Game theGame;
    return (&theGame);
    }
    void setup (int, char**);
    int run (void);
    };

    int main (int argc, char** argv)
    {
    Game* theGame = Game::Instance();
    theGame->setup (argc, argv);
    return (theGame->run());
    }
    This lets you avoid new/delete calls, which in your example can create a memory leak if an exception is thrown or some fatal error occurs that crashes your application. With the above implementation Game::~Game is always called allowing you to do necessary cleanup. Good error recovery is especially important in console games, where a crash can leave the terminal in graphics mode and render the machine effectively unusable.
  10. Re:Your instructor is insane by arkanes · · Score: 2, Informative
    I can translate your post as "I don't know OO very well, so using it takes me longer than doing things my way". This is all well and good but not a very helpful way of teaching. It doesn't take any longer to write an object than it does to write a whole slew of global variables, and you don't need to add any special amounts of scaffolding. Incidently, the OO technique often (but not always, although it generally depends on the skill of the programmer) makes it easier to leverage the fancy scaffolding into place after the fact.

    Personally, I have never seen a functional or procedural technique which could not easily be translated into OO. I've seen lots of OO features that can't be translated into functional or procedural, if only because the lack of language support makes using them that way awkward. So in my (admittedly limited) experience, OO is objectively better. I wouldn't make that as a general claim, of course, but I will make a general claim that encapsulating something like global state as an object is superior to using it as a bunch of global variables. Even making a single global struct would be better.

  11. Re:how about.. by HeghmoH · · Score: 2, Informative

    Interesting you should mention that. Bungie used to be a big fan of this technique, which they used in all of their Marathon and Myth games. The game basically just recorded the player's actions, and saved this record into the file when you hit save. Restore was a matter of loading the level and then replaying the save file until the end. How they managed not to break save files every time they released a new version, I will never know.

    One big advantage of this approach is that you can use the exact same code to create game films. Create a file the exact same way, but display the results when you play it back, and presto! Bungie did this for Marathon and Myth as well.

    The big disadvantage is that you have to be absolutely sure that the game will play back exactly the same way every time. As far as I know, the Aleph One open-source version of the Marathon engine has never, and probably will never, read normal Marathon saved games or films because of subtle differences in how the engines operate.

    --
    Mod down posts with a "Free Mac Mini/iPod" sig, they're spam!
  12. Id does it quick and dirty. by Anonymous Coward · · Score: 2, Informative
    The major problem with persisting an object graph, is maintaining the internal relationships without causing duplication on load. To that end, Quake2 (and possibly its successors) has a fairly radical approach to this problem that works really well.

    Basically, keep all your game state in a large array of entities:
    #define MAX_ENTS 600 // or some other large number
    typedef struct Entity{
    int ent_type;
    int health;
    int armor;
    int behavior_state;
    Entity* target;
    Entity* leader;
    // and so on...
    };
    Entity[MAX_ENTS] game_ents;
    (BTW, the scheme above has certain distinct advantages over freely new/delete managed objects, via object pooling)

    When it comes to persist, simply rewrite all the internal pointers to other entities as indicies and dump the entire list of entities to disk. Loading from a save file is merely the same operation in reverse: translate offsets into pointers.
    FILE* save_file = fopen("save.dat","wb+");

    // save object graph of Entities via offset translation
    for(int i=0; i<MAX_ENTS; i++){
    Entity temp = game_ents[i]; // copy
    temp.target = (Entity*)((int)temp.target - (int)game_ents);
    temp.leader= (Entity*)((int)temp.leader - (int)game_ents);
    // you get the idea...
    fwrite(&temp,sizeof(Entity),1,save_file);
    }
    fclo se(save_file);
    (or merely stuff the base array pointer into the save file and just retranslate on load)

    The tradeoff is a quick save/load feature for less flexibility in your game entity scheme (relatively inflexible for oop, but can be done). Also, you'd need an array for every base type you wish to mantain in a proper graph.

  13. Re:Singletons or Class variables.. all the same. by mrami · · Score: 2, Informative
    In C++, at least, using static variables poses a maintainability problem, because even though they may all be file scope for now, as soon as a cross-file dependency arises, you have the initialization-order problem (that is, C++ doesn't specify which file is to be initialized first). Singletons (whether classes or plain-old functions in C++) avoid this problem.

    For those of you who haven't seen this before, check out Effective C++ by Scott Myers (or Meyers; I don't have it in front of me)

  14. Re:Singletons or Class variables.. all the same. by maraist · · Score: 2, Informative

    Depending on your language of choice. Container-based singleton management is an excellent design pattern. In Java, spring and picocontainer manage setter / constructor injection of singletons; possibly even hiding the fact that some objects are session-scope, request-scope or even non-singletons.

    The code is managed by an XML file (or some external configuration); you get the effect of singletons, but the extensibility to swap out which implementation of the interface/base-class you use in which environment. And as you expressed concern, you don't have to worry about running multiple applications with isolated singletons. The component-manager is an instance variable (not a singleton), so you can have multiple isolated environments within the same application.

    Makes testing a LOT easier too. If you use abstract / interface classes everywhere, you can easily swap out mock-instances for testing purposes. (having a testing-environment component-manager configuration file).

    I'm speaking from the Java side, but the basic concept should be applicable to C++ if there aren't already implementations.

    --
    -Michael
  15. XML-like Tree serialize... by Leadhyena · · Score: 2, Informative
    Whenever I had to save a game state I would write a tree-like structure, and pass a file object around. Choose the format of your save file to be XML-like in the sense that you have some sort of marker that identifies the beginning of a game object and the end of a game object, and that your objects are nestable.

    Next, create a utility class that contains functions that can read a file up to a marker and return what was read, and another that can identify a marker and jump to the appropriate class. When you do the save, tell the game to save by storing simple objects in order and then passing the save call to the complex objects in order. Those objects will do the same thing, delimiting their objects by bracketing them. Then when restoring, read to each tag and jump to the appropriate class constructor. In effect, you're crawling the tree of objects that are being used for the game.

    The advantage of this method is that you store exactly what you need to store, adding game objects is a breeze and almost append themselves to the game tree, and if you expand your game you can tell the reader to ignore tabs it can't identify and work around them, meaning that your older save-files won't break the newer version.

    One other thing to keep in mind is that make sure that no constructor actions take place until everything is loaded, so that you don't have say player objects trying to render on a board that hasn't been constructed yet. Make sure you have both a constructor and an initializer funciton for all of your game objects.

  16. Re:Serialize the objects in question... by bokmann · · Score: 2, Informative

    First of all, the guy is talking C++, not Java.

    Second of all, serialization in Java is not as simple as just saying 'implements Serializable'... In some cases it can be this simple, but if you are doing anything with controlled object construction, such as singletons or the typesafe enum pattern, or if your objects hold onto things that don't make sense to be persisted (such as network connections, open files, etc), then you are going to have to do some special things.

    Take a look at the ReadResolve and WriteReplace methods, the Externalizable interface, and for Gods sake, Read the relevant parts of the book 'Effective Java' by Josh Bloch before doing anything with serialization.

  17. Sorry I Haven't Gotten It Yet by GreyArtist · · Score: 2, Informative

    First, I'd like to thank everyone for all the well-written responses I've received to my question, but...

    I think that in some instances the crux of my problem may have been overlooked. The title of this post may have been somewhat misleading (my fault), but I was really more concerned with the avoiding code bloat aspect of my question than the "how to save a game state" part, all while maintaining object-oriented methodology. Some people seem to consider this a silly question, as the object-oriented methodology supposedly allows any technique you want. I beg to differ. When I started out programming many years ago (writing BASIC on an Apple II), I instinctively wrote top-down, procedural, and somewhat structured code that had very few GOTO statements (maybe some when I had mismanaged line numbers). The methodologies I just mentioned do allow for any technique you choose to employ, and can rival the efficiency of any of the spaghetti code that came before. I have never found the same thing to be true for object-oriented methodologies.

    Some of my concerns about the techniques mentioned:

    The singleton technique seems to be a politically correct label for a global data structure. I don't think my ex-instructor would approve (but seriously, it does seem to be somewhat of an anti-oop construct).

    The object serialization technique is basically a sophisticated term for the MyObject.savemyself() that I mentioned at the top of the post. Of course the game data would need to be serialized, rather than letting objects write randomly to the save game file with a brief header describing who had done the writing. My overriding concern with this technique is that it would involve so many function calls for a game that included say, 40,000 objects. My secondary concern would be that depending on the class hierarchy, many different versions of a save game state function would have to be employed. While one post claimed that a universal base class would be able to write the game state out for every derivation, I cannot believe it. Derived classes will always have extra features that affect the game state which they will need to handle themselves (their parents and children can't do it for them).

    After spending 15 years trying to clearly understand the object-oriented philosophy (even taking a class at my age), it occurs to me that the object-oriented hype seems to be an attempt at scaling down the operating system philosophy to fit single applications. Objects are "mini-programs" that are supposedly decoupled from the surrounding super-program and protected from dangerous and misbehaving code in other modules (of the same super-program). Funny that after 20 years of procedural programming, I have seldom beheld any of the claimed symptoms of not using object-orientation.

    I think the industry went in totally the wrong direction. We should have been fixing our operating systems to more closely mimic the single application philosophy...

    But I would still be interested in hearing about a truly efficient, simple, elegant, and nearly code-less object-oriented solution to the save game state problem.

    email: aofi7@hotpop.com