I’m currently working on an iPad RPG.  The game is level based,  where you have a world level from which you can enter several village levels. Those in turn may lead onto other dungeons or cellars.  So it was necessary to decide on a strategy for game saves. And as an iPad app, I knew I wanted to support adding new content to levels after the game was released, but in a way that would play friendly with the state of the levels players had already saved locally.

I figured binary serialization would be a good way to go as its fairly flexible and efficient.  If you haven’t used serializers before, there is a fair bit of information on it floating about. I won’t go deeply into serializing as it is outside the scope of this article. As a quick overview,  serializing a level involves iterating over all the live objects and having them write their variables to a stream.  To reload the level,  you can load the stream, and de-serialize the objects. Often the same code can work in both directions since it merely lists which variables need serializing.  Here is a quick example of serializing a door entity from my game.  Note the & symbol (which I stole from Boost syntax)  means the variable is read or written depending on the mode of the stream, so the same code works as a read as well as a write.

void Door::Serialize( DMSerializer &stream )
 
  {
 
    parent::Serialize(stream);
 
  
 
    stream & m_isLocked & m_isOpen;
 
  }
 
  

So this is all fine and dandy.   The big problem comes after you have published your game and you now want to add new content to a level in the next version update.  In my original plan, I would load a level file on the first visit to a level, but on subsequent visits, I would just load the serialized version.  However, that meant that once a player had seen a level,  it would never look at the level file again. If I added new NPCs or new towns, the player would never see them unless they started a clean profile.

This is a problem I’d never come across in my years of doing console titles.  Generally in updates we had to add new levels or extra profile information,  but never modify a level that already existed in a savegame.  I tried designing around the problem by saying that levels would always reset on entering them,  but that meant that destructible objects would reset each time which was too big an exploit.

After a few days of considering options, I settled on the following solution.  But before you get bored reading a lot of text and code,  here is a video showing the final result of updating a saved level.

Every Entity Gets a Unique Integer ID

Every entity gets a unique integer ID.   This is effectively a HANDLE for your entity.   Other entities can safely reference your entity by keeping a copy of this handle. This is useful for serialization since an entity may need to save a reference another entity. You can’t serialize a pointer, but you can serialize an integer. I was previously using weak pointers to entities, but after implementing unique ID handles, the weak pointers are no longer required and have been stripped out of the codebase.

All UIDs are registered in a table for a quick resolve. I use a simple array of entity pointers,  since I restrict the number of UIDs to 8192 so a 32k table can hold all known UIDs.  If an array is not viable, you could use a map or hashtable, but be aware the lookup time will be much worse.

UID -1 is considered an INVALID HANDLE.  This is important so you can tell if a UID no longer references any entity.
Split your UIDs into 3 ranges.  There 3 distinctive types of entity for this system and they each need to be isolated so you can tell what type of entity it is by looking at the UID.

  1. Entities created in code that don’t appear in a level file,  such as the player.  These entities will be statically assigned a UID. For instance, my main character always gets UID 0.  These shall be called Global UIDs
  2. Entites that exist in a level file. These will have their UID listed in the level file. The UID can never change and can never be resused in a future update. If an object is deleted from the level,  that UID should never be recycled.  These shall be called Static UIDs.
  3. Dynamically created entities.  These do not exist in the level file but are spawned during the cause of gameplay. For instance, a spawner might spawn a giant rat. The spawner itself would be specified in the level file, so it is a type 2 entity.  The rat is spawned dyamically so it is a type 3 entity and will be assigned a dynamic UID from the range that has been reserved.  Code for allocating, freeing and resolving entities is posted below (edit – I have updated this code following responses to this article. Note that ths code is for demonstration purposes. You will want to add range checking assertions.).
u16 DMSceneManager::AllocDynamicUID( DMEntity *pNewEntity )
 
  {
 
    u16 newUID = m_freeUIDs[ 0 ];
 
    m_freeUIDS[ 0 ] = m_freeUIDs[ --m_freeUIDCount ];
 
    m_entityArray[ newUID ] = pNewEntity;
 
    return newUID;
 
  }
 
  
 
  DMSceneManager::FreeDynamicUID( u16 uid )
 
  {
 
    m_freeUIDs[ ++m_freeUIDCount ] = uid;
 
    m_entityArray[ uid ] = 0;
 
  }
 
  
 
  DMEntity *DMSceneManager::ResolveEntity( u16 uid )
 
  {
 
    return m_entityArray[ uid ];
 
  }
 
  

Saving a level

As we exit a level,  or just exit the app,  we will serialize the current level to disk.  I support multiple profile slots so each level is serialized to the file  ”<ProfileID>_<levelName>.sav”.   All entities of type 2 and 3 get serialized.  Entities of Type 0 get serialized to the players profile file since they are effectively global and not bound to any level and should only be restored on the initial profile load.

void DMSceneManager::Serialize(DMSerializer &stream)
 
  {
 
    DMEntity *pEntity = m_pEntityList;
 
    while (pEntity)
 
    {
 
      u16 uid = pEntity->UID();
 
      if (IsUIDStatic(uid) || IsUIDDynamic(uid))
 
      {
 
        stream << UID();
 
        stream.StartBlock();
 
        stream << GetTemplateName();
 
        pEntity->Serialize(stream);
 
        stream.EndBlock();
 
      }
 
      pEntity = pEntity->m_pNext;
 
    }
 
    stream << INVALID_ENTITY_HANDLE;
 
  }
 
  

Loading a level

As we enter a level,  we will throw out all entities of type 2 and 3 from above from the previous level.  They will have been serialized and will be restored if the player returns to the old level.  If we followed this approach correctly,  then before loading a level,  no entities of type 2 ad 3 should exist.   As we load the level file, we will be creating all the type 2 objects and giving them their UID numbers which will still match what they were last time the player was in this level.

Finally we deserialize the stream for this level. The UID is grabbed first and we check if an object exists with that UID.

  • If the object exists,  then we serialize over the top.  This means serialize functions must cope with the object already being initialised.
  • If the object doesn’t exist,  but is a type 2 UID,  just ignore it.  This means it was a level file object but that object has been removed from the level.
  • If the object doesn’t exist, but is a type 3 UID,  then create it and deserialize it.
void DMSceneManager::Deserialize(DMSerializer &stream)
 
  {
 
    u16 uniqueID;
 
    stream >> uniqueID;
 
  
 
    while (uniqueID != INVALID_ENTITY_HANDLE)
 
    {
 
      stream.StartBlock(DMString());
 
  
 
      DMString templateName;
 
      stream >> templateName;
 
  
 
      DMEntity *pEntity = ResolveEntity(uniqueID);
 
      if (pEntity)
 
      {
 
        // this entity was created in the level file - just just serialize over the top of its current state
 
        pEntity->Serialize(stream);
 
      }
 
      else if (IsUIDDynamic(uniqueID))
 
      {
 
        // dynamic entity - doesn't primed by the TileMap so we need to create it...
 
        pEntity = (DMEntity *)DMObjectFactory::Get()->CreateObject(templateName);
 
        pEntity->SetUID(uniqueID);
 
        pEntity->Serialize(stream);
 
      }
 
  
 
      stream.EndBlock();   // skips to the end of the block if entity didn't get deserialized
 
  
 
      // get next unique ID
 
      stream >> uniqueID;
 
    }
 
  }
 
  

What does this system allow you to update?

Doing this allows you to add and remove objects to an already serialized level.  This is essential for adding new content such as quest givers or opening new areas with doors or towns.

You can also update anything in the entities that isn’t serialized. That is, any data you assign via the level file, but will not dynamically change  during gameplay.  This can be scripts or blocks of data defining behaviour, such as wander paths or spawn areas. As an example, I have npc’s that give a short speech when spoken to. The speech is embedded in the level file.  Since this text will not change during gameplay, it is not serialized and any modifications to the level file will not be override by saved games.

Another point I don’t delve into is that a serialized level has a version number that can be checked when de-serializing.  This means if you modify the variables in an entity,  you can still provide a fall back serialization routine that converts the old stream into some sensible values under the new system.  This is also relevant to being able to provide updates to a title,  but is a fairly standard technique so I haven’t gone into it in any detail here.

Final Questions

How do you address the issue of saving your levels?  Are you having to deal with the same issue, but have come up with a difference solution?  Please post about it in the comments below. I’d love to read about how others have tackled this problem.