Storing Worm Information


The Worm class stores coordinate information about the worm in a circular buffer. It includes testing methods for checking if the player has clicked near the worm's head or body and includes methods for moving and drawing the worm.

The issues which make things more complicated include:

  • Having the worm grow in length up to a maximum size

  • Regulating the worm's movements to be semi-random so that it mostly moves in a forward direction

  • Getting the worm to go around obstacles in its path

Growing a Worm

The worm is grown by storing a series of Point objects in a cells[] array. Each point represents the location of one of the black circles of the worm's body (and the red circle for its head). As the worm grows, more points are added to the array until it is full; the worm's maximum extent is equivalent to the array's size.

Movement of the full-size worm is achieved by creating a new head circle at its front and removing the tail circle (if necessary). This removal frees up a space in the cells[] array where the point for the new head can be stored.

The growing and movement phases are illustrated by Figure 3-6, which shows how the cells[] array is gradually filled and then reused. The two indices, headPosn and tailPosn, make it simple to modify the head and tail of the worm, and nPoints records the length of the worm.

Figure 3-6. Worm data structures during growth and movement


The numbered black dots (and red dot) represent the Point objects which store the (x, y) coordinates of the worm's parts. The numbers are included in the figure to indicate the order in which the array is filled and over-written; they are not part of the actual data structure, which is defined like so:

     private static final int MAXPOINTS = 40;     private Point cells[];     private int nPoints;     private int tailPosn, headPosn;   // tail and head of buffer     // additional variables already defined     cells = new Point[MAXPOINTS];   // initialise buffer     nPoints = 0;     headPosn = -1;  tailPosn = -1;

The other important Worm data structure is its current bearing, which can be in one of eight predefined compass directions: N = north, NE = northeast, and so on, around to NW = northwest. The choices are shown in Figure 3-7.

Figure 3-7. Compass directions and corresponding integers


Each compass direction is represented by an integer, which labels the bearings in clockwise order. The relevant constants and variable are shown here:

     // compass direction/bearing constants     private static final int NUM_DIRS = 8;     private static final int N = 0;  // north, etc going clockwise     private static final int NE = 1;     private static final int E = 2;     private static final int SE = 3;     private static final int S = 4;     private static final int SW = 5;     private static final int W = 6;     private static final int NW = 7;     private int currCompass;  // the current compass dir/bearing

Limiting the possible directions that a worm can move allows the movement steps to be predefined. This reduces the computation at run time, speeding up the worm.

When a new head is made for the worm, it is positioned in one of the eight compass directions, offset by one "unit" from the current head. This is illustrated in Figure 3-8.

Figure 3-8. Offsets from the current head position


The offsets are defined as Point2D.Double objects (a kind of Point class that can hold doubles). They are stored in an incrs[] array, created at Worm construction time:

     Point2D.Double incrs[];     incrs = new Point2D.Double[NUM_DIRS];     incrs[N] = new Point2D.Double(0.0, -1.0);     incrs[NE] = new Point2D.Double(0.7, -0.7);     incrs[E] = new Point2D.Double(1.0, 0.0);     incrs[SE] = new Point2D.Double(0.7, 0.7);     incrs[S] = new Point2D.Double(0.0, 1.0);     incrs[SW] = new Point2D.Double(-0.7, 0.7);     incrs[W] = new Point2D.Double(-1.0, 0.0);     incrs[NW] = new Point2D.Double(-0.7, -0.7);

Calculating a New Head Point

nextPoint( ) employs the index position in cells[] of the current head (called prevPosn) and the chosen bearing (e.g., N, SE) to calculate a Point for the new head.

The method is complicated by the need to deal with wraparound positioning top to bottom and left to right. For example, if the new head is placed off the top of the canvas, it should be repositioned to just above the bottom.

     private Point nextPoint(int prevPosn, int bearing)     {       // get the increment for the compass bearing       Point2D.Double incr = incrs[bearing];       int newX = cells[prevPosn].x + (int)(DOTSIZE * incr.x);       int newY = cells[prevPosn].y + (int)(DOTSIZE * incr.y);       // modify newX/newY if < 0, or > pWidth/pHeight; use wraparound       if (newX+DOTSIZE < 0)     // is circle off left edge of canvas?         newX = newX + pWidth;       else  if (newX > pWidth)  // is circle off right edge of canvas?         newX = newX - pWidth;       if (newY+DOTSIZE < 0)     // is circle off top of canvas?         newY = newY + pHeight;       else  if (newY > pHeight) // is circle off bottom of canvas?         newY = newY - pHeight;       return new Point(newX,newY);     }  // end of nextPoint( )

The code uses the constant DOTSIZE (12), which is the pixel length and height of the circle representing a part of the worm. The new coordinate (newX, newY) is obtained by looking up the offset in incr[] for the given bearing and adding it to the current head position.

Each circle is defined by its (x, y) coordinate and its DOTSIZE length. The (x, y) value is not the center of the circle but is its top-left corner, as used in drawing operations such as fillOval( ) (see Figure 3-9).

Figure 3-9. The coordinates of a worm circle


This explains the wraparound calculations which check if the circle is positioned off the left, right, top or bottom edges of the canvas. The panel dimensions, pWidth and pHeight, are passed to the Worm object by WormPanel at construction time.

Choosing a Bearing

The compass bearing used in nextPoint( ) comes from varyBearing( ):

     int newBearing = varyBearing( );     Point newPt = nextPoint(prevPosn, newBearing);

varyBearing( ) is defined as:

     private int varyBearing( )     // vary the compass bearing semi-randomly     { int newOffset =            probsForOffset[ (int)( Math.random( )*NUM_PROBS )];       return calcBearing(newOffset);     }

The probsForOffset[] array is randomly accessed and returns a new offset:

     int[] probsForOffset = new int[NUM_PROBS];     probsForOffset[0] = 0;  probsForOffset[1] = 0;     probsForOffset[2] = 0;  probsForOffset[3] = 1;     probsForOffset[4] = 1;  probsForOffset[5] = 2;     probsForOffset[6] = -1;  probsForOffset[7] = -1;     probsForOffset[8] = -2;

The distribution of values in the array means that the new offset is most likely to be 0, which keeps the worm moving in the same direction. Less likely is 1 or -1, which causes the worm to turn slightly left or right. The least likely is 2 or -2, which triggers a larger turn.

calcBearing( ) adds the offset to the old compass bearing (stored in currCompass), modulo the compass setting ranges North to North West (0 to 7):

     private int calcBearing(int offset)     // Use the offset to calculate a new compass bearing based     // on the current compass direction.     {       int turn = currCompass + offset;       // ensure that turn is between N to NW (0 to 7)       if (turn >= NUM_DIRS)         turn = turn - NUM_DIRS;       else if (turn < 0)         turn = NUM_DIRS + turn;       return turn;     }  // end of calcBearing( )

Dealing with Obstacles

newHead( ) generates a new head using varyBearing( ) and nextPoint( ), and it updates the cell[] array and compass setting:

     private void newHead(int prevPosn) // not finished yet     {       int newBearing = varyBearing( );       Point newPt = nextPoint(prevPosn, newBearing );       // what about obstacles?       // code to deal with obstacles       cells[headPosn] = newPt;     // new head position       currCompass = newBearing;    // new compass direction     }

Unfortunately, this code is insufficient for dealing with obstacles: what will happen when the new head is placed at the same spot as an obstacle?

The new point must be tested against the obstacles to ensure it isn't touching any of them. If it is touching, then a new compass bearing and point must be generated. I try three possible moves: turn left by 90 degrees, turn right by 90 degrees and, failing those, turn around and have the worm go back the way it came.

These moves are defined as offsets in the fixedOffs[] array in newHead( ):

     private void newHead(int prevPosn)     {       int fixedOffs[] = {-2, 2, -4};  // offsets to avoid an obstacle       int newBearing = varyBearing( );       Point newPt = nextPoint(prevPosn, newBearing );       if (obs.hits(newPt, DOTSIZE)) {         for (int i=0; i < fixedOffs.length; i++) {           newBearing = calcBearing(fixedOffs[i]);           newPt = nextPoint(prevPosn, newBearing);           if (!obs.hits(newPt, DOTSIZE))             break;     // one of the fixed offsets will work         }       }       cells[headPosn] = newPt;     // new head position       currCompass = newBearing;    // new compass direction     }  // end of newHead( )

Key to this strategy is the assumption that the worm can always turn around. This is possible since the player cannot easily add obstacles behind the worm because the worm's body prevents the user from placing a box on the floor.

Moving the Worm

The public method move( ) initiates the worm's movement, utilizing newHead( ) to obtain a new head position and compass bearing.

The cells[] array, tailPosn and headPosn indices, and the number of points in cells[] are updated in slightly different ways depending on the current stage in the worm's development. These are the three stages:

  1. When the worm is first created

  2. When the worm is growing, but the cells[] array is not full

  3. When the cells[] array is full, so the addition of a new head must be balanced by the removal of a tail circle:

     public void move( )     {       int prevPosn = headPosn;                   // save old head posn while creating new one       headPosn = (headPosn + 1) % MAXPOINTS;       if (nPoints == 0) {   // empty array at start         tailPosn = headPosn;         currCompass = (int)( Math.random( )*NUM_DIRS );  // random dir.         cells[headPosn] = new Point(pWidth/2, pHeight/2); //center pt         nPoints++;       }       else if (nPoints == MAXPOINTS) {    // array is full         tailPosn = (tailPosn + 1) % MAXPOINTS;    // forget last tail         newHead(prevPosn);       }       else {     // still room in cells[]         newHead(prevPosn);         nPoints++;       }     }  // end of move( )

Drawing the Worm

WormPanel calls Worm's draw( ) method to render the worm into the graphics context g. The rendering starts with the point in cell[tailPosn] and moves through the array until cell[headPosn] is reached. The iteration from the tailPosn position to headPosn may involve jumping from the end of the array back to the start:

     public void draw(Graphics g)     // draw a black worm with a red head     {       if (nPoints > 0) {         g.setColor(Color.black);         int i = tailPosn;         while (i != headPosn) {           g.fillOval(cells[i].x, cells[i].y, DOTSIZE, DOTSIZE);           i = (i+1) % MAXPOINTS;         }         g.setColor(Color.red);         g.fillOval( cells[headPosn].x, cells[headPosn].y, DOTSIZE, DOTSIZE);       }     }  // end of draw( )

Testing the Worm

nearHead( ) and touchedAt( ) are Boolean methods used by WormPanel. nearHead( ) decides if a given (x, y) coordinate is near the worm's head, and touchedAt( ) examines its body:

     public boolean nearHead(int x, int y)     // is (x,y) near the worm's head?     { if (nPoints > 0) {         if( (Math.abs( cells[headPosn].x + RADIUS - x) <= DOTSIZE) &&             (Math.abs( cells[headPosn].y + RADIUS - y) <= DOTSIZE) )           return true;       }       return false;     } // end of nearHead( )     public boolean touchedAt(int x, int y)     // is (x,y) near any part of the worm's body?     {       int i = tailPosn;       while (i != headPosn) {         if( (Math.abs( cells[i].x + RADIUS - x) <= RADIUS) &&             (Math.abs( cells[i].y + RADIUS - y) <= RADIUS) )           return true;         i = (i+1) % MAXPOINTS;       }       return false;     }  // end of touchedAt( )

The RADIUS constant is half the DOTSIZE value. The test in nearHead( ) allows the (x, y) coordinate to be within two radii of the center of the worm's head; any less makes hitting the head almost impossible at 80+ FPS. touchedAt( ) only checks for an intersection within a single radius of the center.

The addition of RADIUS to the (x, y) coordinate in cells[] offsets it from the top-left corner of the circle (see Figure 3-9) to its center.



Killer Game Programming in Java
Killer Game Programming in Java
ISBN: 0596007302
EAN: 2147483647
Year: 2006
Pages: 340

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