Exercise 18.1: Time the flow in Dambuilder

Let's go back to the original inspiration for Dambuilder. It has this name in memory of the childhood activity of making dams in little streams. The goal in building dams like this is to make the water move through the system as slowly as possible, but without the water getting stuck in any one place (and overflowing one of the dams).

You can use the cursor tools to make new walls and move them. Click with the Equals cursor to copy, prick with the Pin cursor to delete, drag with the Hand cursor to move. This is how you 'play the game'.

How do you tell how well you're doing in Dambuilder? We need to have a score that calculates the average time that it takes a critter to fall from the top to the bottom of the screen. We reset the critters' ages when they wrap bottom to top.

And we need some kind of penalty if a critter gets stuck for too long. Perhaps if a critter's age gets larger than some MUSTBESTUCK time value, then the critter explodes and destroys everything near it, including the dam that it's stuck behind. So your goal is to get the critters moving along as slowly as possible just short of being stuck and destroying the dams.

Exercise 18.2: Rotate the walls

Add a tool that lets you rotate the walls of the dams. The cCritterWall might need some special override of the turn code for this. An easy way to call this method would be to give the cCritterWall a scooter-style listener that turns it provided that the wall is currently the pfocus() of its owner.

If you feel more ambitious you could make a new cursor tool that uses the left click to rotate left and the right click to rotate right.

Exercise 18.3: Make a maze game

If you remove the gravity forces from the critters in Dambuilder and start with a cGraphicsOpenGL preference and set the critter viewer to ride the player, you get a fairly effective-looking first-person shooter game. But rather than changing Dambuilder, you might simply add walls to the GameStub game, which is already set up with enemies and health-packs. Note that this exercise is basically similar to Exercises 14.5 and 14.7 combined.

Exercise 18.4: Make a Pinball game

The hardest thing about pinball is making flippers. You would probably have cCritterFlipper be a child of cCritterWall. See if you can make a cCritterFlipper and put it into the Dambuilder game. You will also need to write a listener that uses the Left Arrow and Right Arrow to rotate the flipper around one of its ends.

Exercise 18.5: Make a Pachinko game

First get Pinball working, then find out what the popular Japanese arcade game Pachinko looks like and emulate that. Quite briefly, Pachinko is like a pinball game in which you have dozens of balls active at once. But there's more to it than that. Like pinball, many Pachinko machines are quite beautiful.

Exercise 18.6: PacMan

We all have a pretty clear idea of what PacMan looks like. Let your player be a cCritterPacman with a standard cListenerArrow. You can use cCritterWall objects.

Students have indeed written this game using the Pop Framework, and one issue is that in PacMan we have 100 or so cCrittterPowerpellet objects which are little yellow dots lining the maze paths. If you aren't careful, having so many critters can drastically slow down your program's execution speed. Make sure to set the _fixedflag to TRUE for these critters, and make sure that the various collision-controlling parameters are set so that the game's cCollider will ignore all Powerpellet collisions except those featuring a cCritterPowerpellet and a cCritterPacman. And in these collisions, have the cCritterPowerpellet get eaten: calling delete_me and adding some score to the cCritterPacman.

Another thing to keep in mind if you're worried about speed is that the Release build will run very noticeably faster than the Debug build. If the speed were still to be unacceptable, you could use a different approach for eating power pellets. Rather than having the PacMan check its distance from each and every power pellet at every update, you could use a sniff method and make the power pellets a distinctive color.

The biggest difficulty students find in making PacMan style games is in having the player's enemies be good at chasing the player or, if they are to be victims, be good at running away from the player. We discuss this in the next exercise.

Exercise 18.7: Smarter enemies for maze games

Suppose you write an adventure game in which the enemy critters use a cForceObjectSeek to run towards the player. Suppose also that your game has cCritterWall walls in it ? think of something like a PacMan game in which the ghosts chase the player (or, if the player is powered-up, run away from the player).

If you use a simple cForceObjectSeek, the enemies will often get stuck pushing against a wall they can't get through. The easiest way out is simply to provide more enemies and expect that some of them will manage to be a threat.

But really you'd want to create a 'smarter' kind of seeking force. Let's discuss three increasingly sophisticated ways in which we can try and improve the situation.

  1. A simple, but somewhat effective thing you could do instead is to create a cForceObjectSeekImpatient. The idea is that this is a seek force that will sometimes turn itself off in the hope that the enemy might then happen to bounce into a better location. We'll think of the default, non-seek-force motion as a 'cruise' motion. By a cruise motion we mean a force-free motion in which the critter simply moves along in straight lines bouncing off the walls.

    Give the class these fields and initialize as suggested:

    int _frustration;//Start at 0 
    int _maxfrustration; 
        //Try 1 to start with and then try making it larger. 
    Real _oldsatisfaction; 
        //Start at a large negative number like -1000000.0 
    Real _cruisetime; //Some time in seconds, like 3.0 
    BOOL _cruising; //Start FALSE 
    Real _resumeage //A scratch-paper field we can initialize to 0.0 

    Now use a cForceObjectSeekImpatient force coded something like this.

    cVector cForceObjectSeekImpatient::force(cCritter *pcritter) 
        if (_cruising) 
                //Keep moving fast 
            if (pcritter->age() >_resumeage) 
                _cruising = FALSE; 
                return cVector::ZEROVECTOR; 
        Real_newsatisfaction = -pcritter->distanceTo(_pnode); /* Use 
            minus so the bigger the distance to the target, the lower 
            your satisfaction is. */ 
        if (newsatisfaction <= oldsatisfaction) 
            _frustration ++; 
        if (frustration <= 0) 
            _frustration = 0; 
        if (frustration > _maxfrustration) 
            _cruising = TRUE; 
            _resumeage = pcritter->age() + cruisetime; 
            _frustration = 0; 
        return cForceObjectSeek::force(pcritter); 
  2. For a more sophisticated solution, you would want to use a cMaze class that inherits from CArray<cCritterWall *, cCritterWall*>. As we add the cCritterWall objects to our world, we also put them into the cMaze.

    It will be useful to give the cMaze class a BOOL blocks(const cVector& start, const cVector& end) method, which walks through the array of member cCritterWall, checks the value of cCritterWall::blocks(start, end) for each wall, and returns TRUE if any of the member walls blocks the path.

    In addition, we'd want to give the cMaze a CArray<cVector, cVector&>_waypoint member, and as we added in the cCritterWall objects to the maze, we'd want to fill _waypoint with points corresponding to significant points in the maze's passageways. In particular, you'd want to have a waypoint at each corner, and before and after each gap or doorway in the maze.

    Once you have the cMaze in place, you could create a cForceObjectSeekImpatientMaze which has a cMaze *pmaze member. When 'frustrated' the force could direct the caller critter to proceed towards the nearest waypoint of the maze. We can measure frustration as before, or simply become frustrated right away if the cMaze blocks the path from the enemy position to the player position.

  3. The truly correct thing to do would be to use a cMaze as in (b), but to have a cForceSolveMaze which would determine the proper sequence of waypoints to follow in order to get to the critter being sought. This involves considering the tree of all non-repeating arrays of waypoints one might visit, starting with the nearest waypoint. We will only consider those waypoint sequences in which the maze doesn't block the path between any two successive waypoints. And our goal will be to reach a waypoint W such that the maze doesn't block the path from W to the target critter. If you know a little about AI, you would probably want to use a so-called A* search strategy; otherwise a simple breadth-first search will work well enough, provide your maze isn't too big.

    To keep the speed of the program up it might suffice to only recompute the current path of cForceSolveMaze after every ten or twenty updates.

Exercise 18.8: A labyrinth game

There's a popular wooden maze game in which you move a ball by manipulating two knobs on the sides of the box. The knobs tilt the top surface of the game east/west or north/south. On the board is a ball-bearing, some little walls, and about 50 holes. There's a path drawn on the top, and your goal is to manipulate the knobs so that the ball rolls along the path from beginning to end, missing all 50 of the holes.

To implement this as a cGameRollingMaze, we'll have a cCritterRollingball as our player. We can use cCritterWall objects for the pieces of the maze. We should have some cCritterHole objects for the balls to fall into: make them fixed critters with perhaps a black cBubble for their sprite, and override their collide method so that when they collide with a pcritter they (a) add an acceleration to pcritter which points towards their center if the pcritter isn't fully inside (the way a ball speeds up towards a hole when it's partly over one edge) and (b) the call pcritter->delete_me() if the pcritter is inside. Well, actually doing a delete_me on a game's _pplayer has no effect (because all hell breaks loose if you have NULL player, and cBiota has a 'foolproofing' feature of ignoring delete requests on active players). But we'd like to keep the delete_me for the cCritterHole as we might sometime want to reuse the holes. What we really want to happen when the player falls in the hole is that the player goes back to the starting position. So you should override your cCritterRollingball::delete_me to call an overridden reset() which will indeed put the ball at the starting gate.

Whenever you have a lot of critters, you want to be careful not to try and compute unnecessary collisions as this will make the program run too slow. Regarding the many cCritterHole objects, the only kind of collisions we're interested in is between a hole and the player. And since we've overridden the cCritterHole::collide method for the special behavior, we want to call these collisions in the form phole->collide(pplayer). So set the relevant _collidepriority or collidesWith methods accordingly.

How about the knobs? Probably you can use a cListenerTipper listener that increments or decrements the acceleration in the x and y directions with the Left/Right Up/Down keys. You will want to put a good amount of friction on the ball's motions and/or give it a low _maxspeed so it doesn't get out of control.

How best to add all the cCritterHole and cCritterWall objects to the world? We might consider something like the cMaze object described in Exercise 18.7.b. Call it a cRollerBoardlayout. But, on second thought, maybe our class cRollerBoardlayout needn't hold critters as in 18.7b. Maybe it should just have geometrical information. It could store the coordinate information for the various walls and holes, and have a constructor like, perhaps, cRollerBoardlayout(int wallcount, Real wallthickness, Real[] wallenda, Real[] wal lendb, int holecount, Real holeradius, Real[] holecenter). And it could have an all-important putBoardInGame(cGame *pgame) method to create and add the desired cCritterHole and cCritterWall objects to pgame. So then our code could have some easily tweaked Real numbers WALLCOUNT, WALLTHICKNESS, HOLECOUNT, HOLERADIUS and three Real arrays WALLENDA, WALLENDB and HOLECENTER as statics or #define at the top of the cgamerollingmaze.cpp. And the cGameRollingMaze constructor would construct a temporary cRollerBoardlayout layout(WALLCOUNT, WALLTHICKNESS, WALLENDA, WALLENDB, HOLECOUNT, HOLERADIUS, HOLECENTER WALLENDS, HOLES)ü and then call layout.putBoardInGame(this).

Exercise 18.9: Slot Car Racer

Use the trail-sniffing technique to make a car-racing game. Have your race course be a bitmap with a track marked in pure white pixels, give your player a cListenerCar listener, and make a robot car to race with you.

You can count on the update with the sniff method to keep the robot on the track, but how do you keep the robot going in the right direction? Assuming your track is roughly a closed curve around the origin you might try giving the robot a reasonably strong cForceVortex to keep it going in generally the right direction.

Rather than using sniffing, you may find it works better to keep your player on the track simply by lining the track by cCritterWall. If you are careful not to use too many walls (use a few long ones rather than a lot of short ones), the speed will be okay.

Whether or not you use use sniffing, you will need a reliable method to make your robot driver opponent do a good job. What works the best is to use a cForceWaypoint as described in Exercise 7.2.

Once you get this working you may want to tweak the listener controls to make the driving experience better. It should be possible to have a world larger than you see on the screen, provided you set the track player option so that the player is always on a visible part of the screen.

Try implementing a two-player mode, and have the other player car have a listener that's controlled by some letter keys, such as VK_S and so on.

Exercise 18.10: Feeding

Make a game in which you can feed the critters. One approach would be to have a player who scatters new cCritterFood when you press, say the left mouse button. Basically the player could be like a cCritterArmed that shoots bullets that don't move. In fact cCritterFood could be a child of cCritterBullet. And maybe in your feeding process you are trying to lure the critters into a pen. Like catching chickens.

Exercise 18.11: Flocking

Make a cCritterBoid class and give its members a cForceClassFlock force which makes the 'boids' do the following.

  • Collision avoidance: avoid collisions with walls and nearby flockmates.

  • Velocity matching: attempt to match velocity with nearby flockmates.

  • Flock centering: attempt to stay close to nearby flockmates.

Now for a few words on each of these behaviors.

  • Collision avoidance: each boid keeps track of some optimal cruising distance that it would like to maintain between itself and its nearest flockmates. If a boid's nearest visible neighbor is at a distance less than this cruising distance, then the boid is in danger of colliding with its neighbor. The boid avoids the collision by slowing down if the too-near neighbor is in front of the boid, and by speeding up if the too-near neighbor is behind the boid.

    As well as trying not to get too close to the nearest neighbor boid, a boid also tries not to get too far from the nearest visible boid. That is, if you're a boid and the nearest visible neighbor boid is farther than the optimal cruise distance, you speed up if that boid's in front of you, and slow down if it's behind you.

    Note that these adjustments to cruising distance are done solely by changing the boids' speeds, rather than by changing their direction vectors. The phrases 'in front of' and 'behind' for boids are used to stand for computing the angle between a boid's direction and a given object and deciding if the angle is within a specified range.

  • Velocity matching: each boid tries to fly parallel to its nearest neighbor. This is done by adjusting the boid's direction vector to match the direction vector of its nearest neighbor. This does not change the boid's speed.

  • Flock centering: each boid tries to be surrounded by other boids on every side. This is done by having each boid compute the average position or centroid of the other boids, and try and move towards the centroid. To do this, a boid computes the unit vector that points towards the centroid, and then turns its own direction vector to match this unit vector. This does not change the boid's speed.

    Here's some pseudocode for the process.

    --------------------Boid Motion Algorithm--------------------
    Boid Motion: 
    // Avoid the walls 
        IF (The world is in walled mode and you are within 
            NearWallDistance pixels of a wall) 
            THEN (Add to Direction components of size VeerWeight 
                pointing away from the walls that are too near); 
    // Copy your neighbor. 
        Do Collision Avoidance and Velocity Matching with Nearest Boid; 
    // Head towards centroid of the Boid positions. 
        Direction = Direction + CenterWeight * ToCentroid; 

    To make the process work, we actually need to compute some kind of weighted average of the four actions: avoid wall, avoid hitting closest neighbor, match neighbor, head towards flock center.

    More information can be found off the page on the author's website dealing with his Artificial Life Lab program: www.mathcs.sjsu.edu/faculty/rucker/boppers.htm. And, above all, see Craig Reynolds's wonderful Java-based boids pages at http://www.red3d.com/cwr/boids/

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