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. Its always best... by boeserjavamann · · Score: 2, Interesting

    ... NOT to implement a saveMe() Method in every object i think, cause every object should have only a single responsiblity (SRP: http://c2.com/cgi-bin/wiki?SingleResponsibilityPri nciple) it would be best to create a SaveData-Object and SaveDataSaver-Object (stupid name, but u know what i mean :)) the saveData object as a value object and a saveDataSaver for the responsibility to save it.

  2. They're called singletons now by mrami · · Score: 5, Interesting

    :) You make a singleton called "Prefs" and each module stores its values in a map under its own key (and in GCC, the key could be as easy as __PRETTY_FUNCTION__)

    1. Re:They're called singletons now by etedronai · · Score: 2, Interesting

      A singleton is really just a way to get around the way that constructors work in OO - namely that they always generate an object before any of the user code is even invoked. This does not allow for intelligent instantiation of objects and object reuse. The way that you are talking about using it is as a way to simulate global state - which is really no better than just using a global variable in the first place.

  3. Uhhh.. by QuantumG · · Score: 2, Interesting

    Preferably your language has persistence built in. Obviously if it doesn't, *cough* C++ *cough*, you can't do that. In such situations I believe the Memo GoF pattern would be appropriate.

    --
    How we know is more important than what we know.
  4. use state machines and context objects by jamsho · · Score: 3, Interesting

    Globals are simply a first order abstraction when it comes to storing a program's state.

    They get unwieldly fairly fast - as soon as you start hitting any complexity.

    Try state machines (see GOF) and lots of singleton classes with 'Context' in the name.

    The state machines and context objects can save their own state as the change - and read the state back in as necessary. They can save their state to an in-memory object or straight to a database whether it be a file or otherwise. A 'load' would just work the other way...

    Just how I'd approach it.... (it and about anything else non-trivial ....)

  5. Serialize the objects in question... by stoborrobots · · Score: 3, Interesting
    Just implement java.io.Serializable

    From the guide:
    Object Serialization supports the encoding of objects, and the objects reachable from them, into a stream of bytes; and it supports the complementary reconstruction of the object graph from the stream. Serialization is used for lightweight persistence ...


    And serialization has been available from java 1.1 at least...
    1. Re:Serialize the objects in question... by yasth · · Score: 2, Interesting
      You generally do have something like GameMap or MobileObjectsHashTable though. The cool thing about serializable is that it ripples through all serializable things. so you could litteraly do
      ObjectOutputStream oos = new ObjectOutputStream(file);
      oos.writeObject("GameMa p");
      Of course you have to be careful that you don't implent serializable where you don't want it. And you have to have some relinking code to relink evertyhing with the GUI, but it isn't that hard.
      --
      I'd do something interesting, but my server can't handle a slashdotting.
  6. Re:eh? mygame.savemethod()?? by zbyte64 · · Score: 2, Interesting

    This is a bit different... but at archspace we use a sort of db cache system. It involves many of the same problems, just at a larger scale. We cache the db into mem, generate values based off the db etc. Without getting into too much detail, every object inherits a Store class. When a db related variable is changed it checks if its already appended for update, if not, it puts itself in a update stack (sometimes it is a better idea to divide the update stack by object type because this is obviously a threaded game). Every 5 mins the updates are executed. The parent reminds me a bit of this since its all divided up. (its a tad more complex though)

  7. "Serialize" by Ninja+Programmer · · Score: 4, Interesting

    You simply have to model the the essential game state variables, then create a method for "serializing" them into something that can be thrown out to disk. There is no need at all for these variables to be global, just make sure you pass a "game context" down the call stack to any function which can modify the game state.

    The reason why its important to have this abstraction, is that its required in order to make in-game demos, and to have any hope of writing a networked version of your game. It can also let you do strange things like split screen the game and let two people play independent games if you like (a speed contest, for example.)

    1. Re:"Serialize" by AndroidCat · · Score: 2, Interesting
      But if you're serializing two games, never cross the streams! (Total protonic reversal!)

      But seriously, when everything serializes, stuff like game file versions or encrypting the stream on the way through get a lot easier. What happens with the block'o'memory save method when you have to insert one damned int in the middle? Toss all those old saved games?

      --
      One line blog. I hear that they're called Twitters now.
  8. Do what Infocom did: dump everything to a file by jonadab · · Score: 2, Interesting

    Infocom's approach to the save-game feature was simple and effective: dump
    the entire game -- variables, code, objects, constant data, everything -- from
    memory to a big fat binary file. This is not the most efficient save-game
    mechanism in terms of savegame filesize, and if your game application is quite
    large (as most are today) the save and restore process could take several
    seconds (so, you'll want a progress bar), but it has a couple of advantages:

    1. It's easy to get right, easy to debug, easy to test.

    2. It doesn't matter what kind of objects or data structures your code
    uses; you can use objects, lexical closures, continuations, whatever,
    it won't matter: it all gets thrown in the binary file, so it all
    comes back out when you restore.

    The virtual machine that Infocom developed, called the z-machine, is still
    in use to this day in the hobbyist interactive-fiction community, and they're
    still using this method for saving and restoring games, even though the code
    for the games is written in a different language now (a language called
    Inform which was developed by a hobbyist for this purpose). All the game
    code has to do to effect a save or restore is to issue an opcode, and the
    VM does the rest.

    --
    Cut that out, or I will ship you to Norilsk in a box.
  9. How can this fail, let me count the ways... by wowbagger · · Score: 5, Interesting
    Summary of your technique:
    Declare various global variables.
    Save game state into them.
    On Save Game, write block of memory out.

    How can this fail, let me count the ways:
    1. Layout of global section can change from link to link due to project changes, thus making saves version dependant.
    2. Non-save game global data can exist between save items, bloating save file.
    3. You can get the size of the save block wrong, and end up not saving the data you need. e.g. you use &foo and &bar as your start and end pointers, but due to a change there are variables after &bar that need to be saved.
    4. If the items you are saving become full-blown objects with compiler generated information (virtual function tables) you are overwriting that data with possibly incorrect data.
    5. No error checking in file - so a recovered file may screw the game up.

    And that's just what I can come up with before my morning coffee.

    Look, I disagree with your instructer about "global variables are NEVER needed" - what, then are stdout/stderr/stdin/cout/cin/cerr, if not global variables?

    However, global variables are like salt - a little may be needed, but too much will raise your blood pressure.

    Again, this is before my morning coffee, but here's a couple of techniques that are better:
    • Define a function to allocate a block of "saved space" - sort of a "save_malloc()" function. Allocate the objects you wish to save via that function. In that function, you grab a block of N bytes at initialization, you initialize an end pointer, and you "malloc" by moving the end pointer (no freeing allowed). You now know exactly what you need to save. You can also write a version # at the beginning of the file, and you can compute a checksum of the data. For OO types, you can fancy this up by writing a "Save_game" base class, and implementing new() for that base class.
    • The more OO approach: Implement a "Save_game" base class. The base class implements a linked-list, with a static member as the head pointer. The class has Register/Deregister functions, and a pure virtual Size() method. Derived classes implement Size() { return sizeof(*this);} and call Register in their ctor. To save game, walk the list. This also allows you to save the size of the object (even better if you use RTTI, you can save the actual type of the object), and to checksum each object.

  10. Persistence discussion on LtU by Rich+Dougherty · · Score: 3, Interesting

    You may find a recent discussion on Lambda the Ultimate relevant to your question.

  11. Singletons or Class variables.. all the same. by mystran · · Score: 4, Interesting

    While I'd argue that global variables are usually a bad idea, I don't see a reason agaist file-scope variables. The rationale goes like this: In OOP you use Singletons when you only need a single variable of a given type. You do it, because while the Singleton gives you global access to the single variable, it still acts as an encapsulation method. This eliminates one of the problems of global variables: unpredicatable modifications, by allowing the Singleton to define what is allowed and what is not. Class variables can do the same, since only the method of the class can touch the variables (provided they are private). But file-scope (static) variables have the same property; the only thing different is that access is restricted not by a class, but by the compilation unit. You still get the same encapsulation. Another question is whether Singletons should be avoided too. Most of the time I like writing code in such a way, that you can run two (separated) instances of the program within the same process by simply create two objects of the main application class. Unfortunately Singletons are often needed to cope with libraries and APIs that assume there's only client within a process. IMHO Singletons (and class variables and methods) are to OOP what IO-monads are to functional programming: they are "hacks" that try to work around the limitations of a given programming model, and trying to minimize the damage caused.

    --
    Software should be free as in speech, but if we also get some free beer, all the better.
  12. Re:how about.. by Anonymous Coward · · Score: 1, Interesting

    Does anyone remember "The Killing Game Show" on the Amiga etc? After dying, the game would allow you to replay your last life and interrupt and continue at any point. It was a geat idea - pity the game was too hard...

  13. Re:eh? mygame.savemethod()?? by etedronai · · Score: 2, Interesting

    Ideally you would solve this problem using an aspect. This is exactly what aspects and AOP were designed for, a way to abstract functionality that does not make sense any where in traditional OO and that cross cuts a large part of the system. Data loggers are also an example of something that should be solved with an aspect.

  14. Not "under any circumstances" eh? by snorklewacker · · Score: 3, Interesting

    Here's the deal: when your professor tells you that you're never ever ever ever ... ever ever ever ... ever ever (etc) supposed to do something, then as the poster who quoted Miss Manners mentioned, you've got a pretty good idea what the answer to the exam question is. Any professor who's been in the real world (usually people who retire into teaching) will tell you that there's exceptions to any rule.

    Games, with their relentless demands for resource efficiency, will have you breaking lots of rules. Game saves are one of the first walls a junior game designer hits. They've written this fabulously interesting game, unpolished of course, but it's got real potential. But the saved games are two megs each, and take 15 seconds to write out. There goes your console version. You now have to start cutting all kinds of corners to get those save times and sizes down, and that may mean a sacrifice of architectural purity.

    To wrap it up, you probably do not want to blindly serialize all your stateful objects into persistent storage and leave it at that. You can and probably should do that while developing the game (be sure to version your objects while you're at it), but when you need to get efficient, you need to start relentlessly trimming the "serialized" form, and seeing what you can build up, recreate, or even just leave out (e.g. a save game in a RTS probably doesn't need all the scorch marks saved). Then instead of serializing to a stream to persistent storage, you want them to simply notify a "state container" with a reference to themselves (the container can egregiously violate encapsulation -- use inner class adaptors or private inheritance if you're paranoid) and that container can index into a memory segment. Then you just write that segment out to disk. Version the damn thing, so if you patch the game, you're not completely hosed. Keep in mind that you're getting RAM 4K at a time, and writing it to disk in bigger chunks, so don't be too stingy.

    Now go do the rest of your homework yourself.

    --
    I am no longer wasting my time with slashdot
  15. Re:how about.. by stew1 · · Score: 3, Interesting

    Yes. Many games these days are written as if they were a pure simulation. You have your initial state and you change state by applying inputs (user commands & time progression). It should be completely deterministic, so you can have two copies of the game that start with the same state, apply the same inputs, and arrive at the same final state.

    This may seem heavyweight, but, hey, processors are fast these days. Many games use scripting languages -- the "I need to write this in C using only global variables" approach of yesteryear is no longer necessary. Additionally, you get some great benefits. Chief among them is replayability and save&restore, but you can also get a relatively simple network protocol (mostly just send inputs, which are small), maintainable code, easy dead reckoning, and the ability to apply other optimizations on top of the model (e.g. lag thrusters). Also, an important insight is that you don't necessarily have to display your simulation state _exactly_. This way you can have reliable code to track the simulation and reap the benefits of such a model, and you can also use a bunch of hacks on top of the simulation to make the user experience look/feel good. That's a good way to isolate the hacks that go into any game.

    Jon