Basic Entity Concepts

[ LiB ]

All things in the BetterMUD, just as in the SimpleMUD, are entities. They are the physical objects within the game, which will be stored by the databases and operated on by the C++ physics core .

IDs

Entities are accessed by their entityid just as in the SimpleMUD. In the SimpleMUD, I chose to use 32-bit unsigned integers for these IDs, which gave you a range of around four billion available IDs for each entity type.

Due to some limitations in Python, however (the fact that it doesn't support unsigned integers, for one), I used signed integers as IDs in the BetterMUD. A signed integer can have roughly two billion positive values, so if you assume that negative IDs and 0 are invalid, you're left with two billion possible IDs.

NOTE

Older bit MUDs that were built around 16-bit values frequently ran over their boundaries, and this was a serious problem in the past. How ever, 32-bit values are quite large and should be enough for what you need. If you don't think so, it's easy to convert the entities over to some 64-bit format, and with 18,446,744,073,709,551,616 total possible IDs, I think it's a safe bet that you'll never run out. I'm com fortable with my puny two billion entity limit, however.

Now, before you start saying "Oh no! Less is worse !" you should think about that for a moment. Two billion is an incredibly large number. If you assumed that you just stored the IDs for two billion objects in memory, at 4 bytes per ID, you're looking at requiring 8 GB of memory, just to store the ID numbers of two billion entities. I've never seen a MUD that required anywhere near that many entities.

At the heart of it all is the Entity.h file, which contains the entityid datatype and the Entity class. Here's the typedef for entityid :

 typedef signed int entityid; 

Now, whenever you refer to entities in the game, you refer to this typedef. If you need to change the typedef, all it takes is one simple change to this line, and suddenly all your entities are based on a different numbering scheme. It's that simple.

Entity Class

In the SimpleMUD, the base entity class had two things: an ID and a name . I've expanded that a bit, and entities in the BetterMUD have four things:

  • ID

  • Name

  • Description

  • Reference count

The actual entity class has these functions:

 std::string Name() const; std::string Description() const; entityid ID() const; void AddRef(); void DelRef(); int Ref() const; void SetName( const std::string& p_name ); void SetDescription( const std::string& p_desc ); void SetID( entityid p_id ); 

As you can see, these are your standard get-and-set accessor functions. In the SimpleMUD, I used mainly functions that returned references, but that was only for brevity. Strictly speaking, that's bad engineering practice. Even though it's a little bit more work to create separate get-and set- functions for each variable, you'll be thanking me when you need to change the game so that modifying one variable makes something else happen within the game.

Auxiliary Classes

There are a few auxiliary classes you can use in conjunction with entities. Different types of entities may share traits in common with other entities, but not all entities share all traits.

NOTE

Reference Counting and the Future

The reference count in the previous list is designed to facilitate future additions to the MUD. I haven't gone over this idea yet, but there are objects in the BetterMUD that act similar to the SimpleMUD's database pointers; I call them accessors . An accessor is a simple lightweight class that is used to access entities in the databases.

Whenever you create an accessor pointing to an entity, that entity's reference count is increased, and whenever you destroy the accessor, that entity's reference count is decreased. What this means is that if the game is currently using entities somewhere, the reference count will be more than 0. If you've got three different places in the code with accessors pointing to one entity, its reference count is 3.

The idea behind this is that someday you may want to move the BetterMUD into a true multithreading environment, so that the database and the game can operate on different threads, and so that the database knows when it's safe to write individual entities to disk. If the reference count is more than 0, the game is currently using that entity somewhere and may be modifying its data, so it's not safe to write it to disk.

Another benefit is that the accessors could be modified to use mutexes (remember them from Chapter 3?), so that if the database is writing an entity to disk, an accessor has to wait until the database is finished to access the entity. I may implement these ideas one day, if I get a chance to create a better database system for the BetterMUD. Check out the news on the BetterMUD (dune.net, port 5110) to see the latest improvements.

Basic Data Classes

For example, the character and item entity types both need to know which rooms and regions they are in. For other entities, such as portals, regions , and rooms, having this information would be useless; a room can't be in another room (at least in this design, it can't), and a region definitely can't be in a room, because it's supposed to be the other way around. Portals can be in rooms, but they are a special case, since they can be in many rooms at once.

So, what do you do? Do you give characters and items their own m_room variables ? That's a lot of wasted work if you ask me. Do you create a new class, say, RoomEntity , and have characters and items inherit from that? That may work at first, but eventually you're going to end up with lots of multiple inheritance problems if you try sharing other variables across entities.

The method I chose is to use a simple data class . Here is the data class for a room tag:

 class HasRoom { public:     HasRoom() : m_room( 0 ) {}     entityid Room() const           { return m_room; }     void SetRoom( entityid p_room ) { m_room = p_room; } protected:     entityid m_room; }; 

The class has a piece of room data, named m_room , and it has three functions. Room returns the ID, SetRoom sets the ID, and the constructor auto-initializes the room variable to 0 whenever it is created. There are two other classes like this: HasRegion and HasTemplateID .

Container Classes

Following in the same tradition, there are several container classes that you can add to your entities. In the game, entities often need to know the IDs of objects that they contain; characters need to know what items they have, rooms need to know what items they contain (the characters and portals they have), and so on. As before, since different types of entities can share similar containers of items, it makes sense to make a special class that holds a container of that specific item, and inherit from that.

Here's an example of the HasCharacters class:

 class HasCharacters { public:     typedef std::set<entityid> characters;     typedef characters::iterator charitr;     void AddCharacter( entityid p_id )      { m_characters.insert( p_id ); }     void DelCharacter( entityid p_id )      { m_characters.erase( p_id ); }     charitr CharactersBegin()               { return m_characters.begin(); }     charitr CharactersEnd()                 { return m_characters.end(); }     size_t Characters()                     { return m_characters.size(); } protected:     characters m_characters; }; 

The class uses two typedefs to define a set of entityid s and an iterator into that set.

I've decided to go with sets for storing data within entities. I could easily have gone with lists or vectors, but I feel that sets hold the best performance capabilities. Sets have O( log n ) insertion and deletion time, which on average, works out better than the O(1) insertion and O(n) deletion time for lists and vectors. Basic Entity Concepts

NOTE

I use typedefs quite often within the BetterMUD, espe cially for containers. The reasoning for this is quite simple: In the future, you may need to change the way a container works by turning it into a list or something else. This way, whenever someone uses your room class, he can refer to its container of characters as: Room::characters , instead of needing to remember which container it actually is stored in. I've also typedefed the iterator, so you can refer to character iterators inside a room like this: Room::charitr . Trust metypedefs make your life so much easier.

Sets also have an interesting property that will make your life much easier in the long run: They can't hold duplicate data. This means that the set data structure automatically makes sure that you never have more than one ID inside it. This can save you lots of pain in case you accidentally add an entity to a room more than once.

I have created four different container classes: HasCharacters , HasItems , HasRooms , and HasPortals .

NOTE

When an algorithm is classified as O(log n), that means that if there are n elements inside the container, you take the logarithm of that number, and that's approximately how many operations it will take to complete the algo rithm at most an upper bound, in other words. The base-2 logarithm of 128 is 7, meaning that to insert and delete anything from a set of 128 items will take approximately 14 operations (7 for insertion, 7 for deletion). On the other hand, if you used a list of the same size, inserting would be instant (O(1). That means about 1 operation. To delete something from the list, you need to search through the whole thing to find what you want to delete. That would take on average 64 operations (1 at minimum, 128 at maximum). Lists are great for insertions, but bad for searching-deletions.

Complex Function Classes

There are two complex classes that entities inherit from as well: the DataEntity class and the LogicEntity class.

Data Entities

The DataEntity class stores a databank. In Chapter 11, "The BetterMUD," I told you that characters and items have access to a flexible system of attributes, so that you can add and remove attributes from characters and items at any time during the game. A databank implements this behavior; I will go over it in more detail a little later on. For now, all you need to know is what a data entity can do.

A data entity has five functions:

 int GetAttribute( const std::string& p_name ); void SetAttribute( const std::string& p_name, int p_val ); bool HasAttribute( const std::string& p_name ); void AddAttribute( const std::string& p_name, int p_initialval ); void DelAttribute( const std::string& p_name ) 

For the BetterMUD, all attribute values are stored as int s. I've found that I rarely have a use for floats and I dislike their lack of precision, so using int s for attribute values is an acceptable compromise.

All these functions are string based, which means you can use entities that inherit from this class flexibly. Look at the following code, for example, which assumes that I have a data entity named d :

 d.AddAttribute( "strength", 10 );    // insert an attribute into the object int s = d.GetAttribute( "strength" );// get attribute bool b = d.HasAttribute( "strength" );  // true b = d.HasAttribute( "pies" );           // false d.SetAttribute( "strength", 20 );       // stronger now! d.DelAttribute( "strength" );           // no more strength s = d.GetAttribute( "strength" );       // uh oh! Exception thrown! 

Most scripting languages are built around the same ideas ( especially Python, which acts the same with any datatype you use; see Chapter 17, "Python," for the nitty gritty details).

This should give you a really great opportunity to increase the flexibility of your MUD.

Logic Entities

On the other side of the spectrum are logic entities, which wrap around a LogicCollection . A logic collection is a cool object in the BetterMUD. It essentially wraps around a bunch of logic modules, so that entities can have more than one logic module attached to them, as Figure 12.1 shows.

Figure 12.1. A logic collection holds a variable number of logic modules, which can be added and removed at will.

graphic/12fig01.gif


Basically, with a logic collection installed into an entity, you can add whichever logic module you want. If you have a special logic module that responds when someone tells it something, you can add that logic into it, without changing its responses to other actions within the game.

Here are the functions that a LogicEntity has. (I use some classes and concepts you haven't seen before, so bear with me for a moment.)

 bool AddLogic( const std::string& p_logic ); bool AddExistingLogic( Logic* p_logic ); bool DelLogic( const std::string& p_logic ); Logic* GetLogic( const std::string& p_logic ); bool HasLogic( const std::string& p_logic ); int DoAction( const Action& p_action ); int DoAction(     const std::string& p_act,     entityid p_data1 = 0, entityid p_data2 = 0,     entityid p_data3 = 0, entityid p_data4 = 0,     const std::string& p_data = "" ) 

I'm jumping ahead a little here, since I explain the logic system in detail in a later chapter, but for now, it is important to know what your entity classes can do.

Within the game, all logic modules are referenced by name, so you can add door-logic to a portal as follows :

 p.AddLogic( "portallogic_door" ); 

As long as the game knows about a logic module named portallogic_door , your portal now acts like a door, and refuses access to people when it is closed. The cool thing about this is that you can add other modules whenever you want:

 p.AddLogic( "portallogic_onlyadmins" ); 

This kind of door acts like other doors and also blocks access to people who aren't admins.

NOTE

I mentioned that LogicEntity s wrap around LogicCollection s. Because of this, I'm not going to show you the code for this class, since it basically just passes the arguments on to the collection class. One thing you should be aware of, how ever, is that when collections have errors, they throw exceptions by default. However, it's not really a good idea to have your entity classes throwing exceptions around all over the place. The four functions that return Booleans catch exceptions and return false if an error occurred. This way, those functions are safe to call and won't cause your program to cascade out of control if they can't execute.

Pretty cool, isn't it?

You can remove logic just as easily, and check to see if an object has a logic module of a specific type installed, and so on. It's all very flexible, and that's great.

Now, whenever an action happens to the entity, you just send the action event to the entity using DoAction , and every module is automatically told about the action.

I will go over logic modules, collections, and actions in much more detail in Chapter 15, "Game Logic." For now, you only need to have a general idea of what they can do.

Entity Requirements

Tables 12.1, 12.2, and 12.3 show listings of which entities need which auxiliary classes.

Table 12.1. Auxiliary Data Needed by Entities

Entity

HasRoom

HasRegion

HasTemplateID

character

yes

yes

yes

item

yes

yes

yes

room

no

yes

no

portal

no

yes

no

region

no

no

no

account

no

no

no


Table 12.2. Containers Needed by Entities

Entity

HasCharacters

HasItems

HasRooms

HasPortals

character

no

yes

no

no

item

no

no

no

no

room

yes

yes

no

yes

portal

no

no

no

no

region

yes

yes

yes

yes

account

yes

no

no

no


Table 12.3. Complex Containers Needed by Entities

Entity

Data

Logic

character

yes

yes

item

yes

yes

room

yes

yes

portal

yes

yes

region

yes

yes

account

no

no

Basic

Entity

Concepts


Using these classes, you can simply mix-and-match which parts you need for each entity. (It's like putting together a wardrobe.) These classes are typically called mixins . Figure 12.2 shows the inheritance hierarchy for two types of entities: characters and regions.

Figure 12.2. The inheritance hierarchy for characters and regions.

graphic/12fig02.gif


Note that LogicEntity inherits from the base Entity class. This is done because logic entities need to be able to tell their logic modules which ID they are attached to. So if a class inherits from a LogicEntity , the class doesn't have to inherit from an Entity . Here's a sample class declaration for characters, which multiple-inherits from all of its mixins:

 class Character :     public LogicEntity,     public DataEntity,     public HasRoom,     public HasRegion,     public HasTemplateID,     public HasItems 

NOTE

Using multiple inheritance (MI) is a controversial topic. Some people love it, and some people hate it. Some people never even need MI, but if it makes your life easier, why not use it? In the classes I use, MI is simple and easy to manage. This is because all the base classes are mutually exclusive , which means, that they don't share any bases, functions, or data in common. MI gets to be tricky if you use conflicting bases, though. For example, suppose you added another line to the inheritance list of the character class, public Entity . Now, since LogicEntity inherits from Entity as well, what the heck happens? It turns out, by default, C++ creates a class that has two instances of the Entity class, which you almost certainly didn't want. To solve this, you need to use virtual inheritance , which fixes the so-called diamond-inheritance problem . This issue is beyond the scope of this book, so feel free to explore it on your own. Just beware that MI is a tricky concept to implement correctly.

Accessors

In a major departure from the SimpleMUD, I decided against the use of database pointer objects . I have found that it's much easier to keep containers of IDs in memory, rather than pointer objects. All accessors in the BetterMUD can be found in the directory /BetterMUD/BetterMUD/accessors on the CD.

Comparing Database Pointers to Accessors

Instead of pointer objects, I use accessor objects.

Figure 12.3 shows an example of the two different methods you can use to access entities in a database. The first method, used by the SimpleMUD, is slow and elementary. The second method, used by the BetterMUDs, has accessors that are quicker and more efficient because they perform the database lookup when they are created.

Figure 12.3. Two methods for performing functions on entities stored within databases.

graphic/12fig03.gif


NOTE

Database accessors are basically smarter pointers than the database pointers of the SimpleMUD. To avoid confusing the two different meth ods, however, I refer to the new idea as accessors, rather than pointers.

Database pointers were a quick hack in the SimpleMUD; they were essentially a proxy class that would perform lookups from the database and then work on the object. Of course, that was all wasted effort if you did two operations in a row, like this:

 player p = 10; p.Name() = "RON"; p.Money() = 100000000;  // I wish 

The operations perform two lookups in the database, even though the lookups are for the same object. So this represents wasted effort. To fix this in the SimpleMUD, I found myself using the following code quite often instead ( assuming dbp is a player database pointer object):

 Player& p = *dbp; p.Name() = "RON"; p.Money() = 100000000; 

I ended up asking the database pointer object to return a reference to the player, and then used the actual Player object to work on, which really defeated the whole idea of a database pointer in the first place.

Features of Accessors

To fix this, I created a database accessor concept.

Database accessors have several features:

  • They perform lookups when they are created.

  • They increment their entities' reference count when they are created.

  • They are used from within Python to access needed parts of the game.

  • They release their hold on an entity when they are destroyed , decreasing its reference count.

NOTE

In my opinion, the lack of a base class for database accessors or macro is the one big flaw of the BetterMUD. I had to make this choice for many reasons, however. First and foremost is the major problem of circular dependen cies, which I highlighted with the SimpleMUD. C++ limits itself quite a bit sometimes, and it is annoying, but we have to live with it. Another problem is the way that I generate interfaces between Python and C++, which you'll see in Chapter 17 . The code generators I use don't play well with templates and inheritance. (The generators were designed for C, after all.) So, basically, as you'll see later on, accessor classes basically need to be copied and pasted from the class you are accessing. This can be annoying, but since most of the game expansion can be done in Python any way, it's not a big deal. You'll find that you won't need to be changing the accessor class definitions too often.

Here's an example of the character accessor, which performs lookups for Character entities:

 character c( 100 ); c.SetName( "Ron" ); c.SetDescription( "He is an awesome dude, as Strongbad would say." ); 

When the accessor is created, it looks up a pointer to the Character object it needs to access, which it then keeps so that you can perform fast operations on it, without needing to look up the entity again. When the accessor goes out of scope, the accessor tells the entity that it's no longer referencing it, and its reference count is decreased, so that the database knows there is one less accessor pointing at that entity.

NOTE

I chose to use the same naming scheme as the SimpleMUD for entities and their accessor objects. For example, the class Character with a capital "C" represents the actual entity, and the class character with a lowercase "c" represents the accessor objects.

Accessor Iterators

Accessors typically have their own iterators built in, which means that an accessor acts like an iterator. Take the character entity, for example. Characters have container items that they are carrying, and because of this, you need a way to iterate over those items.

Of course, the easiest way to do this would be to make the accessor return an actual iterator to the item container, but alas, there is a problem with this method. The program I use to generate interfaces between C++ and Python doesn't fully support interaction between C++ and Python iterators. (There is limited support for vector iterators, but I'm not using vectors. D'oh!)

So instead, accessors sometimes wrap around a single iterator for each of their containers. Characters wrap around an item iterator, and you can use functions like this:

 character c( 100 ); c.BeginItems(); while( c.IsValidItem() ) {     entityid i = c.CurrentItem();     c.NextItem(); } 

It's fairly simple. If you ever need another iterator, you can easily create another accessor object.

That's as much as I want to tell you about entity accessors for the time being. I'll get back to them later on, after I've shown you the intermediate stuff.

Helpers

To help with matching names of entities, I've included a few helper functions and classes inside the Entity.h file. Since you're probably sick of all of the string-matching code I threw at you in the SimpleMUD, I'll spare you the details and just show you how to work with the helpers.

Manual Matching

Two functors deal with string matching: one matches full names, and one matches partial names. They are called stringmatchfull and stringmatchpart . (How original!) Anyway, they work like this:

  1. Creates a matching functor with the name you are looking for

  2. Loops through a list of names to see if any of them match

Here's an example of full matching:

 stringmatchfull matcher( "the rain in spain" ); bool b = matcher( "The grain in spain" );   // false b = matcher( "the RAIN in spain" );         // true b = matcher( "the rain" );                  // false 

Matchers automatically disregard case, for obvious reasons. Partial matchers work the same way:

 stringmatchpart matcher( "the rain in spain" ); bool b = matcher( "the" );                   // true b = matcher( "spain" );                      // true b = matcher( "ain" );                        // false b = matcher( "grain" );                      // false 

The matchers use the same matching rules that the SimpleMUD used. Partial matching only returns true at the start of words; it won't match sequences of characters inside a word. That's why "ain" returns false , even though it appears twice in the statement.

Automatic Matching

I have three automatic matching functions that you can use on an STL container of entityid s to either perform single or dual-pass matches, or a dual-pass partial name search.

However, the functions need to know what type of accessor you are using to look up the entities. Say you have a set of IDs, named s , which represents a bunch of characters. This is how you would perform a one-pass full match on the set:

 set<entityid>::iterator itr; itr = matchonepass<character>(     s.begin(), s.end(),     stringmatchfull( "Ron" ) ); 

One odd bit of syntax you may notice is the <character> after the function name. When calling template functions, C++ usually deduces the template types by the arguments you pass into it; however, since you're not passing in a character accessor, the function has no way of knowing that you're trying to search a container of characters. Instead, you must tell the function that you're looking for characters. If you were looking through a container of IDs that represented Item objects, you would call it like this: matchonepass<item> .

The function returns an iterator and takes two iterators and a functor as its parameters. The two iterator parameters are supposed to represent the range of items you want to search, so you have the option of searching only a particular range, or the whole container. The third argument is a functor that returns either true or false . When scanning through the container, an iterator is returned that points to the first object that returns true . So, after running this code, itr can be one of two things: an iterator pointing to a character entity whose name is "Ron", or s.end() , which means that "Ron" isn't within the container.

You can easily turn that into a one-pass partial matcher by replacing stringmatchfull with stringmatchpart .

Of course, there will be times when you need to perform a dual pass search on a container as well:

 set<entityid>::iterator itr; itr = matchtwopass<character>(     s.begin(), s.end(),     stringmatchfull( "Ron" ),     stringmatchpart( "Ron" ) ); 

This code performs a two-pass search. First it searches for full matches, and then, it searches for partial matches if no full matches were found.

The final function is a helper that automatically performs a full/partial two-pass search on a container of entityid s:

 set<entityid>::iterator itr; itr = match<character>( s.begin(), s.end(), "Ron" ); 

As you can see, it's a lot cleaner than the other two functions.

[ 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