Pascal Worked Example: Bouncing Ball: Iteration 3

From SwinBrain

In this iteration I plan to update the program so that the ball will bounce when it hits an edge.

Contents

Data Required

There is no additional data that is needed for this iteration. The ball is moving and collides with the boundary, we just need to add code to change directions.

Structure

In a similar way to GetNewLocation we can write a single function that tells us if the ball is At the Edge of the border. This will return true if the indicated location is within one move of the boundary. This gives us the following structure chart.

Cut 1

We can implement all of this feature in one cut. We will need to do the following:

  1. Add the AtEdge function
  2. Change main to use this function

The new or changed code is shown below.

//Indicates if the location is within one of the lowBoundary or highBoundary.
//
// @param location  The current location
// @param lowBoundary The low boundary value, we will check if the location 
//                    is one larger than this.
// @param highBoundary The high boundary value, we will check if the location 
//                     is one smaller than this.
// @returns true if the location is at the edge
function AtEdge(location, lowBoundary, highBoundary: Integer): Boolean;
begin
  if location + 1 = highBoundary then
  begin
    result := true;
  end
  else
  begin
    if location - 1 = lowBoundary then
    begin
      result := true;
    end
    else
    begin
      result := false;
    end;
  end;
end;
 
procedure Main();
var
  x, y, dx, dy: Integer;
begin
  CursorOff();
  
  x := BORDER_START_X + 1;
  y := BORDER_START_Y + 1;
  dx := 1;
  dy := 1;
  
  DrawBorder('Bouncing Ball');
  
  repeat    
    DrawAt(x, y, BALL_CHARACTER);
    Delay(100);
    DrawAt(x, y, ' ');
    x := GetNewLocation(x, dx, BORDER_START_X, BORDER_START_X + BORDER_WIDTH);
    if AtEdge(x, BORDER_START_X, BORDER_START_X + BORDER_WIDTH) then
    begin
      dx := -dx;
    end;
    
    y := GetNewLocation(y, dy, BORDER_START_Y, BORDER_START_Y + BORDER_HEIGHT);
    if AtEdge(y, BORDER_START_Y, BORDER_START_Y + BORDER_HEIGHT) then
    begin
      dy := -dy;
    end;
  until KeyPressed();
  
  CursorOn();
end;

Executing this we find that it runs as expected.

Refactoring

Looking at main it is starting to get a little large. It is time to refactor, adding a new routine for main to call. Also there is a simpler way to return the true/false value from the AtEdge function, we will have a look at that change as well. The new structrue chart is shown below, followed by the refactored source code.

//This is a game where a ball is placed within a designated
//area of the screen. The ball will have a specified velocity
//and will move around within the area. When the ball collides
//with the edge of the area it should change direction. 
//Striking the bottom/top of the area will reverse the y 
//movement, while striking the left/right will reverse the
//x movement.
program BouncingBall;
uses CRT;
 
const
  BORDER_START_X = 2;
  BORDER_START_Y = 2;
  BORDER_WIDTH = 80 - BORDER_START_X - 1; //-1 for right border
  BORDER_HEIGHT = 25 - BORDER_START_Y - 1;
  BORDER_CHARACTER = #178;
  BALL_CHARACTER = #2;
  
//Draws a specified character at a given XY location.
//
// @param X The x location to draw at
// @param Y The y location to draw at
// @param character The character to be drawn
// 
//Side Effects:
// * Draws to the screen
procedure DrawAt(x, y: Integer; character: Char);
begin
  GotoXY(x, y);
  Write(character);
end;
 
//Draws the border around the game area.
//
// @param title The title to be written above the border
//
//Side Effects:
// * Writes to the console
procedure DrawBorder(title: String);
var
  x, y: Integer;
begin
  GotoXY(BORDER_START_X, 1);
  Write(Title);
  
  //Draw the top + bottom borders
  for x := BORDER_START_X to BORDER_START_X + BORDER_WIDTH do
  begin
    //Top
    DrawAt(x, BORDER_START_Y, BORDER_CHARACTER);
    DrawAt(x, BORDER_START_Y + BORDER_HEIGHT, BORDER_CHARACTER);
  end;
  
  for y := BORDER_START_Y to BORDER_START_Y + BORDER_HEIGHT do
  begin
    DrawAt(BORDER_START_X, y, BORDER_CHARACTER);
    DrawAt(BORDER_START_X + BORDER_WIDTH, y, BORDER_CHARACTER);
  end;
end;
 
//Calculates the new location for the ball given the current
//location and amount to move. The low and high boundary values
//are used to ensure that the ball remains in the screen.
//
// @param originalLocation The current location
// @param change  The current movement amount
// @param lowBoundary  The new location cannot be at or below this value
// @param highBoundary  The new location cannot be at or above this value
// @returns The new location for the ball
function GetNewLocation(originalLocation, change, lowBoundary, highBoundary: Integer): Integer;
var
  newLocation: Integer;
begin
  newLocation := originalLocation + change;
  if (newLocation <= lowBoundary) or (newLocation >= highBoundary) then
  begin
    //movement goes off screen... dont move
    result := originalLocation;
  end
  else
  begin
    result := newLocation;
  end;
end;
 
//Indicates if the location is within one of the lowBoundary or highBoundary.
//
// @param location  The current location
// @param lowBoundary The low boundary value, we will check if the location 
//                    is one larger than this.
// @param highBoundary The high boundary value, we will check if the location 
//                     is one smaller than this.
// @returns true if the location is at the edge
function AtEdge(location, lowBoundary, highBoundary: Integer): Boolean;
begin
	result := (location + 1 = highBoundary) or (location - 1 = lowBoundary);
end;
 
//Draws and moves the ball around the screen until a key is pressed.
//
//Side Effects:
// * Draws to console (in called routines)
procedure DoBouncingBall();
var
  x, y, dx, dy: Integer;
begin
  x := BORDER_START_X + 1;
  y := BORDER_START_Y + 1;
  dx := 1;
  dy := 1;
  
  repeat    
    DrawAt(x, y, BALL_CHARACTER);
    Delay(100);
    DrawAt(x, y, ' ');
    
    x := GetNewLocation(x, dx, BORDER_START_X, BORDER_START_X + BORDER_WIDTH);
    if AtEdge(x, BORDER_START_X, BORDER_START_X + BORDER_WIDTH) then
    begin
      dx := -dx;
    end;
    
    y := GetNewLocation(y, dy, BORDER_START_Y, BORDER_START_Y + BORDER_HEIGHT);
    if AtEdge(y, BORDER_START_Y, BORDER_START_Y + BORDER_HEIGHT) then
    begin
      dy := -dy;
    end;
  until KeyPressed();
end;
 
//Draws the border and starts the ball bouncing.
//
//Side Effects:
// * Writes to console (in called routines)
procedure Main();
begin
  CursorOff();
  
  DrawBorder('Bouncing Ball');
  DoBouncingBall();
  
  CursorOn();
end;
 
begin
  Main();
end.

The change to AtEdge greatly simplifies the code. This returns true when (location + 1 = highBoundary) or (location - 1 = lowBoundary). This can be implemented using the nested if statements or by just using this boolean expression. Often if a procedure returns a boolean value we can replace a number of if statement by a carefully crafted boolean expression.

Summary

This iteration has completed the implementation of the game. The following iteration will add a few bells and whistles.

[edit]Bouncing Ball: Overview | Iteration 1 | Iteration 2 | Iteration 3 | Iteration 4
Sample Code » C | C++ | .NET | Pascal | Perl | T-Sql | XHTML | Python | others to come