12.2 The listeners

As we mentioned in Chapter 8: Critters, a critter uses the Strategy pattern to farm out the task of listening to its cListener *_plistener member with a call to feellistener.

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. Because a cListener takes a cGame * argument to its listen method, we can say that the cListener can 'navigate' to a cGame. The caller critter's pgame() holds the cController object that stores all of the keys and mouse actions you need to process.

We pass a dt argument to the plistener->feellistener because the mouse-based listener cListenerCursor needs to know the dt so as to appropriately set the critter's velocity to match the critter's motion from one cursor position to the next.

The feellistener method gets called inside the cGame::step. The successive calls to cGame::step generate calls to feellistener(), move(), update(), feellistener(), move(), update(), feellistener(), move(), and so on. In other words, after startup, the process for an individual critter is this.

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

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

  • Use the _acceleration in move().

Now let's start to look at what the different kinds of listeners do in their listen methods. To begin with, here's a class diagram of some of our listeners (Figure 12.2).

Figure 12.2. The cListener class diagram


The listen(Real dt, cCritter *pcritter) method checks the current state of the keys as indicated by the pcritter->pgame()->pcontroller(). The cListenerArrow::listen code looks like the following.

void cListenerArrow::listen(Real dt, cCritter *pcritter) 
    cController *pcontroller = pcritter->pgame()->pcontroller(); 
        /* Note that since I set the velocity to 0.0 when I'm not 
        pressing an arrow key, this means that acceleration forces 
        don't get to have accumulating effects on a critter with a 
        cListenerScooter listener. So rather than having some very 
        half-way kinds of acceleration effects, I go ahead and set 
        acceleration to 0.0 in here. */ 
    if (!pcontroller->keyonplain(VK_LEFT) && 
        !pcontroller->keyonplain(VK_RIGHT) && 
        !pcontroller->keyonplain(VK_DOWN) && 
        !pcontroller->keyonplain(VK_UP) && 
        !pcontroller->keyonplain(VK_PAGEDOWN) && 
        /* If you get here, you've pressed an arrow key. First match 
            the velocity to the arrow key direction, and then match 
            the attitude. */ 
    if (pcontroller->keyonplain(VK_LEFT)) 
        pcritter->setVelocity(- pcritter->maxspeed() * 
    if (pcontroller->keyonplain(VK_RIGHT)) 
        pcritter->setVelocity(pcritter->maxspeed() * cVector::XAXIS); 
    if (pcontroller->keyonplain(VK_DOWN)) 
        pcritter->setVelocity(-pcritter->maxspeed() * 
    if (pcontroller->keyonplain(VK_UP)) 
        pcritter->setVelocity(pcritter->maxspeed() * cVector::YAXIS); 
    if (pcontroller->keyonplain(VK_PAGEDOWN) && 
        pcritter->setVelocity(-pcritter->maxspeed() * 
    if (pcontroller->keyonplain(VK_PAGEUP)&& pcritter->in3DWorld()) 
        pcritter->setVelocity(pcritter->maxspeed() * cVector::ZAXIS); 
        //Now match the attitude to the motion, if locked. 
    if (pcritter->attitude to motion lock ()) 
        /* If pcritter is cCritterArmed*, its 
            listen does more. 

There are a couple of things to point out. The cListenerArrow::listen has its effects on the critter by setting the critter's velocity. Since I'm directly setting the velocity at each game step, the acceleration will not be able to have any significant effect on the velocity; a line of the form _velocity += dt*_acceleration will have a negligible effect since we keep resetting the velocity in each call of the cListenerArrow::listen. So we have this listener set the acceleration to the zero vector.

Another thing to notice is that we use the cController::keyonplain accessors to see which keys are pressed. This is so that we can use Ctrl+Arrow keys or Ctrl+Shift+Arrow keys for other purposes, such as moving the viewpoint.

A third thing to observe is that the cListenerArrow::listen will set the listening critter's attitude to match the current motion of the critter, if locked. This gives the expected effect of having the critter face left when you press the Left Arrow, and so on.

A final thing to notice is that cListenerArrow::listen is designed to work for critters in 3D worlds as well as for critters in 2D worlds. But we are careful not to impart 3D motion to a critter unless it satisfies the is3D() condition. This condition checks if the critter's _movebox has a non-zero z size.

The cListenerScooter also has an effect of directly setting the magnitude of a critter's velocity, so here we again set the acceleration to the zero vector. cListenerScooter changes the critter's motion vector in one of two ways; by changing the magnitude velocity, or by rotating the critter's motion vectors in various ways.

In order to fully describe the possible rotations in three dimensions, we need to think in terms of the critter as having a trihedron of three perpendicular unit vectors called the tangent, the normal, and the binormal. These three vectors make up the first three columns of the critter's 'motion matrix.' This is shown in Figure 12.3.

Figure 12.3. The trihedron of a critter


We can summarize the effect of cListenerScooter::listen as follows. In testing this, be aware that in the Pop program, the cListenerScooter is chosen with the Player | Scooter Controls selection. 'Scooter' makes sense as a name for this listener because, as with a scooter that only rolls while you kick it, cListenerScooter only moves a player as long as a key is held down.

  • The Up key sets the critter's velocity to its maxspeed times its current tangent direction.

  • The Down key sets the critter's velocity to the opposite, that is, the maxspeed times the negative of the critter's tangent direction. In this case we do not set the critter's attitude to match its motion, that is, we leave its tangent pointing in the same direction as before and let the critter be moving 'in reverse.'

  • The Left and Right Arrow keys 'yaw' the critter by rotating its tangent around the z-axis or, in 3D, around the critter's binormal.

  • In 3D, the Pageup and Pagedown keys 'pitch' the critter by rotating its tangent around its normal.

  • In 3D, the Home and End keys 'roll' the critter by rotating its normal around its tangent.

  • When we rotate a critter we do update its visible attitude to match the new orientation of the motion matrix.

In order to make the game more responsive, it's better to have two turnspeeds for your player, a fast turnspeed for whirling around to shoot something that's sneaking up on you, and a slow turnspeed for accurately aiming your fire so as to hit a small or distant object. We choose the correct rotation speed by using a helper turnspeed method inside the cListenerScooter::listen method. Our cListener::turnspeed function looks at the cController::keyage of a key to determine how large a rotation to return.

The cListenerSpaceship and the cListenerCar do not change the critter's speed directly. Instead they add or subtract from the critter's acceleration. The difference is that the cListenerSpaceship adds an acceleration whose direction is determined by the critter's current visual attitude, and the cListenerCar adds an acceleration whose direction is determined by the critter's current motion.

The cListenerSpaceship and the cListenerCar rotate the critter in the same way as the cListenerScooter controls. The difference is that the cListenerSpaceship rotates the critter's attitude, while the cListenerCar rotates the critter's motion.

Both of these listeners are compatible with having forces act upon the critter.

The cListenerCursor moves the critter with the mouse. This is done by setting the critter's acceleration to the zero vector and setting the critter's velocity to be whatever velocity is necessary to move the critter to the pcritter->pgame()->cursorpos() in the allotted dt time slice that is fed into the cListenerCursor::listen(Real dt, cCritter *pcritter) call. If you move the mouse rapidly this means that the critter will in fact move at an extremely high velocity; when a critter has a cListenerCursor we disable the customary condition that limits a critter to moving less rapidly than the value of its maxspeed().

The reason we choose to have the cListenerCursor move the critter by changing its velocity rather than by simply calling a direct moveTo is that we want to be able to 'hit' things with a critter that we move with the mouse, and in order for a critter to collide properly with something, we need for its velocity to match its perceived motion.

Code for these listeners can be found in listener.cpp.

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