MUD Game Programming (Premier Press Game Development)

[ LiB ]

There are six entity classes in BetterMUD, representing accounts, characters , items, rooms, regions , and portals. Most of them share a good deal in common, which makes it easier on us, since all we need to do is make them inherit the features they need. Two of these entities are volatile , which means that they can be created and deleted at any time while the game is running. Accounts can be created, but not deleted, so they're not exactly volatile. The only way to create new rooms, portals, and regions is to reload their template files.

Accounts

The " oddball " of the group is the account entity. Accounts really aren't used within the game, and their main reason for existing is to manage the characters that your players can own. Here's the class definition:

class Account : public Entity, public HasCharacters { public: void Load( std::istream& p_stream ); void Save( std::ostream& p_stream ); // Accessors std::string Password() BasicLib::sint64 LoginTime() accesslevel AccessLevel() bool Banned() int AllowedCharacters() void SetPass( const std::string& p_pass ) void SetLoginTime( BasicLib::sint64 p_time ) void SetAccessLevel( accesslevel p_level ) void SetBanned( bool p_banned ) void SetAllowedCharacters( int p_num ) protected: std::string m_password; // user's password BasicLib::sint64 m_logintime; // time of first login accesslevel m_accesslevel; // access level of player int m_allowedcharacters; // number of characters player is allowed bool m_banned; // is user banned? }; // end class Account

The first thing you should notice is that the class inherits from Entity and HasCharacters , so it automatically gets every piece of data and function from those classes. Consult the previous chapter to review information on the Entity and HasCharacters classes.

In addition to the standard entity datatypes and a collection of character IDs, the account entity has five variables , representing various data about itself including the following: a password, the time the account first logged in, the access level of the account, how many characters the account is allowed, and whether or not the account is banned.

NOTE

I put in the access-level ranking system for future expansion. Even tually, I would like to have an online editor capable of logging into the game and uploading new maps and items; this kind of capability would require a special implementation of the networking system, with its own protocol and handlers.

Most of this should be self-explanatory; the access level is about equivalent to the ranking system of the SimpleMUD, but it's not that important for the BetterMUD.

You can also ban people from the game, if they become too unruly; the logon process will not allow banned characters to log in.

Characters

There are actually two character entity classes in the game: character templates and the actual characters.

A character template is a simple structure that holds data about a character and copies that data into a new character every time the game generates one.

Character Template Class

The character template class is basically just a placeholder; it's meant to hold data loaded from a template file on disk, so that actual character instances in the game can copy this information into their characters. Here's the class skeleton:

class CharacterTemplate : public Entity, public DataEntity { friend class Character; public: void Load( std::istream& p_stream ); protected: typedef std::list< std::string > names; names m_commands; names m_logics; };

The character class is a simple template class that inherits from the Entity and DataEntity mixin classes. The line in bold declares that the Character class is a friend, and that it should have access to all the data held within it.

I typedefed a list of strings and called it names , and then gave the class two of these lists. These lists represent the names of the command modules and logic modules for characters. I'll discuss commands and logic modules when I get to the scripting and python chapters, so for now, all you need to know is that templates hold the names of scripts, rather than script objects themselves .

The template class can't do much. In fact, it only has one function: to load itself from a stream:

void CharacterTemplate::Load( std::istream& p_stream ) { std::string temp; p_stream >> temp >> std::ws; std::getline( p_stream, m_name ); p_stream >> temp >> std::ws; std::getline( p_stream, m_description ); m_attributes.Load( p_stream ); p_stream >> temp; // chew up the "[COMMANDS]" tag while( BasicLib::extract( p_stream, temp ) != "[/COMMANDS]" ) m_commands.push_back( temp ); p_stream >> temp; // chew up the "[LOGICS]" tag while( BasicLib::extract( p_stream, temp ) != "[/LOGICS]" ) m_logics.push_back( temp ); }

As you can see, it's not difficult. Both the name and the description of a character are loaded by line, because their names can have spaces. The databanks know how to load themselves automaticallyyou simply call m_databanks.Load( p_stream ) , and it loads. Here's an example of a sample character template in a text file:

[ID] 1 [NAME] Human [DESCRIPTION] This is a normal Human Being

[DATABANK] health 10 strength 10 [/DATABANK] [COMMANDS] go say chat get give drop look quit quiet [/COMMANDS] [LOGICS] combatmodule encumbrancemodule humanmodule [/LOGICS]

This defines a character template named Human , who has two attributes (health and strength), nine game commands, and three logic modules. It's not important what these modules and commands do right now, but keep this in the back of your mind.

Character Class

The Character class is a more complex version of the CharacterTemplate class. Instead of having names of commands and logic modules, it has the actual modules, as well as a slew of additional data that templates don't really care about (why would a template care if a character is logged in or not?).

Character Hard-Coded Data

Here's a condensed version of the Character class, with the functions removed so you can first see what data it has:

class Character : public LogicEntity, public DataEntity, public HasRoom, public HasRegion, public HasTemplateID, public HasItems { public: typedef std::list<Command*> commands; protected: entityid m_account; // account number bool m_quiet; // interpret typing as chat or command? bool m_verbose; // print room descriptions? bool m_loggedin; // are you logged in? std::string m_lastcommand; // the last command the character entered commands m_commands; // which commands the character has }; // end class Character

Characters inherit a logic collection, a databank, a room, a region, a template ID, and a collection of item pointers from their mixin classes.

On top of those things, the class also has a pointer to its associated account, two style modes , a logged-in state, a string representing the last command typed in, and a list of command pointers.

The quiet mode of a character is only applicable to players, and it involves how commands are interpreted by the game module. The gist is that if a character is in quiet mode, misspelling a command doesn't result in his accidentally saying something in the current room he's in. See Chapter 15 for a complete description (in the commands section).

NOTE

You can reward and punish players by adding and removing commands. If a player continuously abuses the global chat command, you can delete his chat command object to teach him a lesson, and if players prove worthy to your MUD, you can give them commands that allow them to help you run the realm. It's a cool, flexible system.

The verbose mode determines whether or not room descriptions are printed out to players when they enter a new room. Often, room descriptions are large and make the game run slow if you're moving around quickly, so you have the option of turning them off, and just seeing the name, people, items, and exits within a room.

The logged-in value should be self-explanatory, and the last command string serves the same purpose it did in the SimpleMUD::Game::m_lastcommand string from the SimpleMUD.

The final variable is a list of command pointers, which represents every command you can execute while in the game. If you have a command object named go , you can say go north while in the game, and the game searches for that command and executes it, or gives you an error if you don't have the go command.

Data Accessors

There are accessor functions for each of the hard-coded pieces of data (in addition to those inherited from the base mixin classes):

entityid GetAccount() bool Quiet() bool IsPlayer() bool Verbose() std::string LastCommand() bool IsLoggedIn() void SetAccount( entityid p_account ) void SetQuiet( bool p_quiet ) void SetVerbose( bool p_verbose ) void SetLastCommand( const std::string& p_command ) void SetLoggedIn( bool p_loggedin )

All simple accessors return a variable directly, set a variable directly, or perform a simple calculation. ( IsPlayer checks to see if m_account != 0 .) I don't think any of this code is important, so I'm going on to more interesting topics.

Other Functions

The Character class has more functionsfunctions that are more complex and interesting than the accessor classes:

Character(); ~Character(); void Add(); void Remove(); void LoadTemplate( const CharacterTemplate& p_template ); void Load( std::istream& p_stream ); void Save( std::ostream& p_stream ); commands::iterator CommandsBegin() { return m_commands.begin(); } commands::iterator CommandsEnd() { return m_commands.end(); } commands::iterator FindCommand( const std::string& p_name ); bool AddCommand( const std::string& p_command ) bool DelCommand( const std::string& p_command ) bool HasCommand( const std::string& p_command )

Construction Time Again

The first two functions are the constructor and destructor.

The constructor simply initializes the variables with default values:

Character::Character() { m_account = 0; m_loggedin = false; m_quiet = false; m_verbose = true; }

Obviously, a character doesn't have an account when first created, so it can't be logged in. The quiet mode is set to false , so that players entering the game can chat by default, and the verbose mode is set to true , so that players see the full room descriptions when they start off.

The destructor is important for characters because of the command system. Whenever a new command is retrieved from the command database, a new pointer to a Command object is returned, and the database assumes that the character who requested the command will manage it from then on. This means that you need to delete all commands when a character is destructed:

Character::~Character() { commands::iterator itr = m_commands.begin(); while( itr != m_commands.end() ) { delete *itr; ++itr; } }

Adding and Removing

The Add and Remove functions are helper functions that are used when loading a character from disk. These functions physically add and remove a character from the room and region he is in:

void Character::Add() { region reg( m_region ); reg.AddCharacter( m_id ); room r( m_room ); r.AddCharacter( m_id ); } void Character::Remove() { if( m_region != 0 && m_room != 0 ) { region reg( m_region ); reg.DelCharacter( m_id ); room r( m_room ); r.DelCharacter( m_id ); } }

The code uses accessor classes to perform database lookups, a concept I've discussed before. Later on in this chapter, I'll show you the actual room and region classes used to do this.

The Remove function removes a character from a room and region if its room and region are valid.

The next section explains the reason for these functions.

Loading and Saving

Every entity in the game has the ability to load and save itself to disk. Furthermore, the Load function can reload an entity from a stream, overwriting whatever data already exists in the character with new data. Let me show you the loading function first:

void Character::Load( std::istream& p_stream ) { if( !IsPlayer() IsLoggedIn() ) Remove();

At this point, the code has checked to see if the character is a player and if he is logged in. If either of those conditions is not true , then the Remove function is called, essentially removing the character from his room and region in the game.

Imagine this scenario; you have a character in room 5, and you reload him from a stream. The only problem is that the stream moves him to room 6. If you haven't removed the character from room 5 yet, the character thinks he's in room 6, but room 5 still thinks the character is there! D'oh! So you need to remove the character before any data is loaded. Now you can load the hard-coded attributes:

std::string temp; p_stream >> temp >> std::ws; std::getline( p_stream, m_name ); p_stream >> temp >> std::ws; std::getline( p_stream, m_description ); p_stream >> temp >> m_room; p_stream >> temp >> m_region; p_stream >> temp >> m_templateid; p_stream >> temp >> m_account; p_stream >> temp >> m_quiet; p_stream >> temp >> m_verbose; m_attributes.Load( p_stream );

And load the commands:

p_stream >> temp; // chew up the "[COMMANDS]" tag while( BasicLib::extract( p_stream, temp ) != "[/COMMANDS]" ) { if( AddCommand( temp ) ) { // command was added successfully, continue loading data commands::reverse_iterator itr = m_commands.rbegin(); (*itr)->Load( p_stream ); } else { throw Exception( "Cannot load command: " + temp ); } }

The function will first chew up a [COMMANDS] tag, which represents the start of a block of commands. Then it tries loading command names until it finds [/COMMANDS] . You'll see exactly how this works a bit later when I show you a sample of a character in text form. If a command fails to load, an exception is thrown, and the loading of the character is abandoned . It's up to whomever calls this code to safely reinsert the character into the realm.

When the new command has been loaded, the code obtains a reverse iterator that basically points to the end of the command list, which is the command that was just added; I then tell the new command to load itself from the stream.

Next, the logic module is loaded:

m_logic.Load( p_stream, m_id );

Character files hold more than just characters; they also hold a listing of all the items the character is currently holding. So the function starts a loop to do this:

p_stream >> temp; // chew up "[ITEMS] while( BasicLib::extract( p_stream, temp ) != "[/ITEMS]" ) { ItemDB.LoadEntity( p_stream ); p_stream >> temp; // chew up each "[/ITEM]" tag }

Any list of items in a character is surrounded by "[ITEMS]" and "[/ITEMS]" tags, and every item entry in the character is surrounded by "[ITEM]" and "[/ITEM]" tags. To actually load an item, the ItemDB , which is a global instance of the ItemDatabase class, is called and told to load an entity instance from the stream.

Here's the final act:

if( !IsPlayer() IsLoggedIn() ) Add(); }

This code adds the character back into the game, but only if he's neither a player, nor is logged in. This stipulation exists because players who aren't logged in aren't actually in the game; they're off in some strange imaginary ether -world, and the game just ignores them when they're logged off, so you don't want to add logged-off players to rooms.

NOTE

You can see from the listing that logic modules and command mod ules each have [DATA] and [/DATA] tags after the name of the module. This means that script modules, such as commands and logics, actually store data of their own. A command could keep track of the last time it was executed to ensure it is only executed once a day; or a logic module for a monster could track if it is hunting down a player or trying to get revenge for committing Goblin Genocide. The format is flexible enough for whatever you need to store.

Here's a sample character file:

[ID] 1 [NAME] Mithrandir [DESCRIPTION] You are a plain old boring human. Deal with it. [ROOM] 1 [REGION] 1 [TEMPLATEID] 1 [ACCOUNT] 1 [QUIETMODE] 0 [VERBOSEMODE] 1 [DATABANK] strength 20 health 30 [/DATABANK] [COMMANDS] get [DATA] [/DATA] give [DATA] [/DATA] drop [DATA] [/DATA] [/COMMANDS] [LOGICS] humanlogic [DATA] [/DATA] [/LOGICS] [ITEMS] [ITEM] [ID] 2 [NAME] Pie [DESCRIPTION] A BIG CUSTARD PIE [ROOM] 1 [REGION] 0 [ISQUANTITY] 0 [QUANTITY] 1 [TEMPLATEID] 2 [DATABANK] [/DATABANK] [LOGICS] [/LOGICS] [/ITEM] [/ITEMS]

NOTE

As you can see from the file listing, the data format for this game has become quite complex. The format I have will do for now, but maybe in the future, you could look into something designed to be even more flexible, like XML. The great est thing about XML is that there are XML file editors out there that can edit the data for you, no matter what it stores.

As you can see, it's a pretty complicated data format. Character files can become pretty large if they contain many items, commands, and logic modules. Of course, the benefit of having so much data available is that this becomes a flexible format for storing characters.

Saving players to a stream is a similar process, so I'm not going to bother with the code here.

Loading Templates

Loading a character from a character template is a fairly easy process. For the most part, you just need to copy the data over. You also need to generate new commands and logic modules:

void Character::LoadTemplate( const CharacterTemplate& p_template ) { m_templateid = p_template.ID(); m_name = p_template.Name(); m_description = p_template.Description(); m_attributes = p_template.m_attributes;

The previous chunk of code copies over the template ID (the normal ID of the current character should have already been set by the database when it was first created), the name, and the description. The last line copies over m_attributes , the databank. This is a really cool part of the databank class: You can copy it automatically (almost like magic!) into any other databank, because all the STL containers support copying. The downside is that any existing members of the databank are overwritten, but that's not such a big deal, since we're supposed to be loading the character template into a brand new character anyway.

NOTE

You may have considered using binary files to store data in the game; and it's a viable alternative. Binary data can be packed tightly onto disk, and this saves a great deal of space, especially for elements that require a lot of data, such as the new character class. I chose to avoid binary however, due to the fact that disk space isn't really a huge problem anymore (come on people, you can buy 300+ GB hard drives now). It's easier to edit files in text format, and the most important factor is that I want the datafiles to work on multiple systems. Binary data suffers from endian problems, meaning that different operating systems pack bytes in different orders, making a datafile on Windows completely useless on a Macintosh computer, unless you spend time converting the data into the native format. Rather than deal with that mess, I chose to use ASCII.

The code continues, loading all the commands and logic modules:

CharacterTemplate::names::const_iterator itr = p_template.m_commands.begin(); while( itr != p_template.m_commands.end() ) { AddCommand( *itr ); ++itr; } itr = p_template.m_logics.begin(); while( itr != p_template.m_logics.end() ) { AddLogic( *itr ); ++itr; } }

These two loops circle through the m_commands and m_logics lists inside the template, extracting each name, and adding the command or logic module to the character, using the AddCommand or AddLogic helpers. AddLogic was inherited automatically from the LogicEntity class from the previous chapter.

NOTE

The implication of loading a logic module based only on name alone from a template means that you can't give that module any default data. Giving the module default data would be useful when you want to use a certain logic module that contains different data in many places, but honestly, that situation hasn't come up much for me. Still, if it bothers you, you might want to find a way to eventu ally make script objects clonable (right now they aren't), so that when you create a new character from a template, you can clone the template's scripts, rather than get names and generate new scripts.

Command Functions

The last major grouping of functions in the character entity class deals with the commands. There are six of these functions: two to retrieve iterators into the command list; one to search for a given command name; and functions to add, remove, and check the existence of commands in a character.

CommandsEnd and CommandsBegin are simply wrappers around m_commands.end() and m_commands.begin() , so I won't bother posting them here.

The function to find a command ( FindCommand ) is somewhat complicated, however. Whenever you're playing a text-based game, the ability to condense and shorten commands is a very common feature, because typing attack goblin every time you see a goblin is going to become frustrating.

Instead, you'd like the players to be able to type att go or maybe even a g . Sure, you could hard-code some shortcuts, just as in the SimpleMUD ( a or attack , n or north , and so on), but that quickly begins to limit your engine.

Instead of providing shortcuts in the BetterMUD, I've decided to use a dual-linear search through the commands, trying to find complete and then partial command matches. For example, let's assume a character is given these commands:

go attack look north south east west say

When a user wants to find a command named g , he first loops through all those commands finding one matching the complete name g . Obviously there's no command g , so the game then starts the second iteration, looking for a partial match. This time, go matches partially, so the game thinks the user wants to go somewhere.

This is the same process you've seen used before in the SimpleMUD, when matching item and monster names.

Here's the command matching function, which returns an iterator pointing to the matching function (or the end iterator if none was found):

iterator FindCommand( const std::string& p_name ) { stringmatchfull matchfull( p_name ); // match full commands::iterator itr = m_commands.begin(); while( itr != m_commands.end() ) { if( matchfull( (*itr)->Name() ) ) // check match return itr; ++itr; } stringmatchpart matchpart( p_name ); // match part itr = m_commands.begin(); while( itr != m_commands.end() ) { if( matchpart( (*itr)->Name() ) ) // check match return itr; ++itr; } return itr; }

NOTE

Incidentally, this is the same reason why I don't use an std::map for the commands, but use an std::list instead. When you insert things into a map using strings as the key, the items are sorted by alphabetical order, since maps compare keys using the operator< of the key, and operator< on strings returns true when the items are in alphabetical order ( apple < orange ). For this reason, if you used maps, say would always come before south , so whenever the user types s , meaning to go south, the game will think he's trying to say something instead. This can become particu larly annoying, so using a list is a better idea, because you can rearrange the order in which commands are inserted, and put the most frequently used commands first.

I couldn't use the BetterMUD::match function, since that only works on containers full of entityid s, so I had to hack up my own dual-loop here, but it's not such a big deal.

The other three functions add, delete, or check the existence of command objects. These functions usually involve invoking the scripts in some way. For example, adding a command initializes a script that checks the existence of a command, and deleting a command retrieves its name. Executing scripts is prone to failure. You'll see this theme repeated throughout the last part of this bookscripts can fail at any time. These functions are exception-safe . They catch everything and return a Boolean based on success or failure:

bool Character::HasCommand( const std::string& p_command ) { commands::iterator itr = m_commands.begin(); while( itr != m_commands.end() ) { try { if( (*itr)->Name() == p_command ) // compare name return true; } catch( ... ) {} // just catch script errors ++itr; } return false; } bool Character::AddCommand( const std::string& p_command ) { if( HasCommand( p_command ) ) // can't add if it already has return false; try { m_commands.push_back( CommandDB.generate( p_command, m_id ) ); return true; // command added successfully } catch( ... ) {} // just catch errors return false; } bool Character::DelCommand( const std::string& p_command ) { try { commands::iterator itr = m_commands.begin(); while( itr != m_commands.end() ) { // find command if( (*itr)->Name() == p_command ) { delete (*itr); // delete command object m_commands.erase( itr ); // erase from list return true; // success! } ++itr; } } catch( ... ) {} // just catch errors return false; }

Since all the commands are stored as pointers within the list, you need to dereference them to get a pointer by calling (*itr) , and then use the operator-> to access members of the actual command classes. You can see from the AddCommand function that it tells something called the CommandDatabase to generate a new command (the line is in bold). I will cover this database in Chapter 14. Essentially the database creates a brand new Command object when you give it a name, returns the Command object, and assumes that your character will manage that command from now on. When your character is destroyed , he must delete the command objects, or you will get a memory leak.

Items

Items are the other volatile entity in the game, but they're somewhat simpler than characters. Since items are volatile entities, they're stored within template/instance databases, and thus need template and instance classes. In the naming conventions for characters, items are named ItemTemplate , and Item .

It's good to be consistent, so the classes dealing with items resemble the character classes, and thus I won't need to show you too much about them.

Item Templates

The item template class inherits from the base mixin classes you saw from the previous chapter. Noticing a trend here? The mixin classes are quite helpful since they are used so often:

class ItemTemplate : public Entity, public DataEntity { friend class Item; public: typedef std::list< std::string > names; void Load( std::istream& p_stream ); protected: bool m_isquantity; // is this a quantity object? int m_quantity; // if so, what is the quantity? names m_logics; };

In addition to variables inherited from the mixins , items come with only two hard-coded variables, Boolean and a quantity, which represent whether or not the item represents a quantity item . As I told you in Chapter 11, a quantity item is an item that represents a simple item type that can be grouped together, like coins and jewels .

Items have a databank and a collection of logic modules. Like character templates, the item templates store these logic modules as simple names, and load the actual scripts when the items are instantiated .

Here's a sample item template that can be loaded from a stream:

[ID] 1 [NAME] Fountain [DESCRIPTION] This is a large granite fountain. [ISQUANTITY] 0 [QUANTITY] 1 [DATABANK] [/DATABANK] [LOGICS] cantget [/LOGICS]

This is a simple item template; it doesn't have any data, and it holds one logic module, cantget . You'll see this module in Chapter 18. It prevents any character from picking it up.

That's really all there is to know about item templates.

Items

The actual Item class is somewhat simple too, as you can see from its definition:

class Item : public LogicEntity, public DataEntity, public HasRoom, public HasRegion, public HasTemplateID { public: void Add(); void Remove(); void LoadTemplate( const ItemTemplate& p_template ); void Load( std::istream& p_stream ); void Save( std::ostream& p_stream ); Item(); std::string Name(); bool IsQuantity() int GetQuantity() void SetQuantity( int p_quantity ) protected: bool m_isquantity; // is this a quantity object? int m_quantity; // if so, what is the quantity? }; // end class Item

Items inherit logic collections, databanks, a room, a region, and a template ID. In addition items have the same Add and Remove functions as in the SimpleMUD so that they can be added and removed from a location (items are a bit more complex than characters, though, since they can either be in a room or on a character, whereas characters can only be in a room). Items also have a LoadTemplate function, which loads item template data from an ItemTemplate , and the standard Load and Save functions, which stream an item to and from iostreams.

One function in the Item class is of particular interestthe Name function. You should have noticed that this function has already been inherited from the Entity class, so why in the world would I redefine it in the Item class? The answer involves the fact that items can be quantities . What happens if a player sees an item lying on the ground that represents 27 gold coins? Should it say, "Pile of Coins?" Or how about, "27 Gold Coins?" Or maybe, "Pile of 27 Coins?" There are so many possibilities that you should avoid hard-coding such a thing. Instead, I've opted for a flexible method. Items that are quantities have names such as "Pile of <#> Coins" or "<#> Gold Doubloons". This special Name function finds any instances of "<#>" in the string and replaces it with the quantity of items. So "Pile of <#> Coins" turns into "Pile of 27 Coins". The code to do this is really simple:

std::string Item::Name() { using BasicLib::SearchAndReplace; using BasicLib::tostring; if( m_isquantity ) return SearchAndReplace( m_name, "<#>", tostring( m_quantity ) ); else return m_name; }

This code uses my custom SearchAndReplace function from the BasicLib to search for and replace all instances of <#> with the string representation of the quantity.

The streaming functions and the template loading function are similar to the streaming and template loading functions from characters, so I don't think you'll be interested in seeing all that code.

Here's a sample listing of an item object instance:

[ID] 11 [NAME] Fountain [DESCRIPTION] This is a large granite fountain. [ROOM] 1 [REGION] 1 [ISQUANTITY] 0 [QUANTITY] 1 [TEMPLATEID] 1 [DATABANK] [/DATABANK] [LOGICS] cantget [DATA] [/DATA] [/LOGICS]

As you can see, the layout is similar to that of a template item, with the addition of the room and region, as well as the existence of the data fields for logic modules. The cantget module doesn't need data; therefore, there's nothing within its [DATA] and [/DATA] tags.

Ownership of Items

I've mentioned before that items can be owned by two different kinds of entitiesrooms and characters. I use a really simple method for defining how an item knows where it is. When an item exists within a room, its m_room variable represents the ID of that room, and m_region represents the region.

If the item is on a character, however, m_room is the ID of the character that the item belongs to, and m_region is 0.

Rooms

Rooms are a very simple entity types as well, and they don't add any extra data above and beyond the mixins they inherit:

class Room : public LogicEntity, public DataEntity, public HasRegion, public HasCharacters, public HasItems, public HasPortals { public: void Save( std::ostream& p_stream ); void Load( std::istream& p_stream ); void Add(); void Remove(); }; // end class Room

Rooms have a listing of all characters, items, and portals that are within that room, and they know what region they exist within.

This section is the shortest in the chapter, since there isn't anything remarkable about rooms at all. The real power comes from the logic modules of the rooms, which allow them to react to any event that may occur inside of a room. You'll see this in much more detail in Chapter 15.

Regions

Regions are almost as simple as rooms. In fact regions within BetterMUD exist only for a few reasons. First, regions help with the organization of the datafiles. It is incredibly difficult to manage a huge realm in the SimpleMUD, because the map data is all stashed in one file, and that can get unbelievably unmanageable after a while.

The second reason is behavior; sometimes you want a specific event to occur in every room within a region, and it really doesn't make much sense to use 80 copies of a single logic module within 80 rooms, when all the rooms in a region should have it.

The final reason is efficiency; the game is going to save single regions to disk at different intervals throughout the game, so that you don't end up dumping the entire database to disk at once, and lag up the game. Without regions, you're bound to dump everything at once when your game becomes large.

Almost all the discussion about regions deals with loading and saving them to disk, which is done in the RegionDatabase class, later on in this chapter.

Here's the class listing:

class Region : public LogicEntity, public DataEntity, public HasCharacters, public HasItems, public HasRooms, public HasPortals { public: void Save( std::ostream& p_stream ); void Load( std::istream& p_stream ); std::string& Diskname() { return m_diskname; } void SetDiskname( const std::string& p_name ) { m_diskname = p_name; } protected: std::string m_diskname; }; // end class Region

One variable has been added to the regions you were familiar with in the SimpleMUD, and that variable is their diskname . You'll see later on that regions frequently have names such as "The Elven Forest", or "The Dwarven Mines". There's nothing remarkable about that, except that those names have spaces in them, and on some operating systems, spaces in a file name are a huge no-no.

As you'll see later, I store regions inside their own directories, so "The Elven Forest" would ideally be located inside /data/regions/The Elven Forest/. That's also an illegal directory name on some operating systems, so most of the time you'll be making it look like this instead: data/regions/TheElvenForest/. Of course, it would look stupid in the game if a player saw: Mithrandir enters TheElvenForest . Instead of messing with that, I've added a new string that represents the name of the directory the entity is supposed to be located in. So a region with the name The Elven Forest would have a disk name of TheElvenForest .

Here's a sample of a region entity on disk:

[ID] 1 [NAME] Betterton [DESCRIPTION] Betterton is a run down town. [DATABANK] [/DATABANK] [LOGICS] [/LOGICS]

This region entity has no data or logic yet.

Portals

Portals are a part of the physical world in BetterMUD that differ most from the physical structure of the SimpleMUD.

In the SimpleMUD, the rooms knew how players could exit from them and move into different rooms, but the BetterMUD doesn't use this concept. Instead, it has portals, which define how players move from one place to the next, by defining a starting and an ending point.

A large part of building any world-type game is the ability to deny a player's access to an area and force him to perform tasks to gain access. For example, there's the classic "find the key to this door" quest, which you can probably find in every MUD out there.

The SimpleMUD has no way of denying access to a room; anyone can go anywhere . That makes the game simple, but that was the point in the first place.

In the BetterMUD, on the other hand, such a solution is unacceptable. You could easily hard-code the requirement for rooms to have "key-locks" on the doors, but hard coding such a thing into the physical world is unnecessary and limiting. One day, you might want to come up with a special "double-locked" door (the kind you see in all those military movies) for which you need two keys turned at the same time to access the nuclear command system behind the door. Or maybe a magic door that denies entrance to people of a specified level of "evilness", or whatever else you may think of. Heck, you may even want portals that have traps that switch on when a player enters the door (watch out for those saw blades! Ouch!), or doors that magically bless a player who walks through. Anyway, I'm sure you can think of hundreds of ideas, which is why hard-coding anything into a portal is a bad idea.

The basic design of the portals is that they have innumerable "paths," and each is a one-way path starting at one room and ending at another.

Let's say you have a portal with one pathway , which leads from room 1 to room 2. When a player is in room 1 and tries entering that portal, the game asks the portal if he can enter that portal, and if he can, the game tells the portal that the player has entered it, and then moves him to room 2.

Of course, if the portal doesn't have a path from room 2 back to room 1, the player can't get back (unless of course, there's a different portal object somewhere with that pathway in it).

Figure 13.1 shows a sample of some of the many possible ways to use portals.

Figure 13.1. Portals can be one-way, two-way, three-way and beyond. In addition, two different one-way portals can act like a two-way portal.

Basically, portals are objects that connect rooms and allow characters to move throughout the game.

Portal Entries

When designing portals, you could take any route you want. You could easily make portals just one-way objects, but then you'll have problems linking together two portals that should share a single module of logic (a door closed on one side should be closed on the other side).

You could make portals two-way, but that's not really flexible. If you assume every portal has two ways, you couldn't easily make one-way paths, or other tricky maneuvers.

So, the best idea I could come up with was using an n-way portal, which can contain any number of paths. These paths are defined by a simple portalentry structure:

struct portalentry { entityid startroom; // starting room std::string directionname; // name of the direction entityid destinationroom; // ending room void Load( std::istream& p_stream ); void Save( std::ostream& p_stream ); };

An entry consists of three pieces of data: a starting room, a destination room, and the name of the exit that the path represents.

The SimpleMUD supported four directions in the game: north, east, south, and west. The BetterMUD doesn't support any directions; it only supports exits based on portal names. Any room can have an infinite number of exits from it, as long as each exit has a unique name. For example, if you want a portal to connect rooms 1 and 2, and going from 1 to 2 is considered "going north," the direction name of that exit should be north . You'll see how this works a little later on.

Entries know how to load and save themselves to disk too, which makes loading and saving portals easier. You'll also see this later.

Portal Entity Class

The actual portal entity class is pretty simple. The only thing it adds on top of the various mixins it inherits is a list of portal entries, representing all the paths in a portal:

class Portal : public LogicEntity, public DataEntity, public HasRegion { public: typedef std::list<portalentry> portals; portals::iterator PortalsBegin() { return m_portals.begin(); } portals::iterator PortalsEnd() { return m_portals.end(); } void Load( std::istream& p_stream ); void Save( std::ostream& p_stream ); void Remove(); void Add(); protected: portals m_portals; // list of entries }; // end class Portal

Very simply, every portal has a list of entries and functions to iterate through it. Also present should be the familiar Add and Remove commands (customized for portals, of course), as well as the Load and Save commands. These have, of course, been customized for the portal's needs, but they are very similar to the same functions found in other entities, so I won't show them to you here.

Here is a sample listing of a portal that can be loaded from disk:

[ID] 1 [REGION] 1 [NAME] Garden Path [DESCRIPTION] This is a plain garden pathway. [ENTRY] [STARTROOM] 1 [DIRECTION] North [DESTROOM] 2 [/ENTRY] [ENTRY] [STARTROOM] 2 [DIRECTION] South [DESTROOM] 1 [/ENTRY] [/ENTRIES] [DATABANK] [/DATABANK] [LOGICS] [/LOGICS]

Note the entries. This particular portal contains two entries: one leading from room 1 to room 2, and the other going back the other way. The name of the first path is simply North , meaning that's the direction a player needs to type to enter it (that is, go north ). You can change the name to whatever you want ( flazzleblap ?), as long as it makes sense for the room. You can use this feature to give your game a more "realistic" touch, since your rooms are not limited to 4 or 8 "specific" exits.

Adding and Removing Portals

The Add and Remove functions for portals are a bit more sophisticated than those for the other entity types. This is mostly because a single portal can exist within any number of rooms. So how does one determine if a portal exists within a room? If a portal has a one-way path from room 1 to room 2, should it exist in both rooms? What if it doesn't have a path back?

The best way to manage this is to insert portals only into rooms that are starting points. If you have a portal that goes from 1 to 2 and 2 to 3, it should only exist within rooms 1 and 2, but not 3, since there's no starting point in room 3.

[ LiB ]

Категории