Modeling Poolballs

With our class specification files in place, let’s see if we can design the implementation code.

How do Poolballs Behave?

Here is Newton’s First Law, which tells us how a ball will act:

An object at rest stays at rest, and an object in motion stays in motion with the same speed and direction, unless acted on by an external force.

Hmmm, there are several terms there that we can use to guide us in designing this class, and in modeling how the poolballs behave!

Poolballs, by themselves, only move along in a constant direction determined by some initial force (like the cue stick might provide!) For now, the ball will simply slide along until something happens to make it change direction. The most obvious thing that will happen is that the ball will hit the edge of the pool table. The poolball does not know where the edge of the table is, since it is not really the ball’s concern. Instead, the pool table has a sense of the wall locations, and we can make it behave in a way that monitors all balls moving on the table. Furthermore, the table can tell when two balls get close enough that they might run into each other, requiring that we calculate new directions and velocities for the balls that collide. Sounds like a lot of work!. As we will see, with good OOP design, it ends up being fairly simple (especially if someone gives you the code to handle collisions!)

Modeling the Poolball

As we saw previously, we start off by setting up the basic attributes each poolball must have, and we can create tests to confirm that this part of our code is working. This is exactly like the work we did for the clock class earlier, so I will simply show the initial code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#ifndef POOLBALL_H
#define POOLBALL_H

#include "Graphics.h"

class Poolball {
    private:
        int xPos;                   // current X position on screen
        int yPos;                   // current Y position on screen
        int radius;                 // size of ball n pixels
        ColorName color;            // color of this ball

        int xSpeed;                 // speed in X direction
        int ySpeed;                 // speed in the Y direction

        void check_collisions(void);    // see if we hit a wall

    public:
        // constructors
        
        Poolball(void);             // anonymous constructor
        Poolball(int xPos, int ypos, int radius, ColorName color);

        //mutator methods
        void setX(int pos);         // set current X position on screen
        void setY(int pos);         // set current Y position on screen
        void setRadius(int size);   // set ball radius
        void setBallColor(ColorName color); // set current ball color

        void draw(void);
        void move(void);

        // accessor methods
        int getX(void);             // return current ball X position
        int getY(void);             // return current ball Y position
        int getRadius(void);        // return current ball radius
        ColorName getColor(void);   // return current ball color
};

#endif

And, here is the implementation file that we can use:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include "Poolball.h"
#include <iostream>
using namespace std;


// constructors
Poolball::Poolball(void) {
    this->xPos = 50;
    this->yPos = 50;
    this->radius = 25;
    this->xSpeed = 2;
    this->ySpeed = 2;
    this->color = BLUE;
}

Poolball::Poolball(int xPos, int yPos, int radius, ColorName color) {
    this->xPos = xPos;
    this->yPos = yPos;
    this->radius = radius;
    this->color = color;
    this->xSpeed = 2;
    this->ySpeed = 2;
}

// mutator methods
void Poolball::setX(int pos) {
    xPos = pos;
}

void Poolball::setY(int pos) {
    yPos = pos;
}

void Poolball::setRadius(int size) {
    radius = size;
}

void Poolball::setBallColor(ColorName color) {
    this->color = color;
}

void Poolball::move(void) {
    xPos += xSpeed;
    yPos += ySpeed;
    check_collisions();
}

void Poolball::draw(void) {
    setColor(this->color);
    drawFilledCircle(xPos, yPos, radius);
    cout << this->xPos << " " << this->yPos << " " << this->color << endl;
}

void Poolball::check_collisions(void) {
    if(yPos > WINDOW_HEIGHT-radius) {
        ySpeed = - ySpeed;
    }
}

// accessor methods
int Poolball::getX(void) {
    return xPos;
}

int Poolball::getY(void) {
    return yPos;
}

int Poolball::getRadius(void) {
    return radius;
}

ColorName Poolball::getColor(void) {
    return color;
}


In this code, I have created two behavior routines we need in our simulation. The move method is supposed to model Newton’s law above. It does this by applying the law and updating the location of the ball based on the current value of the velocity in the “X” and “Y” directions. This much is simple, and the ball is in control of its own movement, exactly as it should be.

The problem we face is figuring out how to implement the code needed to handle banging into the edges of the table. In the first attempt at this, I added a check_collision routine that checks to see if we have hit a wall. As shown, it handles one such wall, but we have others to add. The place to do this check is in the move routine where hitting the wall is likely to happen.

In thinking this through, I had to add code to the Poolball class that had nothing to do with a Poolball, it had to do with the Pool Table, so this attempt is not very good. We should refactor this code and set up another class!

Modeling a Pool Table

As designed now, the Graphics library is locked into a fixed size for the drawing canvas. We should provide a way to change that, and doing so is a simple thing, so let’s try!

First modify your Graphics.h file:

graphics.h:
void graphicsSetup( int wx, int wy, int ww, int wh, int argc, char *argv[] );

Then modify the graphicsSetup function so it looks like this:

Graphics.cpp:
void setColor(ColorName name) {
    switch(name) {
        case WHITE: glColor3d(1.0, 1.0, 1.0); break;
        case GREY: glColor3d(0.4, 0.4, 0.4); break;
        case BLACK: glColor3d(0.0, 0.0, 0.0); break;
        case RED: glColor3d(1.0, 0.0, 0.0); break;
        case ORANGE: glColor3d(1.0, 0.5, 0.0); break;
        case YELLOW: glColor3d(1.0, 1.0, 0.0); break;
        case GREEN: glColor3d(0.0, 1.0, 0.0); break;
        case FOREST_GREEN: glColor3d(0.0, 0.25, 0.0); break;
        case BLUE: glColor3d(0.0, 0.0, 1.0); break;
        case CYAN: glColor3d(0.0, 1.0, 1.0); break;
        case MIDNIGHT_BLUE: glColor3d(0.0, 0.0, 0.25); break;
        case PURPLE: glColor3d(0.5, 0.25, 0.0); break;
        case MAGENTA: glColor3d(1.0, 0.0, 1.0); break;
        case BROWN: glColor3d(0.36, 0.16, 0.1); break;
        default: cerr << "Trying to set invalid color. Color unchanged" << endl;
    }
}

void graphicsSetup( int wx, int xy, int ww, int wh, int argc, char *argv[] ) {
     glutInit( &argc, argv );
     glutInitDisplayMode( GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
	 glutInitWindowPosition( wx,wy );
	 glutInitWindowSize( ww,wh );
	 glutCreateWindow("COSC1315 - Graphics Lab");
	 glClearColor(1.0, 1.0, 1.0, 0.0);
	 gluOrtho2D( 0, ww, 0, wh );
}

Now, we must provide the size of the window we want to draw in, plus the height and width in pixels for our pool table.

Real Pooltables

A little research tells us a real pool table is around 4x8 feet, so we should set up our table so it is twice as wide as it is tall, and pick dimensions that fit the screen reasonably. The numbers set up in the original Graphics.h file will still work, so perhaps we should will set them up as follows:

  • wx: 100
  • wy: 150
  • ww: 1000
  • wh: 500

If things are really to scale, the poolball dimension should be 2 7/16 (2.4375) inches in diameter, which works out to a scale radius of:

  • radius = (1000 * 1.21875)/(8*12) = 12.7

(You should check my math on this! the scaling equation looks like this: 96/1000 = 1.21875/radius.

This seems pretty small, so we will play with really big pool balls, say twice that size with a radius of about 25 pixels.

Hmmm, perhaps we should put all of these calculations in our Pooltable class!

#ifndef POOLTABLE_H
#define POOLTABLE_H

class Pooltable {
    private:
        // regulation dimensions
        float regWidth = 96;             // width of a table in inches
        float regHeight = 48;            // height of a table in inches
        float regBall_radius = 2.4375;   // radius of a standard poolball in inches
        float scale_factor = 10.0;      // scale regulation dimensions to pixels

    public:
        void setScale(float factor);    // scale the table by this factor (inches to pixels)
        int getWidth(void);             // return the drawing width
        int getHeight(void);            // return the drawing height
        int getRadius(void);            // return drawing ball size

        // collision detection
        void check_collision(Poolball ball);    // check this ball for collisions
};

#endif

With this initial design, I can set a scale factor to size the table for some screen I am using at some resoultion, and ask the pooltable object to figure out the proper screen dimensions. For a first cut at building this class, this seems like a plan. Obviously, there are other ways to do all of this. We might et the user tell us how many pixels across we want the drawing surface to be. We will consider that later.

First Table Tests

Let’s write some tests for the pool table class. As shown above, I know what to expect if I set a scale factor and ask for the table dimensions, so lets start there.

TEST_CASE("Test pool table scaling", "[pooltable]") {
    table.setScaleFactor(1.0);
    CHECK(table.getWidth() == 96);
    CHECK(table.getHeight() == 48);
}

That was too simple, but it moves us forward, just as we want.

Implementing the basic methods

We have seen how to implement this part of the class in our Poolball class. Here is the code we need:

int Pooltable::getWidth(void) {
    // return table width in pixels
    return scale_factor * regWidth;
}

int Pooltable::getHeight(void) {
    // return table height in pixels
    return scale_factor * regHeight;
}

int Pooltable::getRadius(void) {
    // return poolball radius in pixels
    return scale_factor * regBallRadius;

}

With this code added, our first tests pass.

Refactoring

I am not really happy with this design. I have thought about setting up a scale factor, and have defined a way to find out the screen size of the table based on that, but I have not set up any variables to save those values. So, every time I want to know the table size, I am going to need to calculate that using some math. Sounds like the program will be slower as a result.

A simple solution is to add a few private variables where we can save the calculations and do them only when needed, like when we call the scaleFactor method. Here are the new lines:

Pooltable.h:
    private:    
        // screen dimensions
        int screen_width;
        int screen_height;
        int screen_ball_radius;

And the implementation:

Pooltable.cpp:
void Pooltable::setScaleFactor(float factor) {
    scale_factor = factor;
    screen_height = regHeight * factor;
    screen_width = regWidth * factor;
    screen_ball_radius = regBallRadius * factor * 2.0;     // twice size for effect!
}

After these changes, the tests still pass, which is what we need. Remember that refactoring is not supposed to alter how the code works, just make it work better. Our changes will make it faster at the expense of a few more variables in memory.

Checking for Collisions

The hardest part of this simulation is dealing with balls colliding with something. In our first example, we had code in the Poolball class taking care of that. However, that meant that the Poolball had to have knowledge about the Pooltable, which is not something I am happy with. It would be better to move the collision logic to the Pooltable class and think about the table as having a view of the action. The Table will place the balls, and the table can test to see if a ball has made contact with an edge.

Testing this will be harder than other tests. We will have to place a ball near a table edge, allow it to hit the wall, and make sure the “bounce” occurs. How can we do all of that? Well, it involves more code in the test, but it is not that hard:

TEST_CASE("Test pool ball bounce", "[pooltable]") {
    Poolball ball;      // default ball
    ball.setRadius(table.getRadius());
    ball.setX(100);
    ball.setY(ball.getRadius() + 2);   // slightly above the table
    ball.setXspeed(2);
    ball.setYspeed(-2);
    table.check_collision(ball);
    table.check_collision(ball);
    table.check_collision(ball);
    table.check_collision(ball);
    CHECK(ball.getYspeed() == 2);
    CHECK(ball.getXspeed() == 2);
}

That is a lot more code in the test. What I am doing is placing one ball on the table slightly above the lower edge, and making it move on a diagonal toward that wall. I move it far enough to make sure it should have bounced, then check the speed to make sure it changed properly. I am thinking about the physics of the simulation in setting this up.

Obviously, the test will fail until we add a bit more code.

Here is the new Poolball.cpp move routine:

void Poolball::move(void) {
    xPos += xSpeed;
    yPos += ySpeed;
    cout << xPos << " " << yPos << endl;
}

Notice we removed the logic that used to check for collisions here.

In the Poolball class, we add new code to check for collisions. How should we do that. Well, we let the table control the ball movement. We will let the “check_collisions” method do one move of the ball, then check for collisions:

void Pooltable::check_collision(Poolball &ball) {
    // check top wall
    ball.move();
    if(ball.getY() < screen_ball_radius) {
        ball.setYspeed(- ball.getYspeed());
    }
}

This makes sense, but we will have to modify things to deal with more than one ball!

Adding this change, we still have a failure. Do you have any idea why? Think back to when we discussed parameters and look to see what kind of parameter we provided to our checl_collision method. It is a “value” parameter, meaning the system makes a copy of the ball we are sending to the routine. If we expect the routine to modify things, we need to send a “reference” to that ball. Making this change is all it takes to pass this test.

Controlling the Animation Loop

In my testing of the Poolball simulation, I discovered that the code ran much faster on my PC than it did on my Mac. Exactly why was a bit of a puzzle until I started digging into how OpenGL/GLUT actually work.

The glutMainLoop Routine

The basic logic of the glutMainLoop routine looks like this:

void glutMainLoop(void) {
    while(true) {
        if(content_of_window_has_changed) {
            drawScene();
        if(keyboard_or_mouse_event) {
            handleKey();
        if(nothing to do) {
            animate();
        }
    }
}

You provide the three functions defined in this loop.

Each pass through this loop creates something called a “frame”, a term that dates back to the early days of movies. The “frame rate” is the speed of the loop in “frames per second”, or fps. Typical numbers for this speed are in the 20-30 fps range.

Since we have not done anything to alter how the main loop runs, we do not really know how long it will take to get through the loop until we run the code. Depending on how things are set up on each machine, speeds might be different.

Controlling the speed

GLUT provides a way to control things. There is a function in GLUT that we can call to get the amount of time that has passed since the call to GlutInit was called. The value returned is in “milliseconds” (1/1000 seconds).

double start_time;

start_time = glutGet(GLUT_ELAPSED_TIME);

This function can be called whenever we like to see how long it takes to do something. For example, in out animate function, we could do this:

void animate(void) {
    double start_time, stop_time, duration;
    start_time = glutGet(GLUT_ELAPSED_TIME);
    ;
    table.check_collisions(ball);
    glutPostRedisplay();
    ;
    stop_time = glutGet(GLUT_ELAPSED_TIME);
    duration = stop_time - start_time;
    fps = 1.0/duration
    cout << fps << endl;
}

Wasting Time

Once we know how fast our code is, we can slow things down so they work better. We pick a desired frame rate (say 60 fps), and calculate how much time we need to waste to get that rate. A pause_ms routines has been added to the library that can use used to delay your code a fixed number of milli-seconds (1/1000 of a second).

There may be cases where you have so much work to do that you cannot get things done in time. In this case, you are not going to waste any time, and things will just slow down. You might be studying your code to find ways to speed things up in this case.

We can modify the main code in the program to see how things work. Adding an output statement in the aninate loop will show you how fast your loop is actually running. From that, you can add a pause with an appropriate number. I will let you experiment with that if your animation is running too fast.

There is a problem with this, though. The actual time it takes to get through the code in the animate function may actually be extremely short, far shorter than one millisecond. So, we get a duration of zero, which does not help.

However, if the code is that fast, we can ignore the time it takes to do our work, and simple calculate how long we should take to get through the loop.

If we want a frame rate of something like 60 fps, the amount of time we want to waste in each pass is 1/60 second, which works out to 16.6 ms. Try adding this delay in your loop and see if it works.