13.3 The ''cCritterBullet''

As mentioned in the last section, when a cCritterArmed fires a shot, its shoot method is called, and a new bullet gets initialized in two stages. First the bullet's no-argument constructor cBullet() is called. The constructor does the following.

  • Sets the bullet's _collidepriority to the relatively high value cCollider::CP_BULLET. Since this value is higher than cCollider::CP_CRITTER, this means that when a bullet hits a normal critter, the bullet invokes the collide method as the caller instead of as the argument.

  • Tells the bullet to have a limited lifetime by setting _usefixedlifetime to TRUE and setting the size of _fixedlifetime to cCritterBullet::FIXEDLIFETIME, which happens to be set to 3.0 seconds. (Note that you can do this for other critters, too if you like, although the default behavior is for critters to be 'immortal' with _usefixedlifetime set to FALSE.)

  • Assigns the bullet a yellow isosceles triangle to be its default sprite.

  • Sets the bullet's speed, its _maxspeed, and its _hitstrength.

In the second stage of a bullet's initialization, the cCritterBullet:: initialize(cCritterArmed *pshooter) is called with the shooting critter as the pshooter argument. The reference to pshooter allows the bullet to set its target according to the preferences of the pshooter.

The default cCritterBullet constructor gives the bullet a yellow isosceles triangle for its sprite. The default cBullet::initialize method does the following.

  • Matches the bullet's attitude to the shooter's.

  • Positions the bullet at the tip of the shooter's gun.

  • Sets the direction of the bullet's velocity to match shooter's _aimvector, using the speed that the bullet acquired in its constructor. The result gives us what we might call a 'muzzle velocity.' It would be physically correct to then set the bullet velocity equal to this muzzle velocity plus the shooter's velocity ? but in practice this gives unattractive game behavior. So we don't normally add in the shooter's velocity. We'll say more about this in the section on cCritterArmedPlayer below.

  • Attaches copies of the shooter's physics forces such as gravity and friction to the bullet.

  • Copies the shooter's _movebox to the bullet.

  • Gives the bullet the same _ptarget as the shooter.

The cBullet::update method is written to call the base class cCritter::update and to kill off the bullet if it has touched an edge of the world and its _dieatedges flag is on. The way that update can tell if a critter has touched the edges of the world (wrapped or bounced from the edge on its last move) is to look in its _outcode, which is set by the cCritter::move method to be non-zero if the critter touches the border. The cBullet::update code looks like this.

void cCritterBullet::update(CPopView *pactiveview) 
    cCritter::update(pactiveview); /* Feels force, also checks _age 
        against _lifetime. */ 
    if (_outcode && _dieatedges)/* _outcode nonzero means near an 
        edge. This keeps bullets from bouncing or wrapping, but it 
        also makes the critters unable to fire when they are really 
        near an edge. */ 

The _dieatedges flag is TRUE by default for the cCritterBullet and the cCritterBulletSilver. The reason for having it normally be TRUE is that the game tends to look confusing if too many bullets wrap or bounce, so we normally kill them whenever they hit the edge. But you can optionally turn this behavior off by setting the _dieatedges flag to FALSE. The cCritterBulletRubber constructor and the cCritterBulletSilverMissile constructors both set _dieatedges to FALSE, in the first case because it's fun to watch the rubber bullets bounce around, and in the second case because we want to make the silver missiles particularly lethal and hard to escape.

Also remember that since a bullet has its _usefixedlifetime flag on, the base cCritter::update call will kill off the bullet if it's older than the age _fixedlifetime, which is normally going to be three seconds.

The most characteristic part of a bullet's behavior is to damage things, and this code is in the cCritterBullet override of the cCritter::collide(cCritter *pcritter) method. We don't put the damage-other-critters-when-you-touch-them code into the cCritter::update because bullets hitting things is about the interactions between pairs of critters. It makes more sense ? and is more time-efficient ? to handle this inside the collide method which is already in place to look at each pair of critters that you think you might be interested in.

The bullet collide code goes like this.

BOOL cCritterBullet::collide(cCritter *pcritter) 
    if (isTarget(pcritter)) //If you hit a target, damage it and die. 
        if (!touch(pcritter)) 
            return FALSE; 
        int hitscore = pcritter->damage(_hitstrength); 
        delete_me(); /* Make a service request, but you won't go 
            away yet. */ 
        if (_pshooter) //Possible that _pshooter has died, is NULL. 
        return TRUE; 
    else //Bounce off other critters in a normal fashion. 
        return cCritter::collide(pcritter); /* Bounce off non-target 
            critters */ 

Given that the bullet's collision behavior is more complicated than a standard critter's collision behavior, the Pop Framework gives a bullet priority in calling the collide method. As mentioned above, we do this by setting the bullet's _collidepriority to a higher value than ordinary critters have. In addition, we override the int cCritterBullet::collidesWith(cCritter *pcritter) method to return cCollider::DONTCOLLIDE if pcritter is (a) the bullet's shooter or (b) another bullet from the same shooter. Condition (a) is fairly obvious. We need (b) because sometimes if you are moving in the direction that you're shooting, your bullets may be overlapping each other, and you don't want them to destroy or to bounce off of each other. You can check Chapter 11: Collisions to review the details about how collidesWith is used for adding collision pairs to the game's cCollider list of collision pairs.

The BOOL cBullet::isTarget(cCritter *pcritter) tells whether a given pcritter is something that the bullet wants to damage. The default isTarget method returns TRUE for every pcritter except for critters of the type cCritterWall.

The cCritterBulletSilver overrides the isTarget method to only target one particular critter. What makes these bullets 'silver' is that they are targeted for one thing and one thing only, in analogy to the silver bullets of legend with which one is supposed to be able to shoot a werewolf. For these guys, the isTarget method is simplicity itself.

BOOL cCritterBulletSilver::isTarget(cCritter* pcritter) 
    return pcritter == _ptarget; 

An example of a use of cCritterBulletSilver occurs in the Spacewar game, in which the enemy UFO critters are shooting silver bullets at the player. We want the player to be able to fend off these bullets by shooting at them. You might think the player's bullets would blow the silver bullets up in any case, as the default cCritterBullet::isTarget would return TRUE for these bullets. But remember that when we have a pair (pa, pb) we only call pa->collide(pb) or pb->collide(pa), but not both. If pa and pb are both bullets; we might be unsure about which one gets to control the collide; indeed this could simply depend on where they happen to be listed in the _pbiota array, which in turn depends on exactly when you pressed the spacebar to shoot your bullet relative to when the UFO shot its silver bullet. Seemingly it could happen that when your bullet hits a silver bullet, it is the silver bullet's collide method which is in control.

In order to avoid this bad state of affairs, we have the cCritterBulletSilver constructor set the silver bullet's _collidepriority to a value cCollider::CP_ SILVERBULLET which is slightly less than cCollider::CP_BULLET. This means that only correctly ordered bullet-to-silver-bullet collision pairs will be added to the game's collider list.

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