Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
114 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

Item Management Systems


The Item Database

The database will of course be dependent on the data to be stored for each item. For the purposes of this article, the information stored will be the name of the object, a short description of it, its value and its weight. They will be stored into structures, one structure per item:

struct sItemData {

  std::string name, description;
  unsigned long value, weight;

};

The item database at its core should be an easy means of accessing information about an object based on its type. This leads to the following properties:

  • Returning the item data for any given item
  • Creating an item corresponding to a given item name

To represent the data manager, I've chosen to use a monostate object. It is an object that can only exist in one instance at a time, but unlike a singleton, that instance is not directly accessible. However, it could become useful at a point or another to re-load the object, in which case the new version will simply replace the previous one. As a monostate, the object should have:

  • Functions to initialize it (when the program starts)
  • Functions to delete it (when the program exits)

There is no actual need to turn this object into a class, a namespace will do:

namespace cItemDatabase {

  const sItemData & getData( const cItem & );
  cItem create( const std::string & );
  void initialize( const std::string & );
  void unload( void );

};

There is an additional concern: exceptions. There are three possible errors in the above functions. First, all functions that require the object to be intialized beforehand cannot fail silently because they need to return something (and it would be a bad idea to fail silently because this can cause bugs). Next, the create function may not find an item corresponding to its argument, it must then have a way to alert the user. And finally, the user might try to get the data for an object that does not exist in the database. This will go through this enum type:

enum eDatabaseError {
  IDBERR_NOT_INITIALIZED,
  IDBERR_INVALID_NAME,
  IDBERR_INVALID_ITEM
};

More exceptions may come later on, depending on new behaviors we will implement into the database (such as an IDBERR_OUT_OF_COFFEE if we ever implement coffeemaking solutions).

First, the variables required for the implementation :

namespace cItemDatabase {

  std::deque< sItemData > item_database_entries;
  bool item_database_initialized = false;

};

The above boolean serves as an indicator to tell if the object has already been initialized. I have chosen to store the data as a deque because it allows a O(1) random access without sacrificing memory usage (we need to store all the objects anyway), and because unlike a vector it does not need a contiguous memory space. The initialization and deletion functions are the following:

void cItemDatabase::initialize( const std::string & s ) {

  item_database_entries.clear( );
  //FILE LOADING SEQUENCE
  item_database_initialized = true;

}

void cItemDatabase::unload( void ) {

  item_database_entries.clear( );
  item_database_initialized = false;

}

I did not write here any actual loading process, because I think it's up to you to choose the one that fits best your style (all of them should be doing the same thing anyway: filling up the deque with the items). The source code provided at the end of the article, however, is fully functional, albeit with the help of a possibly buggy loading system (which is not error-checked at all).

const sItemData & cItemDatabase::getData( const cItem & i ) {

  if( item_database_initialized ) {

    unsigned long type = i.getID( );
    if( type >= item_database_entries.size( ) )
      { throw IDBERR_INVALID_ITEM; }
    else { return( item_database_entries[type] ); }

  }
  else { throw IDBERR_NOT_INITIALIZED; }

}

The getData function extracts the type identifier of the object and checks if it is withing the bounds of the database, which causes it to either return the correct object, or throw an invalid item exception. Since item descriptions can get fairly large, it is best to return a constant reference of an existing description instead of returning a copy. It is both natural and necessary for the reference to be constant: modifying it would cause bugs in other areas of the code, and the description of an item type is not something that should not be modified from inside the game anyway.

cItem cItemDatabase::create( const std::string & s ) {

  if( instance == 0 ) { throw IDBERR_NOT_INITIALIZED; }
  unsigned long i;
  for( i = 0; i != instance->data.size( ); ++i ) {

    if( i->name == s ) { return( i ); }

  }
  throw IDBERR_INVALID_NAME;

}

The creation function, as simple as possible : it checks that there is an instance somewhere, then runs through the entire map looking for an object with the same name as it. Naturally, it's a time-consuming O(n) process, so I suggest using it sparingly (it is not meant for mainstream use anyway, only for debugging/cheating/scripting purposes, as name-to-identifier correspondence can usually be evaluated at release time).

cItem cItemDatabase::create( const std::string & s ) {

  if( !item_database_initialized ) { throw IDBERR_NOT_INITIALIZED; }
  long i;
  for( i = item_database_entries.size( )-1; i >= 0; --i ) {

    if( item_database_entries[i].name == s ) { return( cItem(i) ); }

  }
  throw IDBERR_INVALID_NAME;

}

The implementation of the above function is quite straightforward: check if the data has been initialized, then run through the entire vector until a match is found, and if there is none, throw an exception.

Conclusion

This six-file coding spree has left us with a basic, yet efficient item system. Applications using this system will be moving around cItem instances without knowing what's inside, and whenever some data is needed, a quick call to the database will bring up everything. Item piles are even more useful than items themselves because they can represent the player's inventory, items on the ground, or any kind of item stack anywhere.

The way the system works is demonstrated in the zip file that comes with the article (see below). The archive contains all the source code in a VC++6 project/workspace, along with two additional files that provide a testing scaffold. The associated executable is a dos console application that loads an object list from a file, and allows you to manage an inventory by adding, removing and looking at things inside.

(Download the source code here)





Contents
  Introduction
  Item Piles
  The Item Database

  Source code
  Printable version
  Discuss this article