10.4 Arrays of critters: the ''cBiota'' class

One of the key members of the cGame class is a cBiota *_pbiota field. The cBiota class is a collection class that holds an array of pointers to cCritter objects. The cBiota object is based on an MFC array template, with a few special methods added. We encapsulate these methods into the cBiota rather than having them in cGame as part of the OOD strategy of not giving any one class too many responsibilities.

Some readers may be wondering why we have to use a collection of cCritter* pointers. Beginning programmers have a fear of pointers and their burdensome requirements of being initialized with new and removed with delete. Why not just a collection of cCritter objects? Once again, this is because we want polymorphism to work. In C++, a call like _pcritter->update(...) will work polymorphically and figure out the correct version of the method depending on what kind of cCritter* child class _pcritter actually is. But a call like _ccritter.update (...) will always just use the base class cCritter::update.

Now let's think about which kind of collection to use; an array or a linked list. This type of decision depends on what you plan to do with your collection, so let's think about what we'll do with the critters in a game.

Typically there will be 10?50 critters in action. As we step the game, we will repeatedly iterate through the collection of critters, updating them, moving them, drawing them, and so on. Now and then we will want to add new critters to the collection or delete old ones. Should we use a list or an array for our collection of critter pointers? Well, iterating through an array is faster than iterating through a list, but deleting objects is faster with a list than with an array. The cost of deleting something from the middle of an array is noticeable because then all of the higher-indexed array members need to be moved down one position in the array. On the whole, we expect more of our computation time to involve iterations than object deletions, so we'll use an array.

MFC provides a range of useful array templates. The particular template we use here is called CTypedPtrArray. This is what's known a serializable type-safe array. There's a bit about these templates in Chapter 22: Topics in C++, and Chapter 30: Serialization explains why the CTypedPtrArray is useful for saving and loading parameter files.

Because we may need arrays of critter pointers elsewhere in the program, we define a simple critter pointer array class cCritterArray and derive cBiota from that. We've drawn the diagram (Figure 10.3) so as to display the additional fact that our cGame class is going to hold a single cBiota object and we've also included some navigation arrows. Recall that we use the composition diamond symbol to indicate when an object (the object with the diamond) holds one or more objects of the class type at the other end of the line leading from the diamond.

Figure 10.3. Class Diagram of the cBiota


A cBiota object is an array of cCritter* pointers which also holds a cGame *_pgame pointer and a special bookkeeping array of simple objects called cServiceRequest. The member fields of cBiota can be seen in this partial listing of the prototype from biota.h.

class cBiota : private cCritterArray 
public: //Statics 
    static const int NOINDEX; 
        // -1, impossible index, For use by cBiota::_index(). 
//Non-serialized helper members; 
    cGame* _pgame; 
    CArray<cServiceRequest, cServiceRequest> _servicerequestarray; 
    cBiota(cGame *pownergame); 

The cBiota uses its _pgame field to get to a particular cGame object, and the cGame uses its _pbiota field to get to a particular cBiota object. But the roles of the two pointers are different: the former is simply a navigational aid, the latter is an example of composition.

When a cGame object is deleted, its _pbiota object is deleted as well. This is knows as a 'cascading delete.' The _pbiota member of cGame is a composed object, and it is typical for a delete to cascade to a composed object.

When you delete a cBiota object you don't cascade the delete to the member _pgame. The _pgame member of cBiota is simply a navigational aid that is put in place by the cBiota(cGame *pownergame) constructor.

One reason why we want the cBiota to navigate to the cGame is so that the cCritter objects can go through the cBiota to get at cGame information. Thus, the cCritter class has a pgame() accessor that's defined as _pownerbiota->pgame().

We populate a cBiota by using its Add method, which is an override of the standard CTypedPtrArray::Add. We override the method so that it won't let you accidentally add the same thing twice; also it sets the added critter's _pownerbiota field.

A cBiota object is responsible for deleting all of its members. That is, the cBiota destructor calls the destructor for each of the critters in the array.

The most important methods of the cBiota class are its 'array-walking' methods. It has draw, move, update, animate, render, and listen methods, each of which simply walks its array and calls the corresponding method for each member of cCritter. Thus, for instance, the cBiota::move looks something like this.

void cBiota::move(Real dt) 
    for( int i=0; i<GetSize(); i++) 

The place where almost all the cBiota array-walking methods get called is inside the CGame::step method, which is careful to call them in a certain order so as to make the program's behavior as parallel as possible. The draw method, however, gets called by CPopView::draw.

It's worth mentioning that the cBiota::draw walks the array in reverse order. This is because it's convenient to think of the critters early in the array as being visually on top of the others. But when you draw the critter sprites on the screen, the first drawn is going to have the other sprites drawn on top of it. The first shall be farthest, as it were. In graphics programming, this fact is called 'the painter's algorithm.' Usually our player is the first member of the cBiota array, and we like to have our player on top. So we walk the array in reverse order.

This consideration only matters in two-dimensional graphics, of course, as in three dimensions we would have the critters located at different depths, and use our current viewpoint to determine which order to draw them in.

Some of our games, such as PickNPop, or Dambuilder, allow us to use a pick or a drag cursor to select critters, and we store this information in the game by setting a cGame::_pfocus pointer to point to the critter being specially handled. One other special thing that cBiota::draw does is to put a highlight around the sprite of a critter that happens to be the 'focus critter' of the game. If you don't like this feature, either don't use the pick cursor or comment out the feature from the cBiota::draw code.

Service requests: the Command pattern in action

The cBiota class has a CArray<cServiceRequest, cServiceRequest> _servicerequestarray. The cServiceRequest class is a simple utility class that holds two fields, cCritter *_pclient and a CString request.

The purpose of the _servicerequestarray is to queue up requests from the critters. The reasoning goes like this. Suppose that during its update process a critter notices that its health is 0. Now the critter wants to do the right thing and die. If it's to die, then we should delete it from the simulation; if you're shooting hundreds of bullets it wouldn't do to keep all the bullets around after they hit something and 'die.' To get rid of a cCritter *pcritter we need to do at least two things:

  • Call delete pcritter.

  • Remove the invalid pcritter pointer from our cBiota array.

These are not actions that you'd want to take inside the middle of an i loop that's walking along a cBiota array. First of all it seems problematic to ask a cCritter to delete itself, and secondly it's not a good practice to change the size of an array you are currently walking through.

Our solution is to let a cBiota store up requests to do things: to delete critters, add critters, replicate critters, change the array location of critters, etc. And this is what we use the _servicerequestarray for.

One more point. It would mess up our cGame code if the current _pplayer ever actually got deleted. So if you were to look into the cBiota code, you'd find that even if a player makes a delete_me request, when the cBiota processes the service requests it doesn't actually call delete on the player.

    Part I: Software Engineering and Computer Games
    Part II: Software Engineering and Computer Games Reference