13.6 The two-way ''cCritterArmed/cCritterBullet'' association

This optional subsection has some information about the somewhat tricky issue of how we maintain a two-way association between objects that can be deleted at any time.

As we mentioned at the start of the chapter, we will allow only some limited number of player bullets to be active at one time. If a player has shot, say, eight bullets and wants to shoot another, what we'll do is to remove the oldest of the bullets when we shoot a new one. (Of course the number doesn't have to be exactly eight; you can set it as cCritterArmed::MAXBULLETS.) This design means that the player needs to maintain an array of bullets. We choose an array rather than a list, because we traverse the collection a lot, and its not going to be big enough to make removing items from it a burden.

So a cCritterArmed object has an array holding pointers to the bullets that it has shot. And we do this for two reasons.

  • If an armed critter wants to shoot more than some limited number of bullets, it should delete its oldest bullet in order to make space for a new one.

  • When an armed critter is deleted it needs to tell its bullets that it's gone.

The reason the armed critter needs to tell its bullets when it's gone is because each bullet needs to know who shot it. That is, a cCritterBullet has a cArmedCritter *_pshooter pointer to the 'shooter' critter that fired it. This for the following three reasons.

  • A bullet should not collide with or damage its shooter.

  • When the bullet damages something it should award points to its shooter.

  • When the bullet dies is it needs to tell its shooter that it's gone.

We often have a situation where an object A has a member pB which is a pointer to an object B. Now if B is not 'doing anything' independent of A, this is quite safe. We simply let A create and destroy the B in, respectively, its constructor and destructor. But if there is a chance that B might go off and get deleted 'on its own,' then we need to make sure that B tells A that its pB is no longer useable.

In the case of the bullets and the shooters, each of them is doing things on its own, and may be deleted while the other one is still alive. Since the shooter has pointers to its bullets, when a bullet dies, the bullet's destructor needs to tell the shooter to get rid of the soon-to-be invalid bullet pointer. Conversely, since a bullet has a pointer to its shooter, when the shooter dies, its destructor needs to tell each of the shooter's bullets to get rid of its soon-to-be invalid shooter pointer.

Let's take a look at the two relevant destructors, first the bullet's, and then the shooter's.

    if (_pshooter) /* The cCritteArmed destructor sets _pshooter to 
        NULL in its destructor. So if _pshooter isn't NULL, then it's 
        still a good pointer. */ 

The removeBullet method looks to see if the bullet is in fact in the _bulletarray, and if it is it takes out that entry from the array and shrinks the array by one, moving any higher positions down one slot.

And here's the cCritterArmed destructor.

    /* It could cause a crash if any surviving cCritterBullet still 
        has a _pshooter pointer to this deleted cCritterArmed. So I 
        set all the _bulletarray bullets' pshooters to NULL, 
        and everywhere in the bullet code where I might use a 
        pshooter, I always check that it isn't NULL. */ 
    for (int i=0; i<_bulletarray.GetSize(); i++) 
        _bulletarray.GetAt(i)->_pshooter = NULL; 

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