Databases

[ LiB ]

Now that you have a basic comprehension of entities, I can move on to the databases of the BetterMUD.

Designs

For the SimpleMUD, I threw a design at you and said, "This is what the SimpleMUD is using!" Well, there are many ways to implement databases, especially for a persistent-world game.

The SimpleMUD and the BetterMUD don't actually have databases but rather simple containers that hold entities and aren't nearly as complex as some of the real databases out there.

There are tons of technologies to choose from. SQL is a popular database format. (I've played around with the MySQL implementation of SQL, and I liked what I saw. It's free, too! You can download it at http://www.mysql.org.) Lots of computer languages have built-in APIs that talk to these databases.

There are database programs you can buy for lots of money, but you probably won't need the kind of performance that they provide. MUDs usually aren't nearly as ambitious (in terms of player and world size) as the latest MMORPGs, so most of the time having a dedicated database program is a waste of effort.

The key benefit of a dedicated database is the fact that they (usually) abstract the data and the logic of a game onto two separate machines. Look at Figure 12.4 for a moment.

Figure 12.4. Typical database setup.

graphic/12fig04.gif


Usually, you have the database on one machine, and the game on the other. Whenever the game needs to access information, it asks the database to look it up and send it over the network connection. Although networks are much more unreliable than having everything in one machine, databases are typically installed on the same network as the game machine, so you probably have a fast Ethernet connection between the two. That eliminates any concerns about speed, and connection losses are also going to be rare (if not nonexistent).

The really great thing, however, is that the database machine can safely offload (store on to disk) the data, without slowing down the game. This is a flaw that the SimpleMUD and the BetterMUD both have; eventually, the world can get large enough so that just saving the database periodically ends up lagging the game.

I would have loved to have shown you how to implement a full "real" database for the MUD, but I only have so much room. So I chose to go light on databases and focus on the really cool parts of the scripting system of the BetterMUD.

Database Types

There are five major database types in the BetterMUD, four of which I present in this chapter. The fifth variation is the PythonDatabase , which is a class that manages Python scripts. I'll go over that in Chapter 16, "The Networking System," along with its offspring: the CommandDatabase and the LogicDatabase .

All of the database classes I discuss in this section can be found on the CD in the files BetterMUD/BetterMUD/databases/database.h and database.cpp, within the same directory.

Figure 12.5 shows the relationship hierarchy between all the databases I discuss in this chapter.

Figure 12.5. Relationships between all the entity databases in the BetterMUD.

graphic/12fig05.gif


The base Database class implements a number of helper functions that all entity databases use, and the map- and vector-based databases are similar in design to the databases of the SimpleMUD.

Note that the Template/Instance database doesn't inherit from either the map or vector databases; instead, it contains a copy of each. Databases

Basic Database

The basic Database class is a generic class that uses a single container to store entities. The main reason for the existence of this class is the fact that even though maps and vectors store data in two completely different ways, there are operations that you'll need to perform on both types. Here is a listing of those functions:

 container::iterator begin();     container::iterator end();     virtual entityid findname( const std::string& p_name ) = 0;     virtual entity& get( entityid p_id ) = 0;     virtual entity& create( entityid p_id ) = 0;     size_t size() { return m_container.size() - 1; }     void LoadEntity( std::istream& p_stream );     void SaveEntity( std::ostream& p_stream, entity& p_entity );     void LoadDirectory( const std::string& p_dir );     void LoadFile( const std::string& p_file );     void Purge(); 

The begin and end functions simply return iterators into the database, and findname does a full name match for entities within the database. The other functions need to be described in more detail, though.

Getting and Creating

Data retrieval is the most common function you perform with a database. The get function retrieves an existing entity from the database and returns a reference to that entity. However, you have to be really careful when performing this function; if you get an entity that doesn't exist, the database throws an exception at you.

On the other hand, if you want to create a new entity in the database, you should call the create function, which creates an entity at the ID you specify and returns a reference to the brand-new entity. If you call this function by the name of an entity that already exists, the function still works. It's designed so that if you call it, you are guaranteed to get an entity, unless you run out of memory or some other problem occurs. In addition, all entities returned from this function already have their m_id data filled out.

You should note that these two functions are purely virtual, meaning that the Database class doesn't implement them; it only says that the functions are available. This is because maps and vectors are fundamentally different data structures and require different methods to perform these functions.

Loading and Saving Entities

Loading and saving entities is more automated in the BetterMUD than in the SimpleMUD. All entities are required to have these two functions:

 void Save( std::ostream& p_stream ); void Load( std::istream& p_stream ); 

They save and load an entity to or from a stream, which is a great thing to have, because you can use file streams with these functions and save them to disk.

The Database class counts on the fact that these functions are available, and because of this, it has automated its loading and saving functions. Here's an example of the loading function:

 void LoadEntity( std::istream& p_stream ) {     entityid id;     std::string temp;     p_stream >> temp >> id;    // load the ID     entity& e = create( id );  // load/create entity     e.Load( p_stream );        // load it from the stream     p_stream >> std::ws;       // chew up extra whitespace } 

This function performs the following simple task, which is repeated quite often. For that reason, the function is built into the base database class:

  1. Eat up the [ID] tag in the stream.

  2. Load in the ID of the entity.

  3. Create/load the entity from the database.

  4. Load the data from the stream and put it into the entity.

Because of this process, the Load function of each entity should not load IDs from the stream It should always assume that the ID has already been loaded previously by the database. This also means that every entity in a file must start off with its ID tag, like this:

 [ID]                        542 

And the rest of the data follows after that.

The saving process is simpler; it just writes out the tag, writes out the ID, and then writes out the entity using its Save function.

Loading a Directory or File

From working with the SimpleMUD, I learned that working with one file per entity type was an incredible pain in the butt. So, for the BetterMUD, I've decided that the ability to automatically load an entire directory of files is a powerful tool. Therefore, the BetterMUD expands on the same ideas used by the player database of the SimpleMUD.

In every directory that databases load files from, there is a manifest file, simply named "manifest". It's a simple text file, and on every line is the name of a file within that directory that you want the database to load. Figure 12.6 shows an example of a manifest file pointing to other files that a database is supposed to load.

Figure 12.6. Files that aren't listed in the manifest aren't loaded.

graphic/12fig06.gif


NOTE

Even though C++ doesn't have the ability to iterate over the files in a directory, the need for such an operation is so common that it's built into almost every operating system. Because every operating system has its own operating method and none of the implementations are similar, people have created their own libraries to wrap around operating systems. One library is the C++ Boost library, which you can download and use free at http://www.boost.org . It's a really cool library that has a component named boost::filesystem that implements iterators to iterate over files in a directory. Unfortunately, due to some issues with VC6, and the fact that boost is still a work-in-progress, I couldn't get it to work prop- erly (though it worked like a charm in VC7 and GCC for Linux). So, I don't use boost here. You should feel free to look into it on your own, however. I've included the most recent version on the CD for you to play around with if you want. It's in the directory / goodies /Libraries/boost.

So, like in the SimpleMUD's player database, the BetterMUD's general Database class can load a directory of files using a manifest file. Here is the code to do it:

 void LoadDirectory( const std::string& p_dir ) {     std::ifstream dir(         std::string( p_dir + "manifest" ).c_str(),         std::ios::binary );     // open the manifest file     dir >> std::ws;             // chew up whitespace     std::string filename;     while( dir.good() ) {         dir >> filename >> std::ws;         LoadFile( p_dir + filename );     } } 

The function loads up the manifest file and then reads in name after name. For each name, it calls the LoadFile helper function. LoadFile simply loops through every entity in a file, loading each one:

 void LoadFile( const std::string& p_file ) {     std::string filename = p_file + ".data";     std::ifstream f( filename.c_str(), std::ios::binary );     f >> std::ws;     while( f.good() )         LoadEntity( f ); } 

Purging

There may be times in the game when you want to purge the entire contents of a database (rare, but it happens). Because of this, all databases have a Purge function, which completely empties the database.

Map and Vector Databases

The map and vector databases are similar to the map and vector databases from the SimpleMUD, so I'm not even going to show you their implementations. The vector-based database class is called VectorDatabase , and it doesn't add anything to the functions of normal dataabases.

The map-based database, MapDatabase , has some extra functions: FindOpenID , which finds an open ID and returns it, and erase , which finds an entity and erases it from the database. Vectors can't delete entities, because they shouldn't contain open spaces, and deleting entities at any index leads to open holes in the vector. I haven't come across the need to delete portals, rooms, or regions while the game is running, so I didn't bother implementing that function.

Template/Instance Database

Now, in the game, the dynamic objects (rooms, portals, and regions are all static objects, which cannot be created or deleted at runtime, except by loading a new database) are the characters and the items. All these items are generated from a template and are essentially stored in a database similar to the enemy database of the SimpleMUD; whenever a new character or item is created, the entity is copied over from a template.

To do this, I created a special database, the TemplateInstanceDatabase , which actually contains two databases: one vector-based, and one map-based. Figure 12.7 shows this concept. Whenever a new instance is created, a template is copied into it to store the initial values. Since the template database doesn't inherit from the other databases, it has to wrap over their functions and add new functions as well.

Figure 12.7. The template-instance database is a dual database that holds templates and instances.

graphic/12fig07.gif


Functions

Here's a listing:

 instances::iterator begin(); instances::iterator end(); templates::iterator begintemplates()     { return m_templates.begin(); } templates::iterator endtemplates()       { return m_templates.end(); } entity& get( entityid p_id ); size_t size(); size_t sizetemplates(); entityid findname( const std::string& p_name ); void erase( entityid p_id ) bool isvalid( entityid p_id ) void LoadEntityTemplate( std::istream& p_stream ); void SaveEntityTemplate( std::ostream& p_stream, entity& p_entity ); void LoadEntity( std::istream& p_stream ); void SaveEntity( std::ostream& p_stream, entity& p_entity ); void Cleanup() void Purge() void LoadFile( const std::string& p_file ) templateentity& gettemplate( entityid p_id ); entityid generate( entityid p_template ); 

The begin , end , get , size , findname , erase , and all the Load/Save functions simply wrap around the template instance database that it contains. The only functions involved with templates at all are gettemplate , generate , begintemplates , and endtemplates . The generate function takes an ID of a template as its parameter, creates a new entity instance based on that template, and returns the ID of the new instance.

For example, if you had a human template character at ID 1, you could create a new human character like this:

 entityid human = CharacterDB.generate( 1 ); 

And then human would hold the ID of the new character.

Here's the code for that function:

 entityid generate( entityid p_template ) {     entityid id = m_instances.FindOpenID();     entity& e = m_instances.create(id);     e.LoadTemplate( m_templates.get( p_template ) );     return id; } 

It finds an open ID within the instances database, creates a new entity at that ID, and then calls that entity's LoadTemplate function to copy the template over.

The instance class entity and the template class entitytemplate don't have to be different, but I would make them different. I've found that templates usually need less data than instances (which you see when you look at the Character and CharacterTemplate classes), so it's a good idea to keep the data to a minimum.

Garbage Collection

Deleting instances of entities in the game is dangerous. Since you can never be completely sure who is hanging on to a reference of an entity when you delete it, a template/instance database keeps a set of all of the IDs that the game wants cleaned up. For example, here's what happens in the erase function:

 void erase( entityid p_id ) {     m_cleanup.insert( p_id ); } 

When you want to remove an instance, instead of being blown away immediately, the instance's ID is inserted into the m_cleanup set, and the game eventually cleans up that entity at a later time, by calling the Cleanup function:

 void Cleanup() {     cleanup::iterator itr = m_cleanup.begin();     while( itr != m_cleanup.end() ) {         cleanup::iterator current = itr++;         entity& e = m_instances.get( *current );         m_instances.erase( *current );         m_cleanup.erase( current );     } } 

This essentially loops through the entire cleanup set, and erases the instances from the instance database.

[ LiB ]


MUD Game Programming
MUD Game Programming (Premier Press Game Development)
ISBN: 1592000908
EAN: 2147483647
Year: 2003
Pages: 147
Authors: Ron Penton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net