Archived

This topic is now archived and is closed to further replies.

RajanSky

Supporting multiple versions of a file

Recommended Posts

RajanSky    100
Supporting multiple versions of a file can be a pain. At my job, what we do is have multiple Read() functions, one for each version. This works fairly well, but it can be a hassle to maintain. So, I came up with a "new" way to do saving and loading. I'm curious to get some feedback, and see what kind of save/load methods you guys use. Here's an overview: ===Goals==== 1. Easy maintenance 2. Asynchronous loading- i.e. "load stuff while the game is idling" 3. Reduce the risk of bugs ===How it works=== 1. Create an enum (say it's called IOParam) with an entry for each data member in the class 2. Write a read/write function which takes an IOParam and reads or writes that member 3. For each version of the structure, represent it as an array of IOParam's 4. To read the structure, just call Read() and pass in the correct array ---1. Creating the enum--- So say we have this simple class:
class Dude
{
public:
   char   m_Name[20];
   int    m_Age;
}; 
Then, here's what the enum would look like:
enum Dude_IOParams
{
    Dude_IO_name1,   // the 1 signifies the version
    Dude_IO_age1
};
 
---2. Writing the read/write functions--- Here's what the read function would look like:
bool Dude::Read( ulong param, File &file )
{
    switch( param )
    {
        case Dude_IO_name1:
            return file.Read( &m_Name, 20, 1 );

        case Dude_IO_age1:
            return file.Read( &m_Age, sizeof(int), 1 );
    };
}
 
---3. Representing the structure as an array of IO Params--- Here's what the version would look like:
ulong DudeVersion1[] =
{
    DUDE_IO_name1,
    DUDE_IO_age1
}
 
---4. Read it---
Read( DudeVersion1 );
 
... So, that's how it works. Now, here's the advantages of this method. ===1. Why this helps easily write and maintain save/load functions=== Suppose we decide to make a version2, but we change the age to be a float instead of an int. No problem- here's what we do: 1. We create our new structure:
ulong DudeVersion2[] =
{
    DUDE_IO_name1,
    DUDE_IO_age2    // this has been changed to float  
}
 
2. We change our handling code in Dude::Read() for reading Dude_IO_age1:
case Dude_IO_age1:
{
    int temp;
    bool success = file.Read( &temp, sizeof(int), 1 );
    m_Age = (float) temp;
    return success;
}
 
So, now any time it encounters an outdated age parameter, it handles it correctly. This is nice because say we had not just DudeVersion1 which was outdated, but say there were 15 outdated versions. We only have to make the change in 1 place, not 15 places, so maintenance is relatively easy. The major drawbacks of this system though, are that it's a bit slower due to all the switch's, and a bit of a pain to set up initially... I'm not sure if much can be done about the speed, but as for the syntax, I got around that using macros. There are lots of neat tricks that this system lends itself to.. For example, say you have this structure:
ulong MonsterVersion5[] =
{
    MONSTER_IO_hp1,
    MONSTER_IO_mp1,
    MONSTER_IO_stamina1,
    MONSTER_IO_armor2,
    MONSTER_IO_attack1,
    MONSTER_IO_speed2
};
 
Now, say we want to create a version6, but all we want to do is add *one* new parameter- say it's "MONSTER_IO_weapon1"... Instead of recopying all of those parameters, all we have to do is:
ulong MonsterVersion6[] =
{
    STRUCTURE( MonsterVersion5 ),
    MONSTER_IO_weapon1
};
 
Basically this "STRUCTURE" macro just means to re-use all of the parameters from the specified structure. This makes it pretty easy to create new versions without having lots of redundant code all over the place. ===2. Why this helps with asynchronous loading=== The ulong array gives us everything we need to load the structure. We can easily store this array and then iterate through it during the game's idle time, taking a few chunks at a time. ===3. Why this helps reduce the risk of bugs=== One of the major problems with save/load code is that your read and write code must be similar. If you read 5 parameters but you only write 4, it's not going to work. Using this method, your read and write code is always going to be the same, because they are both using the same ulong array as a reference. ===Problems==== 1. Asynchronous loading assumes each param is small. If you have a large one, it will cause a delay. 2. Although macros and inheritance can help make this easier, it's still a bit of work to set up. 3. It uses switches, although it might not matter since file I/O is already quite expensive 4. Asynchronous saving assumes that data will not have changed by the time it saves it Anyways, right now this is a bit half-baked for practical use, but I think something like this could be really useful. I mean, file I/O seems like it'd be a pretty trivial thing, but once you start supporting multiple versions and things like that, it can be a nightmare imho. Well, sorry for making this post so godawful long and hope to hear from some other gamedev'rs on here! Raj [edited by - Rajansky on August 12, 2003 6:24:33 PM]

Share this post


Link to post
Share on other sites