Adding New Features to the Baseline

[ LiB ]

I've added a bunch of new things to the game in this chapter: rooms, room databases, room database pointers, stores, and store databases. Table 9.1 lists all the new components and where they are stored within the /Demos/Chapter09/Demo09-01/SimpleMUD directory on the CD.

Table 9.1. New Component Files

Component

Location

Room class

Room.h and .cpp

RoomDatabase class

RoomDatabase.h and .cpp

room database pointer class

Pointers.h and .cpp

Store class

Store.h and .cpp

StoreDatabase class

StoreDatabase.h and .cpp


Rooms

The first order of business is the Room class. A Room , like almost every other object within the game, is derived from an Entity . This means, of course, that rooms have names and IDs, but you don't typically search through room names as you did in the previous chapter with players and items. Room names simply describe the room to the player, so he can navigate around the MUD easily, always having a general idea of where he is.

Room Data

Rooms have all the data described in Chapter 7, "Designing the SimpleMUD," and they store three different groups of data.

Template Data

First, there is the "template" datadata that is loaded from disk, but never changes within the game:

 RoomType m_type;                 // type of the room int m_data;                      // storeid if it is a store string m_description;            // description of the room entityid m_rooms[NUMDIRECTIONS]; // exits enemytemplate m_spawnwhich;      // which enemy to spawn int m_maxenemies;                // how many enemies max 

Three new features need to be described at this point:

  • RoomType enumeration describing room function

  • NUMDIRECTIONS constant. Number of exits per room

  • enemytemplate database pointer for enemy templates

RoomType is a simple enumeration, much like the ItemType and PlayerRank enumerations in the previous chapter. (They are found within the Attributes.h file.) The three values are PLAINROOM , TRAININGROOM , and STORE , and these values obviously represent the three types of rooms. This design means that rooms can serve only one purpose.

NUMDIRECTIONS can also be found in Attributes.h, and simply represents the number of directions a player can move in any room. (Remember: for the SimpleMUD this is four.)

NOTE

This is a classic method of represent ing special rooms, and it works well in the SimpleMUD. However, there may be a time when you need rooms to be more than one type (for example, a room that serves both as a training room and a store). For that kind of implementation, you should consider an alternative method of storing this information. One way would be to use bitmasks , in which the first bit would represent whether the room is a store, the second bit would repre sent whether it is a training room, and so on. The BetterMUD in this book takes a completely different approach, as you'll see in Chapter 11, "The Better MUD."

Finally, the enemytemplate type is a databasepointer<EnemyTemplate, EnemyTemplateDatabase> . Unfortunately, I don't go over those until the next chapter, so at the top of Room.h, I define it as a simple entityid :

 typedef entityid enemytemplate;     // REMOVE THIS LATER 

Volatile Data

Here's the next group of data:

 list<item> m_items;              // items in the room money m_money;                   // money on the floor 

Basically, this data is volatile. (It changes.) Whenever the game needs to save all the current rooms to disk, there's no need to write all the template data I showed you previously, such as the room name and description (since it never changes), so instead of writing out all a room's data, it needs to write only the volatile data. I'll go into more detail on this when I cover room databases.

Because I mentioned in Chapter 7 that the capacity of a room is limited to 32 items, you may think that it would be logical to use an array-like structure, such as a vector, to store the items. However, vectors are problematic for two reasons. First, you'd need to keep one of these arrays per room, which means that every room would have the capacity to store all 32 items all the time. Obviously, you'll never even come close to having that many items lying around in the game, so this would waste space. The second problem is that the continuous adding and removing of items are expensive vector operations. In contrast, lists are quick for insertions and removals, so they actually work better in this situation.

Temporary Volatile Data

Here's the final group of data:

 list<player> m_players; list<enemy> m_enemies; 

These two lists store players and enemies (in the form of databasepointer s, of course). You won't see the enemy class until Chapter 10, "Enemies, Combat, and the Game Loop," but for now you can assume that it's a databasepointer<Enemy, EnemyDatabase> .

However, the lists are never saved to disk (as items and money are). Keeping this information on disk is redundant, since players and enemies already know the room they are in.

Imagine this scenario for a moment: You're editing the databases, and you change the room that a player is in. The map databases track which players are in which rooms, though, which means that you'll have to find the room within the room database and move that player's ID to the new room. This is an incredibly inconvenient way of editing the databases, and it also wastes space by storing redundant data.

Since players and enemies know which room they are in (on disk), when the players and enemies are loaded, the game finds what rooms they exist in and inserts the entities into the appropriate rooms. Then, depending on their identities, the players and enemies insert themselves into the m_players or m_enemies lists.

Room Functions

Next I want to cover the functions that you can use on rooms. Many functions are fairly simple, but a few are complex; I'll show you the simple functions first.

Accessors

As with most classes in this book, a bunch of simple accessor functions simply allow you to access and modify room attributes. Here's a listing of the accessors:

 inline RoomType& Type()         { return m_type; } inline int& Data()              { return m_data; } inline string& Description()    { return m_description; } inline entityid& Adjacent( int p_dir ) { return m_rooms[p_dir]; } inline enemytemplate& SpawnWhich() { return m_spawnwhich; } inline int& MaxEnemies()        { return m_maxenemies; } inline list<item>& Items()      { return m_items; } inline money& Money()           { return m_money; } inline list<enemy>& Enemies()   { return m_enemies; } inline list<player>& Players()  { return m_players; } 

I don't really need to explain all those to you, do I? Make a special note of the Adjacent function, however. The function returns the ID of the room adjacent to it in the given direction as an entityid . Basically, you can call it like this:

 Room r; // *** do init stuff somewhere here *** entityid south = r.Adjacent( SOUTH ); 

Of course, as with all aspects of entity IDs, the value 0 is invalid, so the return of zero from the function means there is no exit in that direction. The enumeration values are defined in Attributes.h as part of the Direction enumeration, and there are four values representing the four directions: NORTH , EAST , SOUTH , and WEST .

Player Functions

The two player functions are somewhat simple. They allow you to add and remove players (based on ID) to and from a room:

 void Room::AddPlayer( player p_player ) {     m_players.push_back( p_player ); } void Room::RemovePlayer( player p_player ) {     m_players.erase( std::find( m_players.begin(),                                 m_players.end(),                                 (entityid)p_player ) ); } 

As you can see, it's simply wrapping STL code into easy-to-use functions. A new player is always added to the back of the list. Players are removed using a combination of std::list::erase and std::find . The find function is used to find a player within the list, compared to p_player . You may have noticed, however, that p_player is explicitly converted to an entityid before it is used within the find function. This is because database pointers don't know how to compare themselves with other database pointers, but they do know how to compare themselves to entityid s.

NOTE

I used lists to store the players in a room here, but you should feel free to use whatever you want. Lists are helpful because they are resizable, and keep things in order. In the BetterMUD, you'll see that I didn't have much use for keeping players in order, so I opted to use sets instead.

NOTE

Ambiguity Errors

The lack of an operator== to compare two database pointers with each other is intentional. In addition, there is no operator== function that compares entityid s either, but to the compiler, it looks as if there is one because all database pointers have an operator entityid() conversion operator function. If you have a database pointer named dbp1 and an entityid named id , the line dpb1 == id would actually end up automatically calling the conversion operator of the pointer object, thus converting it into an entityid . This would then call operator== on the IDs. Since you know that IDs are just typedefs for integers, you should see that they already have a built-in operator== .

If, however, you wrote some code such as dbp1 == dbp2 , you would get an ambigu ity error , meaning that the compiler couldn't figure out which function to call. Since database pointers can be converted to actual pointers (for example, Player* in the case of class player ), they can't figure out if you're trying to compare Player* s or entityid s (even though you know they should be equivalent).

It gets even worse if you add an operator== to compare database pointers into the mix, because then the program can't figure out if you want to compare Player* s, entityid s, or database pointers. That means that the new operator== you just added would never be called and would be pointless code. Because of this, it is required that you convert your database pointers into plain entityid s before you compare them with other database pointers.

Item Functions

Three functions deal with items, and two of the functions are virtually identical to the two player functions I just showed you: AddItem and RemoveItem . Both functions take item database pointers as parameters.

Since the code is virtually identical, I'll show you only the third item function, which doesn't have a player equivalent:

 item Room::FindItem( const string& p_item ) {     std::list<item>::iterator itr =         // find an item that matches             BasicLib::double_find_if(                 m_items.begin(), m_items.end(),                 matchentityfull( p_item ), matchentity( p_item ) );     if( itr == m_items.end() )         return 0;     return *itr; } 

Using a full or partial name as a parameter, this function finds the ID of an item within the room. Basically, it's a wrapper around the double_find_if function, which you used in the previous chapter. If an item isn't found, the ID of zero is returned, and if an item is found, its ID is returned. For example, if you had items "Sword", "Axe", and "Club" in a room, and you run this function with input "sw", it returns the ID of the "Sword" object.

NOTE

There is one tiny difference in the AddItem function: When the function detects that there are 32 or more items in the room, it automatically pops off the first item in the room. Since all new items in the room are added to the end of the list, the first item in the list is also the oldest item in the list. This means that whenever there are 32 items in a room, and you drop a 33rd, the oldest item in the room is destroyed , and you can't get it back. This prevents certain rooms from getting completely flooded with items that people don't seem to pick up.

File Functions

Three functions deal with loading and saving rooms to and from disk. They are

 void LoadTemplate( istream& p_stream ); void LoadData( istream& p_stream ); void SaveData( ostream& p_stream ); 

The first function loads all the template data from disk. There's no equivalent save function, simply because there doesn't need to be one; template data is loaded once from disk and shouldn't be modified.

On the other hand, the next two functions deal with loading and saving the volatile data namely, the items and money within a room.

I don't want to spend much space showing you the contents of these functions; they closely resemble the player and item loading and saving functions described in the previous chapter. I'll show you sample entries, however.

Here's what a sample room template looks like:

 [ID]            1 [NAME]          Town Square [DESCRIPTION]   You are in the town square. This is the central meeting place for the realm. [TYPE]          PLAINROOM [DATA]          0 [NORTH]         2 [EAST]          25 [SOUTH]         4 [WEST]          5 [ENEMY]         0 [MAXENEMIES]    0 

This shows you that the ID of the room is 1, the name is "Town Square", and a brief description of the room follows . It's a plain room, meaning that it's not a store or a training room; thus, the [DATA] tag isn't meaningful for this room. For a store, however, this tag would be the ID of the store.

There are exits in all four directions; going north leads you to room 2, east to 25, south to 4, and west to 5. There are no enemies within the room, which means that the [MAXENEMIES] entry is meaningless, too.

Now here's an example of volatile data entry for a room:

 [ROOMID] 1 [ITEMS] 10 5 6 3 1 11 0 [MONEY] 200 

The entry contains the room ID, a list of all item IDs in the room (ending with a zero, commonly known as a sentinel value ), and the amount of money in the room. From this entry, you can see that there are six items in the room (items 10, 5, 6, 3, 1, and 11, whatever they may be), and $200 on the floor.

I'm going to show you the LoadData function, because it contains the most interesting function code. Here it is:

 void Room::LoadData( istream& p_stream ) {     string temp;  p_stream >> temp;   // chew up "[ITEMS]" tag     m_items.clear();                  // clear and load items     entityid last;     while( extract( p_stream, last ) != 0 )            m_items.push_back( last );     p_stream >> temp;   p_stream >> m_money;  // load money } 

The function uses the BasicLib::extract function to extract item IDs from the stream and scan until it reaches zero, the sentinel value.

I need to make one important point about the function. Whenever this function is called, the room clears its item list. The reasoning is that you may want to reload the room while in the game, and if you start appending data to the end of the item list, you may end up with duplicated data. Imagine that a room contains items 10, 11, and 12, and then in the middle of the game, you reload that room. If you don't clear the item list, you'll end up with two of each item: 10, 11, 12, 10, 11, 12.

Also, as with the item and player loading functions that were described in Chapter 8, "Items and Players," the database manages the loading in and setting of the entity ID; therefore, this code doesn't load in the [ROOMID] tag.

The Room Database

Like players and items, rooms have a database as well. If you guessed that the database is called RoomDatabase , you earned a cookie! No, I'm not mailing you a cookie; you'll have to come here and get it.

Anyway, the room database is very simple, and it inherits from the EntityDatabaseVector class I showed you in the previous chapter. Here's the class definition:

 class RoomDatabase : public EntityDatabaseVector<Room> { public:     static void LoadTemplates();     static void LoadData();     static void SaveData(); };  // end class RoomDatabase 

The room database class has functions for loading the templates for each room, loading the volatile data for each room, and saving the volatile data for each room.

All the room data is stored within two files: /maps/default.map, which holds all the template data, and /maps/default.data, which holds all the item and money data.

The maps are separated into two files for a reason; periodically in the game, all the item and money data must be written to disk. Unfortunately, since I'm using ASCII text files, there's really no way to go into the existing file and modify what has been changed. As I told you in Chapter 8, the only way to write out ASCII data to disk is to completely destroy the file and rewrite all the data. Obviously, since there is no reason to write the template data back to disk, it makes sense to keep the template data in one file, and the volatile data in another, so that just the volatile data can be written out when it needs to be.

Figure 9.1 shows a sample of the file setup for rooms. You can see that the template data is stored in one file, regular volatile data in another, and temporary volatile data isn't stored at all.

Figure 9.1. Sample file setup for rooms.

graphic/09fig01.gif


The code for the three file functions is pretty simple and similar to what you've seen before with the player and item databases, but since you haven't seen a vector-based database yet, I'll show you some of the code in action.

Loading Templates

The process for loading the templates from disk is fairly straightforward. For each room, the room ID is loaded in first, and then the vector is resized if there isn't enough room available. Once you know there's enough room in the vector for the room you're loading the template for, the ID for that room is set, and the template is loaded:

 void RoomDatabase::LoadTemplates() {     std::ifstream file( "maps/default.map" );     entityid id;        std::string temp;     while( file.good() ) {         file >> temp >> id;                // load ID         if( m_vector.size() <= id )        // check if there's room             m_vector.resize( id + 1 );     // resize if there isn't         m_vector[id].ID() = id;            // set the ID         m_vector[id].LoadTemplate( file ); // load template         file >> std::ws;                   // eat whitespace after it     } } 

Basically, the only thing new here is the vector resizing. With an associative container such as a map , STL automatically creates and inserts items when you call its operator[] on a key that doesn't exist, but vectors aren't as easy to work with. Calling operator[] on a nonexistent index is an error, and depending on your STL implementation, it may throw an exception. Don't worry, though; this behavior is actually a good thing. With a map, if you accidentally try inserting something with a large key (in the billions, say), you'll insert at most one new item. If a vector auto-resized on operator[] , however, your program would try to resize the array to within the billions, which is obviously not a good thing.

Loading Data

The LoadData function is similar to LoadTemplates . Essentially , the only differences are that it calls Room::LoadData on each room, instead of Room::LoadTemplate , and it doesn't resize the vector either. LoadData doesn't resize the vector because it shouldn't need to. Whenever you load a room from disk, the template should have already been loaded; thus, the ID should already be valid.

NOTE

In a more robust MUD, it would be a good idea to add some error checking. I've neglected to do so due to time and space constraints, but you should always put lots of error-checking code into your MUD projects.

Saving Data

Saving the temporary data, just as with every other saving function you've seen in this book so far, is a simple task:

 void RoomDatabase::SaveData() {     std::ofstream file( "maps/default.data" );     iterator itr = begin();     while( itr != end() ) {         file << "[ROOMID] " << itr->ID() << "\n";  // write ID         m_vector[itr->ID()].SaveData( file );      // write data         file << "\n";         ++itr;     } } 

As I mentioned earlier, rooms don't know how to load or save their IDs to and from disk, so it is the responsibility of the database to do it instead. The function essentially loops through every room, writes out the ID, and then writes out the rest of the data.

Room Database Pointers

If you're beginning to notice a trend here, you're absolutely correct. Everything entity-based in the game has a similarly designed database and similar means of being accessed. Therefore, it shouldn't come as a surprise that the room database also has database pointers, just as players and items do. The definition of these pointers has been added to the /SimpleMUD/DatabasePointer.h and .cpp files:

 // .h file: DATABASEPOINTER( room, Room ) // .cpp file: DATABASEPOINTERIMPL( room, Room, RoomDatabase ) 

Keeping with the standard naming scheme I've been using all along, pointers to rooms are called room (lowercase), while the actual room objects are Room s (first letter capitalized).

Stores

The other major topic I cover in this chapter is stores. Stores are simple entities; their only purpose is to store a list of all the kinds of items that can be bought and sold there.

Because of this, the Store class is simple:

 class Store : public Entity { public:     typedef std::list<item>::iterator iterator;     iterator begin()    { return m_items.begin(); }     iterator end()      { return m_items.end(); }     size_t size()       { return m_items.size(); }     item find( const string& p_item );     bool has( entityid p_item );     friend istream& operator>>( istream& p_stream, Store& s ); protected:     list<item> m_items; }; 

In terms of data storage, all a store needs is a list of the items that are bought and sold there.

Helper Functions

To make stores more usable, stores utilize some of the standard STL container functions, which basically wrap around existing algorithms and functions.

For example, you can see that stores have the standard begin , end , and size functions, in addition to two search functions: find and has .

The find function searches the store using a double full-then-partial name match, such as you saw used in Chapter 8, and it returns the ID of the item it found, or zero if nothing is found. The other searching function simply returns true or false if an item ID exists within the database.

Those two functions simply wrap around the BasicLib::double_find_if and std::find algorithms respectively, so let me just skip to the file streaming function.

Stream Extraction Function

Since stores don't change throughout the game, you need only one function to deal with files: a stream extraction function. Obviously, this function extracts a store from a stream.

First, let me begin by showing you a sample store entry in a file:

 [ID]        1 [NAME]      Bobs Weapon Shop [Items]     40 41 42 43 44 58 59 60 61 62 63 64 65 66 67 68 0 

The ID and the name are inherited from Entity , so every store has them as well as a list of all items the store sells. This should remind you of the way that items are stored inside of rooms, and it should come as no surprise that these are loaded in the same manner:

 inline istream& operator>>( istream& p_stream, Store& s ) {     string temp;     p_stream >> temp >> std::ws;                 // chew whitespace     std::getline( p_stream, s.Name() );          // read name     s.m_items.clear();                           // clear existing items     entityid last;     p_stream >> temp;                           // chew up "[ITEMS]" tag     while( extract( p_stream, last ) != 0 )     // loop while item ID is valid         s.m_items.push_back( last );            // add item     return p_stream; } 

As usual, the database manages the loading of the [ID] tag, and this function simply loads the name and every item until the sentinel value 0 is reached (using the BasicLib::extract function for help).

The Store Database

The store database is probably the simplest database class in the entire game. Take a look at the definition:

 class StoreDatabase : public EntityDatabase<Store> { public:     static bool Load(); };  // end class StoreDatabase 

As you can see, the store database inherits from the map-based EntityDatabase class. It contains a single function to load the database from disk.

All stores are kept in a text file named /stores/stores.str. The Load function is almost identical to the other EntityDatabase loading functions you saw from Chapter 8, so I'm not going to show you the code here; it's largely redundant and boring.

Also note that there is no associated database pointer for the Store class; there simply isn't a need for one in the game.

[ 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