8.3 Critter methods

8.3 Critter methods

In order to run a game, we repeatedly call six methods for each critter, cycling through the calls over and over. The cGame::step method orchestrates the calls.

Figure 8.4 isn't any particular kind of official UML diagram, it's simply an informal way of showing the order in which a critter object is cycled through its main method calls, with time flowing in the clockwise direction.

Figure 8.4. Critter methods called by the cGame step method


The Update , Feelforce , and Feellistener methods

We give the cCritter a basic update(CPopView *pactiveview, Real dt) method. The argument isn't often used; a bit more about it appears below.

The basic update method feels the forces affecting the critter, and changes the acceleration of the critter accordingly . In addition, the basic update checks if the critter should die of old age.

 void cCritter::update(CPopView *pactiveview, Real dt)  {      feelforce();      if(_usefixedlifetime && _age > _fixedlifetime)          dieOfOldAge(); /* I don't call die() because I like to use              die for when a critter dies of unnatural causes, like              getting shot. It's more likely that I override die()              to do something dramatic than that I override              dieOfOldAge(). */  } 

The pactiveview arguments aren't used by any of our critters in the standard Pop Framework files; they're in place simply for possible use. Some students have in fact written two-dimensional games in which the update feeds the pactiveview into the method COLORREF cCritter::sniff(const cVector &snifflocation , CPopView *pactiveview) . The purpose of this is to let a critter adjust its behavior according to the colors of the nearby pixels as drawn in the active view; we've designed car-racing games in this way, for instance, by using the sniff method to let a critter know when it had driven off the track.

As we already discussed in Chapter 7: Simulating Physics, the feelforce method sums up the forces acting on the critter and applies Newton's Law to compute the acceleration.

 void cCritter::feelforce()  {      cVector forcesum; //Default constructor (0,0)      for (int i=0; i<_forcearray.GetSize(); i++)          forcesum += _forcearray.GetAt(i)->force(this);      _acceleration = forcesum/mass(); /* From Newton's Law:          Force = Mass * Acceleration. */  } 

Recall that the base class cForce::force returns a zero vector, but we have 'physical' child classes cForceGravity , cForceDrag , and cForceVortex which return non-trivial values. We also have 'behavioral' force classes like cForceObjectSeek , cForceClassEvade , and cForceEvadeBullet

 void cCritter::feellistener(Real dt)  {      _plistener->listen(dt, this); /* We pass the pointer "this" to          the listener so that it can change the fields of this calling          cCritter as required. The caller critter's pgame() holds the          cController object that stores all of the keys and mouse          actions you need to process. */  } 

Taken together, the sequence of actions involving the update , feelforce , and feellistener methods can be summarized as follows .

  • Call update and, within update , call feelforce() .

  • Call feellistener(dt) and possibly add in some more acceleration.

  • Use the _acceleration in move(dt) .

The Move method

The cCritter::move method has a dt argument because we want the motion to adapt itself according to the speed of the processor running the program. A fast processor will pass very small dt to the move method, and we will want the critters to move only a slight amount with each update. A slow processor will pass larger dt timesteps to the move method, and in that case we need for the critters to move a larger amount with each update.

What we basically want from our move(dt) method is these two lines.

 _velocity += dt * _acceleration;  _position += dt*_velocity. 

But, as we mentioned in the last section, our move(dt) has to do a bit more.

  • Age the critter by dt seconds.

  • Add acceleration * dt to the velocity.

  • Clamp the velocity's speed against maxspeed.

  • Add velocity * dt to the position.

  • Wrap, bounce, or clamp the new position relative to the border.

  • Update the critter's normal and binormal to reflect the current state of motion.

  • Set the critter's outcode according to which border edge, if any, it hit.

In terms of the special cCritter field names , this can be put a bit more precisely as follows.

  • Increment the _age by dt seconds.

  • Add _acceleration * dt to the _velocity , and recalculate _speed from _velocity .

  • Clamp the _speed against _maxspeed , possibly redefining the _velocity .

  • Add _velocity*dt to the _position .

  • Wrap, bounce, or clamp the _position relative to the _movebox .

  • Update the critter's _tangent , _normal and _binormal to reflect the current state of motion.

  • Set the critter's _outcode according to which _movebox edge, if any, it hit.

As usual, for the fully complete and accurate version, look at the actual code in critter.cpp .

Although it isn't really necessary, we happen to have implemented our cCritter::move , and some other critter-moving methods such as clamp and moveTo , so that they return the outcode. But the real use of the outcode is as the internal cCritter field int _outcode .

Let's say a few words about the meaning of the outcode. The word 'outcode' comes from computer graphics. The outcode value of the critter is set to reflect the relationship between the border box of the world and the last position the critter moved to (prior to having this position clamped or wrapped).

In two dimensions the outcode would distinguish among nine positions relative to a rectangle: inside the rectangle, to its right, to its top right, to its top, and so on. The idea is that we imagine extending the edges of the rectangle into infinite lines, and these lines cut space into nine regions . This is shown in Figure 8.5.

Figure 8.5. The nine outcode zones in two dimensions


Relative to a box in three dimensions, an outcode can distinguish among 27 possible regions: think of a 3 x 3 x 3 Rubik's cube of space regions built up around the central box. Rather than making up 27 different outcode names, it's more useful to OR together bitflags specifying a location's region relative to each axis.

The values we use for our outcodes are defined in realbox.h as follows. (These happen to be implemented as define values rather than static int constants.)

 #define BOX_INSIDE 0  #define BOX_LOX 1  #define BOX_HIX 2  #define BOX_LOY 4  #define BOX_HIY 8  #define BOX_LOZ 16  #define BOX_HIZ 32 

Thus in the plane, a critter located to the 'northwest' of a box would have an outcode of BOX_LOX BOX_HIY . And to perform some action dosomething() only if a critter had touched the lower edge of a box, we could put a line like this into an overridden version of the critter's update method.

 if(_outcode & BOX_LOX)      dosomething(); 

Indeed, looking back at our Space Invaders Exercise 3.10 in Chapter 3: The Pop Framework, recall that we suggested a condition of just this form for use in the critter's update to detect if the critter had touched the bottom edge of the screen during its last move .

The Draw method

In order to draw a critter we need to have a pointer to a cGraphics object. Also we may have some drawflags to indicate some special aspects of how we want to draw the critter; generally , for instance, we draw a circle around the player critter, and we draw our critters as 'hollow' if they have been recently damaged.

So the cCritter::draw code looks essentially like the following, though the actual code you'll find in critter.cpp is a little more complicated.

 void cCritter::draw(cGraphics *pgraphics, int drawflags)  {      if (recentlyDamaged())          drawflags = CPopView::DF_WIREFRAME      pgraphics->pushMatrix();      pgraphics->multMatrix(_attitude);      _psprite->draw(pgraphics, drawflags);      pgraphics->popMatrix();  } 

The draw code is an example of the Template Method pattern. We always want to multiply in the attitude matrix, doing the necessary set-up and clean-up to the pgraphics matrix stack. The part of the call that we override is separated out into the virtual cSprite::draw method.

We almost don't need to make cCritter::draw a virtual function, but the cCritterArmed::draw does override and extend the cCritter::draw to draw a short line segment to represent the gun.

The Animate method

Let's say a bit more about the critter's _attitude matrix. This specifies how we are to orient the sprite image that represents the critter. The place where the critter updates the _attitude is in its cCritter::updateAttitude call, which is called by the cCritter::animate .

 void cCritter::animate(Real dt)  {      updateAttitude(dt);      _psprite->animate(dt, this);  } 

If _attitudetomotionlock field is TRUE , the updateAttitude method matches the _attitude matrix to the motion matrix given by the tangent, normal, binormal, and position vectors. This is a good default behavior that makes the critters look lively. If _attitudetomotionlock is FALSE , we allow for the possibility that the critter is spinning.

 void cCritter::updateAttitude(Real dt)  {      _attitude.setLastColumn(_position); //always update position.      if (_attitudetomotionlock)          copyMotionMatrixToAttitudeMatrix();      else //_attitudetomotionlock is FALSE          rotateAttitude(dt*_spin);  } 

As we'll see in Chapter 9: Sprites, the reason we pass this to the cSprite::animate() call is that the sprite may want to change itself depending on the direction or the health of its owner critter.

Randomizing and mutation methods

As well as the randomizePosition and randomizeVelocity methods, we can also change a critter by calling a cCritter::mutate(int mutationflags, Real mutationstrength) method.

The way this works is that we can feed in various combinations of the static MF_ mutation flags, some of which are defined in critter.h , some in sprite.h and some in spritepolygon.h . The cCritter::mutate method changes a few critter values and then makes a call to the cSprite::mutate method. For randomizing purposes, these methods use the singleton cRandomizer::pinstance() object.

The Die and Damage methods

The default cCritter::die method is implemented in-line simply as virtual void die(){delete_me();} . This tells the owner game to delete the pointer and remove it from the cBiota array when the current round of critter updates is done. The reason we wait a bit is that it can cause trouble if you start adding and deleting critters to an array that you're in the process of updating.

Some critters override the die or the damage method to make a noise with a call of the form playSound("Bonk") . Both the cCritter and cGame classes have a playSound method. The string you feed into a playSound call needs to be defined in quotes as the ID of the relevant resource, for instance as 'BONK'. The names of resources are not case sensitive.

The standard cCritter::damage(int hitstrength) code reduces the _health by hitstrength , and if the _health is less than or equal to zero, the critter makes a call to die() .

The Collide method

Collision is a tricky matter, and we'll give a more detailed discussion of it in Chapter 11: Collisions. Critters collide in pairs.

Each critter has the virtual BOOL cCritter::collide(cCritter *pother) method whose default behavior is to perform an elastic collision between the caller critter and the pother critter, changing their positions and velocities in a manner that would be physically natural if the critters were spheres.

We can override a critter's collide method to include a reaction to the critter that it's colliding with, possibly killing one of the critters, adding a score to one, or the like.

Sometimes we may have two cCritter *pcritteri, *pcritterj that belong to cCritter child classes that have different overrides of the collide method. In this case it makes a difference whether you call pcritteri->collide(pcritterj) or pcritteri->collide(pcritterj) . We generally don't want to call both collide methods as (a) this would waste computational time and (b) the collide methods are designed to have a symmetric effect on the critters, so it would be physically incorrect to call collide twice for one particular collision. As we discuss in Chapter 11: Collisions, we give the critters an int _collidepriority field and a virtual int cCritter::collidesWith(cCritter *pcritterother) method to resolve the question of which critter gets to control a given collision.

Software Engineering and Computer Games
Software Engineering and Computer Games
Year: 2002
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net