Lesson 21: A Sample Game: Crab Pong
- Summary
- *
- Text version
- *
- Exercises
- *
- Download source
The Game
Now that we've learned so much about OpenGL and 3D programming, let's put it to use! We're going to make a 3D Pong game. This game is shown below:
Each of the paddles is a 3D crab. The bottom crab is controlled using the left and right keys, while the other three are controlled by the computer using some AI.
We'll divide the program into two components: the gameplay and the drawing. Code for the gameplay will be in game.h and game.cpp, while the 3D drawing code will be in gamedrawer.h and gamedrawer.cpp.
game.h and game.cpp
We'll start by running throug game.h. Near the top of game.h is a comment that explains a lot about the gameplay. Begin by reading the comment, which appears below:
/* The game board is a square with corners (0, 0), (0, 1), (1, 0), and (1, 1) in
* the x-z plane. There are balls and four crabs (paddles) in the game board.
* There is one crab of length CRAB_LENGTH on each edge of the square. Each
* crab other than the first, the one on the edge connecting (0, 0) and (1, 0),
* is controlled by the computer. A player is scored on when a ball reaches the
* player's side, but the crab isn't there to block it. When a player is scored
* on, he loses a point. When a player reaches 0 points, he's eliminated, and
* the side where his crab was turns into a wall. There are four circles of
* radius BARRIER_SIZE centered at the corners that limit balls' and crabs'
* positions. There may be multiple balls in play at once. New balls are added
* to play by fading in at the center of the board. As in the original Pong,
* the direction of a ball after hitting a crab depends on the place where the
* ball hit the crab.
*/
Alright, now you know a little about what we'll see in game.h and game.cpp.
//The length of a crab const float CRAB_LENGTH = 0.2f; //The radius of the four barriers positioned at the corners const float BARRIER_SIZE = 0.12f;
Here, we have the CRAB_LENGTH and BARRIER_SIZE constants.
//Represents a single crab (paddle) class Crab { private: //The maximum speed of the crab float maximumSpeed;
Now, we have our Crab class, which represents a single crab. maximumSpeed is (you guessed it) the crab's maximum speed, in units per second.
//The distance from the center of the crab to the corner on its right float pos0;
pos0 is the distance between the center of the crab and the corner on its right. It ranges between 0 and 1, but it won't reach 0 and 1 because of the length of the crab and because of the barriers.
Note that pos0 ends with "0". This is to distinguish it from the pos method that we'll see later. You'll see this pattern a lot, where variables end with 0 so as not to conflict with methods' names.
//-1 if the crab is accelerating toward the corner on its right, 1 if it //is accelerating toward the other corner, and 0 if it is decelerating int dir0;
The dir0 field is -1 if the crab is going toward the corner on its right, 1 if it's going toward the other corner, and 0 if it's slowing down. So in the case of the human-controlled crab, this depends on whether the left and right keys are pressed.
//The velocity of the crab float speed0;
speed0 is the velocity of the crab. A negative number indicates that the crab is moving toward the corner on its right, and a positive number indicates that the crab is moving in the opposite direction.
//The amount of game time until the next call to step() float timeUntilNextStep; //Advances the state of the crab by a short amount of time void step();
We have a field for the amount of time until we'll call the step method, which advances the state of the crab by a small amount of time.
public: //Constructs a new Crab with the specified maximum speed Crab(float maximumSpeed1);
This is the constructor, which takes as a parameter the maximum speed for the crab.
//Returns the distance from the center of the crab to the corner to its //right float pos(); //Returns a negative number if the crab is accelerating toward the //corner on its right, a positive number if it is accelerating toward //the other corner, and 0 if it is decelerating int dir(); //Returns the velocity of the crab float speed();
These are methods for returning the position, direction, and speed of the crab.
//Returns the acceleration of the crab when it is accelerating or //decelerating float acceleration();
The acceleration method returns the acceleration of the crab whenever it's speeding up or slowing down.
//Sets the direction toward which the crab is accelerating. A negative //number indicates to accelerate toward the corner on its right, a //positive number indicates to accelerate toward the other corner, and 0 //indicates to decelerate. void setDir(int dir1);
setDir sets the direction the crab is heading, depending on whether the parameter is negative, positive, or 0.
//Advances the state of the crab by the specified amount of time void advance(float dt);
Now, we have a method that advances the state of the crab by some amount of time.
//Represents a single ball class Ball { private: //The radius of the ball float r; //The x coordinate of the ball float x0; //The z coordinate of the ball float z0;
Now, we have the Ball class, which represents a ball. There are fields for the radius and position of the ball.
//The angle at which the ball is traveling. An angle of 0 indicates the //positive x direction, while an angle of PI / 2 indicates the positive //z direction. The angle is always between 0 and 2 * PI. float angle0;
The angle0 field indicates the direction the ball is traveling. 0 indicates the positive x direction, while PI / 2 indicates the positive z direction. The field is always between 0 and 2 * PI.
//The fraction that the ball is "faded in", from 0 to 1. It is not 1 //when the ball is fading in or out. float fadeAmount0; //Whether the ball is fading out bool isFadingOut0;
fadeAmount0 is the amount that the ball is "faded in". Usually, it will be 1, but if the ball is fading in or fading out, it will be some fraction between 0 and 1. isFadingOut0 stores whether the ball is currently fading out.
public: //Constructs a new ball with the specified radius, x and z coordinates, //and angle of travel. An angle of 0 indicates the positive x //direction, while an angle of PI / 2 indicates the positive z //direction. The angle must be between 0 and 2 * PI. Ball(float radius1, float x1, float z1, float angle1);
The Ball constructor takes the radius, position, and angle of the ball as parameters. Each ball is initially fading in.
//Returns the radius of the ball float radius(); //Returns the x coordinate of the ball float x(); //Returns the z coordinate of the ball float z(); //Returns the angle at which the ball is traveling. An angle of 0 //indicates the positive x direction, while an angle of PI / 2 indicates //the positive z direction. The returned angle is between 0 and 2 * PI. float angle();
There are methods for returning the radius, position, and angle of a ball.
//Sets the angle at which the ball is traveling. An angle of 0 //indicates the positive x direction, while an angle of PI / 2 indicates //the positive z direction. The angle must be between 0 and 2 * PI. void setAngle(float angle1);
setAngle sets the angle at which the ball is traveling.
//Returns the fraction that the ball is "faded in", from 0 to 1. It is //not 1 when the ball is fading in or out. float fadeAmount(); //Causes the ball to begin fading out void fadeOut(); //Returns whether the ball is fading out bool isFadingOut();
The fadeAmount method returns the amount that the ball is "faded in". fadeOut makes the ball start fading out. isFadingOut returns whether the ball is currently fading out.
//Advances the state of the ball by the specified amount of time void advance(float dt);
advance advances the state of the ball by moving it.
//Manages the state of the game class Game { private: //The four crabs. The first is the one on the edge connecting (0, 0) //and (1, 0), and each subsequent crab is one edge counterclockwise with //respect to the previous one. A crab is NULL if it has been eliminated //by reaching a score of 0. Crab* crabs0[4];
Now, let's move on to the Game class, which keeps track of all of the information for a game. The crabs0 array stores the four crabs, as indicated by its comment. Note that a crab will be NULL if it's eliminated from play.
//The balls that are currently in play
std::vector<Ball*> balls0;
balls0 is a vector of all balls that are in play.
Note that we're using std::vector rather than vector because this header file doesn't have a line that says using namespace std;. You're really not supposed to have that line in a header file, because it pollutes the namespace...er...something.
//The scores for each of the players int scores[4];
The scores array stores the players' scores.
//The amount of game time until the next call to step() float timeUntilNextStep;
And, yeah, we have our timeUntilNextStep field.
//Handles all collisions at the current instant void handleCollisions();
The handleCollisions method handles all of the collisions for balls, much like in the lesson on collisions.
//Adjusts the directions in which the computer-controlled crabs are //accelerating, using some AI void doAI();
doAI uses a little AI to control the directions that the computer-controlled crabs are going.
//Advances the state of the game by a short amount of time void step();
As usual, the step method will advance the state by a little time.
public: //Constructs a new Game with the specified starting score. //maximumSpeedForOpponents is the maximum speed at which the computer- //controlled crabs move. It can be used to control the difficulty of //the opponents. Game(float maximumSpeedForOpponents, int startingScore); ~Game();
Now, we have the constructor. It takes as arguments the starting score and the computer-controlled crabs' maximum speeds. The latter can be used to set the difficulty of the computer crabs; a higher maximum speed will make them more difficult.
~Game();
The Game class also has a destructor.
//Sets the direction toward which the human-controlled crab is //accelerating. A negative number indicates to accelerate toward the //corner on its right, a positive number indicates to accelerate toward //the other corner, and 0 indicates to decelerate. void setPlayerCrabDir(int dir);
The setPlayerCrabDir method sets the direction in which the human-controlled crab is going.
//Returns the score of the specified player int score(int crabNum);
The score method returns a particular player's score.
//Advances the state of the game by the specified amount of time void advance(float dt);
advance advances the state of the game.
//Returns the four crabs. The first is the one on the edge connecting //(0, 0) and (1, 0), and each subsequent crab is one edge //counterclockwise with respect to the previous one. A crab is NULL if //it has been eliminated by reaching a score of 0. Crab** crabs(); //Returns the balls that are currently in play std::vector<Ball*> balls();
crabs and balls return the crabs and the balls that are in play.
That does it for game.h. Let's go on to game.cpp, where we can see the code for the methods we just saw.
namespace { const float PI = 3.1415926535f;
At the top, we have some constants, beginning with everyone's favorite constant, PI.
//The amount of time by which the state of the crab is advanced in each call //to a crab's step() method const float CRAB_STEP_TIME = 0.01f;
CRAB_STEP_TIME is the amount of time by which the crab's step method advances the crab.
//The amount of time it takes for a crab to accelerate from no speed to its //maximum speed const float TIME_TO_MAXIMUM_SPEED = 0.18f;
The TIME_TO_MAXIMUM_SPEED constant is the amount of time it takes a crab to accelerate from a dead stop to its maximum speed.
//The maximum angle formed by the direction at which a ball is hit off of a //crab and the normal direction for the crab const float MAX_CRAB_BOUNCE_ANGLE_OFFSET = 0.85f * PI / 2;
If the center of the human-controlled crab hits a ball, it will go straight forward. If the exact left edge of the crab hits the ball, it will go at an angle of MAX_CRAB_BOUNCE_ANGLE_OFFSET to the left. That's the idea behind the MAX_CRAB_BOUNCE_ANGLE_OFFSET constant.
//The maximum speed of the human-controlled crab const float PLAYER_MAXIMUM_SPEED = 2.2f;
This constant is the maximum speed of the human-controlled crab.
//The amount of time it takes for a ball to fade into play const float BALL_FADE_IN_TIME = 0.5f; //The amount of time it takes for a ball to fade out of play const float BALL_FADE_OUT_TIME = 0.5f;
These constants control how long it takes for a ball to fade into or out of play.
//The radius of the balls const float BALL_RADIUS = 0.04f; //The speed of the balls const float BALL_SPEED = 1.0f;
Here, we have the radius and speed of the balls.
//The ideal number of balls in play const int NUM_BALLS = 2;
NUM_BALLS is the number of balls that ideally will be on the board at one time.
//The amount of time by which the state of the game is advanced in each call //to the game's step() method const float GAME_STEP_TIME = 0.01f;
The GAME_STEP_TIME constant is the amount of time between calls to the Game class's step method.
//Returns a random float from 0 to < 1 float randomFloat() { return (float)rand() / ((float)RAND_MAX + 1); } }
Here, we have our trusty ol' randomFloat function.
Crab::Crab(float maximumSpeed1) {
maximumSpeed = maximumSpeed1;
pos0 = 0.5f;
dir0 = 0;
speed0 = 0;
timeUntilNextStep = 0;
}
On to the Crab class's constructor. It just initializes some of the fields.
void Crab::step() { //Accelerate the crab float ds = CRAB_STEP_TIME * acceleration(); if (dir0 != 0) { speed0 += dir0 * ds; if (speed0 > maximumSpeed) { speed0 = maximumSpeed; } else if (speed0 < -maximumSpeed) { speed0 = -maximumSpeed; } } else { float s = abs(speed0); s -= ds; if (s < 0) { s = 0; } if (speed0 > 0) { speed0 = s; } else { speed0 = -s; } }
Here, we have the Crab class's step method. First, it adjusts the speed0 field. It either increases or decreases or increases the velocity by CRAB_STEP_TIME * acceleration(), up to the maximum speed of the crab, making sure to allow for stopping the crab if its direction is 0.
//Move the crab pos0 += CRAB_STEP_TIME * speed0; if (pos0 < BARRIER_SIZE + CRAB_LENGTH / 2) { pos0 = BARRIER_SIZE + CRAB_LENGTH / 2; speed0 = 0; } else if (pos0 > 1 - BARRIER_SIZE - CRAB_LENGTH / 2) { pos0 = 1 - BARRIER_SIZE - CRAB_LENGTH / 2; speed0 = 0; } }
In the remainder of the step method, we advance the position of the crab by CRAB_STEP_TIME * speed0, subject to not exceeding the barriers.
float Crab::pos() { return pos0; } int Crab::dir() { return dir0; } float Crab::speed() { return speed0; }
These methods return the position, direction, and speed of a crab.
float Crab::acceleration() { return maximumSpeed / TIME_TO_MAXIMUM_SPEED; }
acceleration returns the acceleration of the crab when it's speeding up or slowing down. This is just the maximum speed divided by the amount of time it takes to reach the maximum speed.
void Crab::setDir(int dir1) { if (dir1 < 0) { dir0 = -1; } else if (dir1 > 0) { dir0 = 1; } else { dir0 = 0; } }
The setDir method changes the direction of the crab by setting the dir0 field.
void Crab::advance(float dt) { while (dt > 0) { if (timeUntilNextStep < dt) { dt -= timeUntilNextStep; step(); timeUntilNextStep = CRAB_STEP_TIME; } else { timeUntilNextStep -= dt; dt = 0; } } }
advance advances the state of the crab by calling step the right number of times.
Ball::Ball(float radius1, float x1, float z1, float angle1) { r = radius1; x0 = x1; z0 = z1; angle0 = angle1; fadeAmount0 = 0; isFadingOut0 = false; }
Now, we have the Ball class's constructor. It just initializes a few fields.
float Ball::radius() { return r; } float Ball::x() { return x0; } float Ball::z() { return z0; } float Ball::angle() { return angle0; } void Ball::setAngle(float angle1) { angle0 = angle1; } float Ball::fadeAmount() { return fadeAmount0; } void Ball::fadeOut() { isFadingOut0 = true; } bool Ball::isFadingOut() { return isFadingOut0; }
Here are the methods for returning the radius, position, and angle of a ball, setting the angle of a ball, returning the amount that a ball is "faded in", making a ball fade out, and returning whether a ball is currently fading out.
void Ball::advance(float dt) { if (isFadingOut0) { //Fade out fadeAmount0 -= dt / BALL_FADE_OUT_TIME; if (fadeAmount0 < 0) { fadeAmount0 = 0; } } else if (fadeAmount0 < 1) { //Fade in fadeAmount0 += dt / BALL_FADE_IN_TIME; if (fadeAmount0 > 1) { dt = (fadeAmount0 - 1) * BALL_FADE_IN_TIME; fadeAmount0 = 1; } else { dt = 0; } }
Now, we have the advance method. First, we adjust fadeAmount0 if the ball is fading in or fading out.
if (dt <= 0) { return; } //Advance the position of the ball x0 += dt * cos(angle0) * BALL_SPEED; z0 += dt * sin(angle0) * BALL_SPEED; }
Then, we advance the position of the ball using a little trigonometry.
Game::Game(float maximumSpeedForOpponents, int startingScore) { if (startingScore > 0) { crabs0[0] = new Crab(PLAYER_MAXIMUM_SPEED); for(int i = 1; i < 4; i++) { crabs0[i] = new Crab(maximumSpeedForOpponents); } } else { for(int i = 0; i < 4; i++) { crabs0[i] = NULL; } } for(int i = 0; i < 4; i++) { scores[i] = startingScore; } timeUntilNextStep = 0; }
The constructor for the Game class initializes some fields, including making some new Crab objects if the starting score is positive.
Game::~Game() { for(int i = 0; i < 4; i++) { if (crabs0[i] != NULL) { delete crabs0[i]; } } for(unsigned int i = 0; i < balls0.size(); i++) { delete balls0[i]; } }
The destructor deletes the crabs and balls.
namespace { //Returns whether the point (dx, dz) lies within r units of (0, 0) bool intersectsCircle(float dx, float dz, float r) { return dx * dx + dz * dz < r * r; }
Now, we'll see some functions that we'll use for collision detection. intersectsCircle tells us whether (dx, dz) is within r units of the origin.
//Returns whether a ball is colliding with a circle that is dx units to the //right and dz units inward from it, where r is the sum of the radius of the //ball and the radius of the circle and (vx, vz) is the velocity of the ball bool collisionWithCircle(float dx, float dz, float r, float vx, float vz) { return intersectsCircle(dx, dz, r) && vx * dx + vz * dz > 0; }
collisionWithCircle returns whether a ball is colliding with a circle. It takes as parameters the distance to the circle in x and z directions, the sum of the radius of the ball and the radius of the circle, and the velocity of the ball. It operates by checking whether the ball intersects the circle and is moving towards the circle, based on the dot product of the displacement and the velocity.
//Returns the resultant angle when an object traveling at an angle of angle //bounces off of a wall whose normal is at an angle of normal. The returned //angle will be between 0 and 2 * PI. An angle of 0 indicates the positive //x direction, and an angle of PI / 2 indicates the positive z direction. float reflect(float angle, float normal) { angle = 2 * normal - PI - angle; while (angle < 0) { angle += 2 * PI; } while (angle > 2 * PI) { angle -= 2 * PI; } return angle; }
The reflect function...erm...well...just read the comments.
//Adjusts the ball's angle in response to a collision with a circle at the //specified position void collideWithCircle(Ball* ball, float x, float z) { if (ball->fadeAmount() < 1) { return; } float dx = x - ball->x(); float dz = z - ball->z(); float normal = atan2(-dz, -dx); float newBallAngle = reflect(ball->angle(), normal); if (newBallAngle < 0) { newBallAngle += 2 * PI; } else if (newBallAngle > 2 * PI) { newBallAngle -= 2 * PI; } ball->setAngle(newBallAngle); }
The collideWithCircle function causes a ball to bounce off of a circle. It just computes the normal vector at the point of contact between the ball and the circle and uses the reflect function to figure out the new angle for the ball.
//Returns whether a crab at the indicated position has intercepted a ball at //the indicated position, where the positions are measured parallel to the //direction in which the crab moves bool collisionWithCrab(float crabPos, float ballPos) { return abs(crabPos - ballPos) < CRAB_LENGTH / 2; }
collisionWithCrab returns whether a ball is colliding with a crab. It just computes whether the ball's position is within CRAB_LENGTH / 2 of the crab's position.
//Adjusts the ball's angle in response to a collision with a crab. The //positions are measured parallel to the direction in which the crab moves, //and the crab's position is its distance from its center to the corner to //its right. void collideWithCrab(Ball* ball, int crabIndex, float crabPos, float ballPos) { float angle = (1 - crabIndex) * PI / 2 + MAX_CRAB_BOUNCE_ANGLE_OFFSET * (crabPos - ballPos) / (CRAB_LENGTH / 2); while (angle < 0) { angle += 2 * PI; } while (angle >= 2 * PI) { angle -= 2 * PI; } ball->setAngle(angle); } }
collideWithCrab causes a ball to bounce off of a crab. If the ball hits the center of the crab, it will bounce off at an angle of (1 - crabIndex) * PI / 2. On top of that, we use some math the deviate the angle by some number between -MAX_CRAB_BOUNCE_ANGLE_OFFSET and MAX_CRAB_BOUNCE_ANGLE_OFFSET, depending on the spot where the ball hit the crab.
void Game::handleCollisions() { for(unsigned int i = 0; i < balls0.size(); i++) { Ball* ball = balls0[i]; if (ball->fadeAmount() < 1 || ball->isFadingOut()) { continue; } //Ball-barrier collisions for(float z = 0; z < 2; z += 1) { for(float x = 0; x < 2; x += 1) { if (collisionWithCircle(x - ball->x(), z - ball->z(), ball->radius() + BARRIER_SIZE, BALL_SPEED * cos(ball->angle()), BALL_SPEED * sin(ball->angle()))) { collideWithCircle(ball, x, z); } } }
Now, we have the method that takes care of handling all of the collisions with balls. We have a loop that runs through all of the balls. For each ball, we first check for ball-barrier collisions. We go through the four barriers, and if the ball is colliding with one, we call collideWithCircle to make it bounce off.
//Ball-ball collisions for(unsigned int j = i + 1; j < balls0.size(); j++) { Ball* ball2 = balls0[j]; if (collisionWithCircle(ball2->x() - ball->x(), ball2->z() - ball->z(), ball->radius() + ball2->radius(), BALL_SPEED * (cos(ball->angle()) - cos(ball2->angle())), BALL_SPEED * (sin(ball->angle()) - sin(ball2->angle())))) { collideWithCircle(ball, ball2->x(), ball2->z()); collideWithCircle(ball2, ball->x(), ball->z()); } }
Now, we check for ball-ball collisions. We go through all of the balls that are later in the balls0 vector; that way, we'll only try each pair of balls once. If the two balls are colliding, we'll call collideWithCircle on both of the balls to make them bounce off of each other.
Note that sometimes, the ball bouncing is unrealistic. We're sort of forced to accept this limitation if we don't want the balls to change speed when they hit other balls.
//Ball-crab (and ball-pole) collisions int crabIndex; float ballPos; if (ball->z() < ball->radius()) { crabIndex = 0; ballPos = ball->x(); } else if (ball->x() < ball->radius()) { crabIndex = 1; ballPos = 1 - ball->z(); } else if (ball->z() > 1 - ball->radius()) { crabIndex = 2; ballPos = 1 - ball->x(); } else if (ball->x() > 1 - ball->radius()) { crabIndex = 3; ballPos = ball->z(); } else { crabIndex = -1; ballPos = 0; } if (crabIndex >= 0) { if (crabs0[crabIndex] != NULL) { float crabPos = crabs0[crabIndex]->pos(); if (collisionWithCrab(crabPos, ballPos)) { collideWithCrab(ball, crabIndex, crabPos, ballPos); } } else { float normal = (1 - crabIndex) * PI / 2; float newAngle = reflect(ball->angle(), normal); ball->setAngle(newAngle); } } } }
Here, we handle ball-crab and ball-pole collisions. Poles are walls that appear on an edge whenever a crab is eliminated. First, we figure out if the ball has exceeded any boundary of the board, and, if so, to which crab the boundary corresponds. Then, we check whether there's a collision and handle the collision if there is one.
namespace { //Returns the position at which the specified crab will stop if it //immediately starts decelerating float stopPos(Crab* crab) { float d = crab->speed() * crab->speed() / crab->acceleration(); if (crab->speed() > 0) { return crab->pos() + d; } else { return crab->pos() - d; } } }
The stopPos function, which will be used by the doAI method, returns the position at which a crab would stop if it immediately started decelerating. It uses a bit of physics to find the right spot.
void Game::doAI() { for(int i = 1; i < 4; i++) { Crab* crab = crabs0[i]; if (crab == NULL) { continue; } //Find the position of the ball that is nearest the crab's side, and //store the result in targetPos float closestBallDist = 100; float targetPos = 0.5f; for(unsigned int j = 0; j < balls0.size(); j++) { Ball* ball = balls0[j]; float ballDist; float ballPos; switch(i) { case 1: ballDist = ball->x() - ball->radius(); ballPos = 1 - ball->z(); break; case 2: ballDist = 1 - ball->z() - ball->radius(); ballPos = 1 - ball->x(); break; case 3: ballDist = 1 - ball->x() - ball->radius(); ballPos = ball->z(); break; } if (ballDist < closestBallDist) { targetPos = ballPos; closestBallDist = ballDist; } }
Now we have the code for the AI that moves the computer-controlled crabs. The AI will actually be rather simple. The crabs will move toward the nearest ball. By "nearest ball", I mean nearest in the horizontal direction for the crabs on the left and right and the nearest in the vertical direction for the crab on top. So, the program loops through the balls, and every time it finds a new closest ball, it changes closestBallDist to be the ball's distance and targetPos to be the ball's position.
//Move toward targetPos. Stop so that the ball is in the middle 70% of //the crab. if (abs(stopPos(crab) - targetPos) < 0.7f * (CRAB_LENGTH / 2)) { crab->setDir(0); } else if (targetPos < crab->pos()) { crab->setDir(-1); } else { crab->setDir(1); }
We want to have the crab move toward targetPos. If after stopping the crab, the ball would be in the middle 70% of the crab, we'll just stop. This makes it so that we don't keep overshooting the ball and keep oscillating back and forth around the ball's position. If such is not the case, we'll accelerate towards the ball's position.
void Game::step() { //Advance the crabs for(int i = 0; i < 4; i++) { Crab* crab = crabs0[i]; if (crab != NULL) { crab->advance(GAME_STEP_TIME); } } //Advance the balls for(unsigned int i = 0; i < balls0.size(); i++) { balls0[i]->advance(GAME_STEP_TIME); } //Handle collisions handleCollisions();
Here's the step method. First, it advances the crabs and balls and handles collisions.
//Check for balls that have scored on a player vector<Ball*> newBalls; for(unsigned int i = 0; i < balls0.size(); i++) { Ball* ball = balls0[i]; if (ball->fadeAmount() == 1 && !ball->isFadingOut()) { newBalls.push_back(ball); int scoredOn; if (ball->z() < ball->radius() && (ball->angle() > PI)) { scoredOn = 0; } else if (ball->x() < ball->radius() && (ball->angle() > PI / 2 && ball->angle() < 3 * PI / 2)) { scoredOn = 1; } else if (ball->z() > 1 - ball->radius() && (ball->angle() < PI)) { scoredOn = 2; } else if (ball->x() > 1 - ball->radius() && (ball->angle() < PI / 2 || ball->angle() > 3 * PI / 2)) { scoredOn = 3; } else { scoredOn = -1; } if (scoredOn >= 0 && crabs0[scoredOn] != NULL) { scores[scoredOn]--; if (scores[scoredOn] == 0) { delete crabs0[scoredOn]; crabs0[scoredOn] = NULL; } ball->fadeOut(); } } else if (ball->fadeAmount() > 0 || !ball->isFadingOut()) { newBalls.push_back(ball); } else { delete ball; } } balls0 = newBalls;
Now, we're going to see if anyone's been scored on, and we'll get rid of any balls that have completely faded out. The newBalls vector stores all of the balls that haven't faded out, and after the loop, balls0 is updated to be equal to newBalls. The loop goes through the balls and checks whether they've exceeded one of the boundaries of the board. If so, that crab's score is reduced by 1, if it hasn't been eliminated yet. Then, if its score reaches 0, the crab is deleted and the appropriate element of crabs0 is set to NULL. Finally, the ball is faded out.
//Check whether the game is over bool isGameOver; if (crabs0[0] != NULL) { isGameOver = true; for(int i = 1; i < 4; i++) { if (crabs0[i] != NULL) { isGameOver = false; } } } else { isGameOver = true; }
This piece of code determines whether the game is over, by checking whether all of the computer opponents have been eliminated or whether the human-controlled crab has been eliminated. (We don't want to watch the computer players play each other when the human player been eliminated.)
if (!isGameOver) { //Try to get to NUM_BALLS balls while (balls0.size() < (unsigned int)NUM_BALLS) { //Try to place a ball at the center of the board bool ballFits = true; for(unsigned int i = 0; i < balls0.size(); i++) { Ball* ball = balls0[i]; if (intersectsCircle(ball->x() - 0.5f, ball->z() - 0.5f, 2 * BALL_RADIUS)) { ballFits = false; break; } } if (ballFits) { Ball* ball = new Ball(BALL_RADIUS, 0.5f, 0.5f, 2 * PI * randomFloat()); balls0.push_back(ball); } else { break; } } } else { for(unsigned int i = 0; i < balls0.size(); i++) { balls0[i]->fadeOut(); } }
If the game is not over, we'll try to add balls to the board until there are NUM_BALLS balls. To try to add a ball at the middle of the board, we check whether there's already a ball there, and if not, we add a new ball moving in a random direction.
If the game is over, we make all of the balls fade out.
//Run the AI for the computer-controlled crabs
doAI();
}
Finally, we run the AI.
void Game::setPlayerCrabDir(int dir) { if (crabs0[0] != NULL) { crabs0[0]->setDir(dir); } } int Game::score(int crabNum) { return scores[crabNum]; }
These methods for setting the human-controlled crab's direction and returning a player's score are pretty simple.
void Game::advance(float dt) { while (dt > 0) { if (timeUntilNextStep < dt) { dt -= timeUntilNextStep; step(); timeUntilNextStep = CRAB_STEP_TIME; } else { timeUntilNextStep -= dt; dt = 0; } } }
The advance method just calls step the appropriate number of times.
Crab** Game::crabs() { return crabs0; } vector<Ball*> Game::balls() { return balls0; }
The crabs and balls method return all of the crabs and balls that are in play.
That does it for the gameplay code.
Drawing Code
The main drawing code for the program is in gamedrawer.h and gamedrawer.cpp. But first, we'll take a look at the MD2Model class, as there are a couple of changes. We'll look at md2model.h.
/* Draws the state of the animated model at the specified time in the * animation. A time of i, integer i, indicates the beginning of the * animation, and a time of i + 0.5 indicates halfway through the * animation. textureNum is the index of the texture that is used when * drawing the model. */ void draw(int textureNum, float time); //Loads an MD2Model from the specified file, loading texture images from //the indicated files. Returns NULL if there was an error loading it. static MD2Model* load(const char* filename, std::vector<const char*> textureFilenames);
First of all, there's a second parameter to the load method so that we can load multiple textures for a single MD2 model. There are four textures for the four crabs, which are different colors. The draw method takes parameters indicating the index of the texture we want to use and the particular time in the animation to use.
Now, we'll move over to md2model.cpp and look at the changes there.
//Load the textures (ignore the texture suggested by the MD2 file) MD2Model* model = new MD2Model(); for(unsigned int i = 0; i < textureFilenames.size(); i++) { const char* f = textureFilenames[i]; if (strlen(f) < 5 || strcmp(f + strlen(f) - 4, ".bmp") != 0) { delete model; return NULL; } Image* image = loadBMP(f); GLuint textureId = loadTexture(image); delete image; model->textureIds.push_back(textureId); }
Here's the new code for loading the textures. It puts the textures into a textureIds vector, which has replace the textureId field. Then, down in the draw method, we'll call glBindTexture(GL_TEXTURE_2D, textureIds[textureNum]) rather than glBindTexture(GL_TEXTURE_2D, textureId) when selecting the texture to use.
Also, the way we had it before, the draw method would specify the vertices of each triangle in clockwise order. We'll fix that by changing
for(int j = 0; j < 3; j++) {
to
for(int j = 2; j >= 0; j--) {
so that the loop goes through the vertices in the opposite order.
Now, we'll move on to gamedrawer.h.
class Game; class MD2Model;
These lines of code tell C++ that the Game and MD2Model classes exist. We don't actually have to include everything in game.h and md2model.h; we just need to know that these classes exist.
//Maitains the state of the game by using an enclosed Game object, and takes //care of drawing the game class GameDrawer { private: //The Game object maintaining the state of the game. If no game has yet //been started, this is a placeholder game for which the maximum score //is 0. If the program is waiting for the user to start a new game, //this is the game that was just finished. Game* game;
The GameDrawer class will take care of drawing the game. It contains a Game object for the current game. Before any game has been started, game is just a placeholder game with a maximum score of 0.
//The model for the crab
MD2Model* crabModel;
crabModel is the model for the crab.
//The id of the display list for the four barriers at the corners GLuint barriersDisplayListId; //The id of the display list for a "pole" drawn when a crab has been //eliminated. It is drawn for the side connecting (0, 0, 0) and //(1, 0, 0). GLuint poleDisplayListId;
The barriers and the pole will be drawn using display lists. barriersDisplayListId and poleDisplayListId store the ids of their display lists. There's actually one display list that draws all four of the barriers by calling a display list for one barrier four times.
//The id of the texture for the sand GLuint sandTextureId; //The id of the texture for the water GLuint waterTextureId;
sandTextureId and waterTextureId are the ids of the textures for sand and water.
//The fraction that each crab is "faded in", from 0 to 1. An element is //not 1 when the corresponding crab is shrinking or completely //disappeared after having been eliminated. float crabFadeAmounts[4];
When a player is eliminated, his crab shrinks until it disappears. The crabFadeAmounts field is the amount by which the crab is scaled for this shrinking effect.
//The animation times for the crab model for the four crabs float animTimes[4];
The animTimes array stores the animation times for the four crabs.
//The last known position of each crab. Kept for when crabs become NULL //after being eliminated from play. float oldCrabPos[4];
oldCrabPos is the last known position of each crab. The array is used so that we don't forget where a crab was when we're making it shrink. Remember, a crab becomes NULL when it's removed from play.
//The distance that the water has traveled, modulo the size of each //repetition of the water texture float waterTextureOffset;
The water texture constantly moves forward. waterTextureOffset is the distance it has moved, modulo the size of each repetition of the water texture image.
//Whether the game is currently over bool isGameOver0;
The isGameOver0 field is whether the game is over.
//Whether any game has been started or finished bool waitingForFirstGame;
This field is true only at the very beginning, before the first game is started.
//A negative number if the human-controlled crab is accelerating toward //the corner on its right, a positive number if it is accelerating //toward the other corner, and 0 if it is decelerating int playerCrabDir;
The playerCrabDir field indicates the direction the human-controlled crab is headed.
//The amount of game time until the next call to step() float timeUntilNextStep;
timeUntilNextStep is the amount of time until we next call the GameDrawer class's step method.
//Switches to use the specified Game object void setGame(Game* game);
The setGame method sets the current game to the given Game object. It's called when a new game is started.
//Sets up the display list for the four barriers at the corners void setupBarriers(); //Sets up the display list for the pole void setupPole();
These methods set up the display lists for drawing the barriers and for drawing a pole.
//Sets up the lighting in OpenGL void setupLighting();
The setupLighting method sets up the ambient light and the light sources.
//Draws the crabs and poles. isReflected indicates whether the //reflections of the crabs and poles are being drawn rather than the //crabs and poles themselves. void drawCrabsAndPoles(bool isReflected); //Draws the four barriers at the corners. isReflected indicates whether //the reflections of the barriers are being drawn rather than the //barriers themselves. void drawBarriers(bool isReflected); //Draws the players' scores. isReflected indicates whether the //reflections of the scores are being drawn rather than the scores //themselves. void drawScores(bool isReflected); //Draws the balls. isReflected indicates whether the reflections of the //balls are being drawn rather than the balls themselves. void drawBalls(bool isReflected);
Now, we have some methods for drawing stuff. Each of these methods has a isReflected parameter indicating whether we are drawing the reflections or the objects themselves. If we're drawing the reflections, which we do using a call to glScalef(1, -1, 1) to reflect about the y axis, we'll want to make sure that GL_NORMALIZE is enabled.
//Draws all of the objects that have reflections in the water. //isReflected indicates whether the reflections of the objects are being //drawn rather than the objects themselves. void drawReflectableObjects(bool isReflected);
drawReflectableObjects calls the four methods we just saw.
//Draws the sand void drawSand(); //Draws the water, blending it onto the screen void drawWater();
These methods take care of drawing the sand and the water.
//Draws text indicating the winner of the game and/or some instructions //if a game is not currently in progress void drawWinner();
The drawWinner method draws some text whenever there is no game in progress. It draws some insructions text, as well as the winner of the previous game, if there was a previous game.
public:
GameDrawer();
~GameDrawer();
Now, we've got our constructor and our destructor.
//Draws the game, positioned according to board coordinates, so that the //board's corners are at (0, 0, 0), (0, 0, 1), (1, 0, 0), and (1, 0, 1). void draw();
This is the main drawing method for the GameDrawer class, the method that draws everything.
//Advances the state of the game by the specified amount of time void advance(float dt);
The advance method is for advancing the state of the game.
//Sets the direction toward which the human-controlled crab is //accelerating. A negative number indicates to accelerate toward the //corner on its right, a positive number indicates to accelerate toward //the other corner, and 0 indicates to decelerate. void setPlayerCrabDir(int dir);
The setPlayerCrabDir method sets the direction the human-controlled crab is going.
//Returns false iff a game is not currently being played bool isGameOver();
isGameOver returns whether the game is over.
//Starts a new game with the specified starting score. //maximumSpeedForOpponents is the maximum speed at which the computer- //controlled crabs move. It can be used to control the difficulty of //the opponents. void startNewGame(float maximumSpeedForOpponents, int startingScore); };
The startNewGame method is called to start a new game.
//Performs some initialization required for the GameDrawer class to function //properly. void initGameDrawer(); //Frees some resources used by the GameDrawer class after a call to //initGameDrawer(). void cleanupGameDrawer();
initGameDrawer is supposed to be called at the beginning of the program, while cleanupGameDrawer is supposed to be called at the end. As we'll see later, the former just calls t3dInit to set up the text-drawing functionality, while the latter just calls t3dCleanup to dispose of the text-drawing functionality.
Now, we'll go to gamedrawer.cpp to see the implementation of all of these methods.
namespace { const float PI = 3.1415926535f; //The amount of time by which the state of the game is advanced in each call //to the step() method const float STEP_TIME = 0.01f;
We have a main course of PI, followed by a helping of STEP_TIME, which is the amount of time between calls to the GameDrawer class's step method.
//The duration of a single loop of the walking animation const float WALK_ANIM_TIME = 0.4f; //The duration of a single loop of the standing animation const float STAND_ANIM_TIME = 2.0f;
These constants indicate the duration of a single loop of the walking and standing animations for the crabs.
//The amount of time it takes for a crab that has just been eliminated to //shrink and disappear const float CRAB_FADE_OUT_TIME = 1.5f;
CRAB_FADE_OUT_TIME is the amount of time it takes an eliminated crab to shrink to nothing.
//The number of points used to approximate a circle in the barrier model const int NUM_BARRIER_POINTS = 30; //The number of points used to approximate a circle in the pole model const int NUM_POLE_POINTS = 6;
The circular ends of the barriers and the poles are drawn using NUM_BARRIER_POINTS and NUM_POLE_POINTS at the outside respectively.
//The height of the barrier model const float BARRIER_HEIGHT = BARRIER_SIZE;
BARRIER_HEIGHT is the height of each of the cylindrical barriers.
//The radius of the pole model const float POLE_RADIUS = 0.02f; //The height of the center of a pole above the ground const float POLE_HEIGHT = 0.07f;
These constants indicate the radius of each pole and the height above the ground of the centers of the poles.
//The number of units the human player's crab model should be translated in //the z direction, and that other crabs should similarly be translated const float CRAB_OFFSET = -0.032f;
This constant is the amount by which the human-controlled crab is translated in the z direction. The other crabs are translated similarly.
//The amount of time until the water travels WATER_TEXTURE_SIZE units const float WATER_TEXTURE_TIME = 8.0f; //The length of a single repetition of the water texture image const float WATER_TEXTURE_SIZE = 0.7f;
The water moves forward at a rate of WATER_TEXTURE_SIZE units per WATER_TEXTURE_TIME seconds. Each repetition of the water texture is WATER_TEXTURE_SIZE units square.
//The opacity of the water const float WATER_ALPHA = 0.8f;
This is the opacity of the water, which is blended onto the screen in order to have reflections.
I'll skip over the loadTexture function to the constructor for the GameDrawer class.
GameDrawer::GameDrawer() { game = NULL; playerCrabDir = 0; //Start a new placeholder game with a maximum score of 0 startNewGame(0, 0); waterTextureOffset = 0; vector<const char*> textureFilenames; textureFilenames.push_back("crab1.bmp"); textureFilenames.push_back("crab2.bmp"); textureFilenames.push_back("crab3.bmp"); textureFilenames.push_back("crab4.bmp"); crabModel = MD2Model::load("crab.md2", textureFilenames); setupBarriers(); setupPole(); Image* image = loadBMP("sand.bmp"); sandTextureId = loadTexture(image); delete image; image = loadBMP("water.bmp"); waterTextureId = loadTexture(image); delete image; }
In the constructor, we set the game to be NULL and the human player's crab's direction to be 0. Then, we call startNewGame to set game to be a placeholder game with a maximum score of 0. We set waterTextureOffset to 0. We call MD2Model::load to load the crab model. We call setupBarriers and setupPole to set up the display lists for the barriers and poles. Then, we load in textures for sand and for water.
GameDrawer::~GameDrawer() {
delete game;
}
The destructor just deletes the game object.
void GameDrawer::setGame(Game* game1) { if (game != NULL) { delete game; } game = game1; game->setPlayerCrabDir(playerCrabDir); timeUntilNextStep = 0; isGameOver0 = (game->score(0) == 0); waitingForFirstGame = isGameOver0; for(int i = 0; i < 4; i++) { animTimes[i] = 0; if (!isGameOver0) { crabFadeAmounts[i] = 1; } else { crabFadeAmounts[i] = 0; } Crab* crab = game->crabs()[i]; if (crab != NULL) { oldCrabPos[i] = game->crabs()[i]->pos(); } else { oldCrabPos[i] = 0.5f; } } }
The setGame method deletes the old game if there was one, then sets the game field to be the new game. We set the human player's crab's direction to be playerCrabDir, and set timeUntilNextStep to 0. Then, we determine whether the game is already over, that is, whether we're using a placeholder game. After that, we set some initial values for the animTimes, crabFadeAmounts, and oldCrabPos arrays.
void GameDrawer::setupBarriers() { Image* image = loadBMP("vtr.bmp"); GLuint textureId = loadTexture(image); delete image; GLuint barrierDisplayListId = glGenLists(1); glNewList(barrierDisplayListId, GL_COMPILE); //Draw the top circle glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, textureId); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); GLfloat materialColor[] = {1, 1, 1, 1}; glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, materialColor); glNormal3f(0, 1, 0); glBegin(GL_TRIANGLE_FAN); glTexCoord2f(0.5f, 0.5f); glVertex3f(0, BARRIER_HEIGHT, 0); for(int i = NUM_BARRIER_POINTS; i >= 0; i--) { float angle = 2 * PI * (float)i / (float)NUM_BARRIER_POINTS; glTexCoord2f(-cos(angle) / 2 + 0.5f, sin(angle) / 2 + 0.5f); glVertex3f(BARRIER_SIZE * cos(angle), BARRIER_HEIGHT, BARRIER_SIZE * sin(angle)); } glEnd();
Now, we have the setupBarriers method, where we make the display list for the barriers. We load in the texture for the top of the barriers. Then, we're going to set up a display list for one barrier, which we'll call four times in the display list for all four barriers.
You'll notice there's a call to glMaterialfv. We'll actually be using materials to make the barriers shiny.
First, we draw the top circle of the barrier. The top circle is drawn using a GL_TRIANGLE_FAN. This tells OpenGL to draw triangles connecting the first point and each pair of consecutive points after that. The following diagram illustrates the way we'll specify the circle's vertices:
So, with a bit of trigonometry, we get the appropriate points for drawing the circle.
//Draw the bottom circle GLfloat materialColor2[] = {1.0f, 0.0f, 0.0f, 1}; glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, materialColor2); glDisable(GL_TEXTURE_2D); glColor3f(1.0f, 0.0f, 0.0f); glNormal3f(0, -1, 0); glBegin(GL_TRIANGLE_FAN); glVertex3f(0, 0, 0); for(int i = 0; i <= NUM_BARRIER_POINTS; i++) { float angle = 2 * PI * (float)i / (float)NUM_BARRIER_POINTS; glVertex3f(BARRIER_SIZE * cos(angle), 0, BARRIER_SIZE * sin(angle)); } glEnd();
In the same manner, we draw the bottom cirlce.
//Draw the cylinder part glBegin(GL_QUAD_STRIP); for(int i = 0; i <= NUM_BARRIER_POINTS; i++) { float angle = 2 * PI * ((float)i - 0.5f) / (float)NUM_BARRIER_POINTS; glNormal3f(cos(angle), 0, sin(angle)); float angle2 = 2 * PI * (float)i / (float)NUM_BARRIER_POINTS; glVertex3f(BARRIER_SIZE * cos(angle2), 0, BARRIER_SIZE * sin(angle2)); glVertex3f(BARRIER_SIZE * cos(angle2), BARRIER_HEIGHT, BARRIER_SIZE * sin(angle2)); } glEnd(); glEndList();
Now, we draw the cylinder part using a GL_QUAD_STRIP. A GL_QUAD_STRIP lets us draw a bunch of quadrilaterals, as shown below:
So, with more trigonometry, we determine the points for the cylinder part.
//Make a display list with four copies of the barrier barriersDisplayListId = glGenLists(1); glNewList(barriersDisplayListId, GL_COMPILE); glDisable(GL_COLOR_MATERIAL); //Add a little specularity GLfloat materialSpecular[] = {1, 1, 1, 1}; glMaterialfv(GL_FRONT, GL_SPECULAR, materialSpecular); glMaterialf(GL_FRONT, GL_SHININESS, 15.0f); for(float z = 0; z < 2; z++) { for(float x = 0; x < 2; x++) { glPushMatrix(); glTranslatef(x, 0, z); glCallList(barrierDisplayListId); glPopMatrix(); } } glEnable(GL_COLOR_MATERIAL); //Disable specularity GLfloat materialColor3[] = {1, 1, 1, 1}; GLfloat materialSpecular2[] = {0, 0, 0, 1}; glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, materialColor3); glMaterialfv(GL_FRONT, GL_SPECULAR, materialSpecular2); glEndList(); }
Here, we set up the display list for drawing all four barriers at once. We disable GL_COLOR_MATERIAL so that we can use the glMaterial functions to make the barriers shiny. We set up a little shininess with calls to glMaterialfv and glMaterialf. Then, we call the display list for drawing one barrier four times. Then, we re-enable GL_COLOR_MATERIAL, and eliminate the specularity we just added.
void GameDrawer::setupPole() { poleDisplayListId = glGenLists(1); glNewList(poleDisplayListId, GL_COMPILE); glDisable(GL_TEXTURE_2D); glColor3f(0.0f, 0.8f, 0.0f); //Draw the left circle glNormal3f(-1, 0, 0); glBegin(GL_TRIANGLE_FAN); glVertex3f(BARRIER_SIZE, POLE_HEIGHT, -POLE_RADIUS); for(int i = NUM_POLE_POINTS; i >= 0; i--) { float angle = 2 * PI * (float)i / (float)NUM_POLE_POINTS; glVertex3f(BARRIER_SIZE, POLE_HEIGHT + POLE_RADIUS * cos(angle), POLE_RADIUS * (sin(angle) - 1)); } glEnd(); //Draw the right circle glNormal3f(1, 0, 0); glBegin(GL_TRIANGLE_FAN); glVertex3f(1 - BARRIER_SIZE, POLE_HEIGHT, -POLE_RADIUS); for(int i = 0; i <= NUM_POLE_POINTS; i++) { float angle = 2 * PI * (float)i / (float)NUM_POLE_POINTS; glVertex3f(1 - BARRIER_SIZE, POLE_HEIGHT + POLE_RADIUS * cos(angle), POLE_RADIUS * (sin(angle) - 1)); } glEnd(); //Draw the cylinder part glBegin(GL_QUAD_STRIP); for(int i = 0; i <= NUM_POLE_POINTS; i++) { float angle = 2 * PI * ((float)i - 0.5f) / (float)NUM_POLE_POINTS; glNormal3f(0, cos(angle), sin(angle)); float angle2 = 2 * PI * (float)i / (float)NUM_POLE_POINTS; glVertex3f(1 - BARRIER_SIZE, POLE_HEIGHT + POLE_RADIUS * cos(angle2), POLE_RADIUS * (sin(angle2) - 1)); glVertex3f(BARRIER_SIZE, POLE_HEIGHT + POLE_RADIUS * cos(angle2), POLE_RADIUS * (sin(angle2) - 1)); } glEnd(); glEndList(); }
The setupPole method makes a display list for displaying the cylindrical poles. It makes a cylinder in much the same way that setupBarriers does so.
void GameDrawer::step() { //Advance the game game->advance(STEP_TIME); //Advance the water waterTextureOffset += STEP_TIME / WATER_TEXTURE_TIME; while (waterTextureOffset > WATER_TEXTURE_SIZE) { waterTextureOffset -= WATER_TEXTURE_SIZE; }
In the step method, we start by calling the Game's advance method. Then, we change the waterTextureOffset field to move the water forward.
//Update animTimes, crabFadeAmounts, and isGameOver0 bool opponentAlive = false; for(int i = 0; i < 4; i++) { Crab* crab = game->crabs()[i]; if (crab != NULL) { oldCrabPos[i] = crab->pos(); } //Update animation time if (crab != NULL || crabFadeAmounts[i] > 0) { if (crab != NULL && crab->dir() != 0) { if (crab->dir() > 0) { animTimes[i] += STEP_TIME / WALK_ANIM_TIME; } else { animTimes[i] -= STEP_TIME / WALK_ANIM_TIME; } } else { animTimes[i] += STEP_TIME / STAND_ANIM_TIME; } while (animTimes[i] > 1) { animTimes[i] -= 1; } while (animTimes[i] < 0) { animTimes[i] += 1; } } //Update fade amount if (crab == NULL) { crabFadeAmounts[i] -= STEP_TIME / CRAB_FADE_OUT_TIME; if (crabFadeAmounts[i] < 0) { if (i == 0) { isGameOver0 = true; } crabFadeAmounts[i] = 0; } else if (i != 0) { opponentAlive = true; } } else if (i != 0) { opponentAlive = true; } } if (!opponentAlive) { isGameOver0 = true; } }
Now, we'll loop through the crabs to update oldCrabPos, crabFadeAmounts, animTimes, and isGameOver0. Each element of animTimes is increased a little if the corresponding crab is standing or walking in the positive direction and decreased a little if it's walking in the negative direction, to reverse the walking animation. Each element of crabFadeAmounts is decreased a little if the corresponding crab is NULL. isGameOver0 is set to true if either the human player's crab has disappeared or all of the other crabs have.
void GameDrawer::setupLighting() { GLfloat ambientLight[] = {0.2f, 0.2f, 0.2f, 1}; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambientLight); //Put one light above each of the four corners int index = 0; for(float z = 0; z < 2; z += 1) { for(float x = 0; x < 2; x += 1) { glEnable(GL_LIGHT0 + index); GLfloat lightColor[] = {0.2f, 0.2f, 0.2f, 1}; GLfloat lightPos[] = {x, 1.5f, z, 1}; glLightfv(GL_LIGHT0 + index, GL_DIFFUSE, lightColor); glLightfv(GL_LIGHT0 + index, GL_SPECULAR, lightColor); glLightfv(GL_LIGHT0 + index, GL_POSITION, lightPos); index++; } } }
The setupLighting method sets up ambient lighting and adds four lights above the four corners. This code uses a little trick: that GL_LIGHTn is the same as GL_LIGHT0 + n. For example, GL_LIGHT2 is equal to GL_LIGHT0 + 2.
void GameDrawer::drawCrabsAndPoles(bool isReflected) { if (crabModel != NULL) { glEnable(GL_NORMALIZE); for(int i = 0; i < 4; i++) { Crab* crab = game->crabs()[i]; //Translate and rotate to the appropriate side of the board glPushMatrix(); switch(i) { case 1: glTranslatef(0, 0, 1); glRotatef(90, 0, 1, 0); break; case 2: glTranslatef(1, 0, 1); glRotatef(180, 0, 1, 0); break; case 3: glTranslatef(1, 0, 0); glRotatef(270, 0, 1, 0); break; }
In drawCrabsAndPoles, we'll go through and draw all of the crabs and poles. First, we translate and rotate to the correct side of the board.
if (crab != NULL || crabFadeAmounts[i] > 0) { //Draw the crab glPushMatrix(); float crabPos; if (crab != NULL) { crabPos = crab->pos(); } else { crabPos = oldCrabPos[i]; } glTranslatef(crabPos, 0.055f, CRAB_OFFSET); if (crab == NULL) { //Used for the shrinking effect, whereby crabs shrink //until they disappear when they are eliminated from play glTranslatef(0, -0.055f * (1 - crabFadeAmounts[i]), 0); glScalef(crabFadeAmounts[i], crabFadeAmounts[i], crabFadeAmounts[i]); } glRotatef(-90, 0, 1, 0); glRotatef(-90, 1, 0, 0); glScalef(0.05f, 0.05f, 0.05f); if (crab == NULL || crab->dir() == 0) { crabModel->setAnimation("stand"); } else { crabModel->setAnimation("run"); } glColor3f(1, 1, 1); crabModel->draw(i, animTimes[i]); glPopMatrix(); }
Then, we draw the crab model, if the crab hasn't disappeared yet. Note that if crab is NULL, we'll scale the crab by crabFadeAmounts[i], in order to make the crab shrink when it's eliminated.
if (crab == NULL) { //Draw the pole if (isReflected) { glDisable(GL_NORMALIZE); } glCallList(poleDisplayListId); if (isReflected) { glEnable(GL_NORMALIZE); } } glPopMatrix(); } } }
If the crab has been eliminated, we draw a pole. If we're drawing the pole itself, rather than its reflection, then GL_NORMALIZE doesn't have to be enabled, so we disable it.
void GameDrawer::drawBarriers(bool isReflected) { if (isReflected) { glEnable(GL_NORMALIZE); } else { glDisable(GL_NORMALIZE); } glCallList(barriersDisplayListId); }
drawBarriers just calls the display list for the barriers, disabling GL_NORMALIZE if possible.
void GameDrawer::drawScores(bool isReflected) { float d = 0.1f; for(int i = 0; i < 4; i++) { ostringstream oss; oss << game->score(i); string str = oss.str(); glPushMatrix(); int hAlign; int vAlign; switch(i) { case 0: glTranslatef(0.5f, 0, -d); hAlign = 0; vAlign = -1; break; case 1: glTranslatef(-d, 0, 0.5f); hAlign = -1; vAlign = 0; break; case 2: glTranslatef(0.5f, 0, 1 + d); hAlign = 0; vAlign = 1; break; case 3: glTranslatef(1 + d, 0, 0.5f); hAlign = 1; vAlign = 0; break; } glTranslatef(0, 0.04f, 0); glRotatef(90, 1, 0, 0); glRotatef(180, 0, 1, 0); glScalef(0.1f, 0.1f, 0.1f); t3dDraw3D(str, hAlign, vAlign, 0.3f); glPopMatrix(); } }
The drawScores method draws the players' scores by translating to the appropriate position and drawing strings with each player's score using calls to t3dDraw3d.
void GameDrawer::drawBalls(bool isReflected) { if (isReflected) { glEnable(GL_NORMALIZE); } else { glDisable(GL_NORMALIZE); } glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); vector<Ball*> balls = game->balls(); for(unsigned int i = 0; i < balls.size(); i++) { Ball* ball = balls[i]; if (ball->fadeAmount() < 1) { glEnable(GL_BLEND); glColor4f(0.75f, 0.75f, 0.75f, ball->fadeAmount()); } else { glColor3f(0.75f, 0.75f, 0.75f); } glPushMatrix(); glTranslatef(ball->x(), ball->radius() + 0.01f, ball->z()); glutSolidSphere(ball->radius(), 10, 6); glPopMatrix(); if (ball->fadeAmount() < 1) { glDisable(GL_BLEND); } } }
The drawBalls method draws each of the balls. Note that if the ball is a little faded out, we're using alpha blending to draw a transparent ball. Also, note that the ball is drawn a little above the water. If they weren't, rounding errors might cause parts of the balls' reflections to appear above the water.
void GameDrawer::drawReflectableObjects(bool isReflected) { drawCrabsAndPoles(isReflected); drawBarriers(isReflected); drawScores(isReflected); drawBalls(isReflected); }
drawReflectableObjects just calls other drawing methods.
void GameDrawer::drawSand() { //The height of the sand above the water float height = 0.01f; glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, sandTextureId); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glDisable(GL_NORMALIZE); glColor3f(1, 1, 1); glNormal3f(0, 1, 0); glBegin(GL_QUADS); glTexCoord2f(1, 0); glVertex3f(0, height, 0); glTexCoord2f(1, 1); glVertex3f(0, height, 1); glTexCoord2f(0, 1); glVertex3f(1, height, 1); glTexCoord2f(0, 0); glVertex3f(1, height, 0); glEnd(); }
This method takes care of drawing the sand. Note that the sand is a little above the water, which is at z = 0, so that it isn't submerged by the water.
void GameDrawer::drawWater() {
glDisable(GL_LIGHTING);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, waterTextureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glDisable(GL_NORMALIZE);
glEnable(GL_BLEND);
glColor4f(1, 1, 1, WATER_ALPHA);
glNormal3f(0, 1, 0);
glBegin(GL_QUADS);
glTexCoord2f(200 / WATER_TEXTURE_SIZE,
-waterTextureOffset / WATER_TEXTURE_SIZE);
glVertex3f(-100, 0, -100);
glTexCoord2f(200 / WATER_TEXTURE_SIZE,
(200 - waterTextureOffset) / WATER_TEXTURE_SIZE);
glVertex3f(-100, 0, 100);
glTexCoord2f(0, (200 - waterTextureOffset) / WATER_TEXTURE_SIZE);
glVertex3f(100, 0, 100);
glTexCoord2f(0, -waterTextureOffset / WATER_TEXTURE_SIZE);
glVertex3f(100, 0, -100);
glEnd();
glDisable(GL_BLEND);
glEnable(GL_LIGHTING);
}
drawWater draws the water. We're disabling lighting on the water. As in the floor of the animation lesson, the water is a giant quadrilateral that's supposed to look like it extends forever in each direction, and the water is moved forward just by messing with the texture coordinates.
void GameDrawer::drawWinner() { if (!isGameOver0) { return; } glColor3f(0.2f, 0.2f, 1.0f); if (!waitingForFirstGame) { //Draw the winner string str; if (game->score(0) > 0) { str = "You win!"; } else { str = "You lose."; } glPushMatrix(); glTranslatef(0.5f, 0.05f, 0.5f); glScalef(0.1f, 0.1f, 0.1f); glTranslatef(0, 0.15f, 0); glRotatef(180, 0, 1, 0); glRotatef(-90, 1, 0, 0); t3dDraw3D(str, 0, 0, 0.3f); glPopMatrix(); }
drawWinner draws some text if the game is not in progress. If we've started a game already, and the game is over, we draw text indicating the winner by calling t3dDraw3D.
//Draw instructions glPushMatrix(); glTranslatef(0.5f, 0.05f, 0.35f); glScalef(0.05f, 0.05f, 0.05f); glTranslatef(0, 0.15f, 0); glRotatef(180, 0, 1, 0); glRotatef(-90, 1, 0, 0); t3dDraw3D("Press ENTER for a new game\nPress ESC to quit\n" "(Use left and right to move)", 0, -1, 0.3f); glPopMatrix(); }
If there is no game in progress, we'll draw some instructions.
Note that the instructions string spans multiple lines. You can do that in C++. C++ will interpret it as one string: "Press ENTER for a new game\nPress ESC to quit\n(Use left and right to move)".
void GameDrawer::draw() { //Set the background to be sky blue glClearColor(0.7f, 0.9f, 1.0f, 1); //Draw reflections glCullFace(GL_FRONT); glPushMatrix(); glScalef(1, -1, 1); setupLighting(); drawReflectableObjects(true); glPopMatrix(); //Draw normally glCullFace(GL_BACK); setupLighting(); drawWater(); drawSand(); drawWinner(); drawReflectableObjects(false); }
And now (drumroll please), we have our main drawing function. It sets the background color to sky blue. This color shows up in the reflection on the water. Then, we draw the reflections, by using glScalef(1, -1, 1) to reflect everything. Then, we draw everything normally.
Note that when we're drawing the reflections, we cull the front faces. That's because reflecting about the y axis changes the faces' vertices from being given in counterclockwise order to their being given in clockwise order.
void GameDrawer::advance(float dt) { while (dt > 0) { if (timeUntilNextStep < dt) { dt -= timeUntilNextStep; step(); timeUntilNextStep = STEP_TIME; } else { timeUntilNextStep -= dt; dt = 0; } } }
The advance method calls step the proper number of times.
void GameDrawer::setPlayerCrabDir(int dir) { playerCrabDir = dir; game->setPlayerCrabDir(dir); }
The setPlayerCrabDir method sets the playerCrabDir field and calls the setPlayerCrabDir on the game.
bool GameDrawer::isGameOver() { return isGameOver0; }
isGameOver simply returns the isGameOver0 field.
void GameDrawer::startNewGame(float maximumSpeedForOpponents, int startingScore) { setGame(new Game(maximumSpeedForOpponents, startingScore)); }
The startNewGame method starts a new game by setting the current game to be a new Game object.
void initGameDrawer() { t3dInit(); } void cleanupGameDrawer() { t3dCleanup(); }
As promised, initGameDrawer and cleanupGameDrawer just call t3dInit and t3dCleanup.
main.cpp
Let's go to main.cpp.
const float PI = 3.1415926535f; //The number of milliseconds between calls to update const int TIMER_MS = 25;
We have pi, as well as a constant indicating the number of seconds between calls to update. You can increase this constant if your computer is too slow to run at 40 FPS, so that at least the speed of the game will match the drawing speed. Ideally, in each call to update, we'd measure the amount of time since the last call to update and advance the game by that amount of time. That way, the speed of the game would automatically match the speed of the computer. But we're just doing things the easy way.
GameDrawer* gameDrawer; //Whether the left key is currently depressed bool isLeftKeyPressed = false; //Whether the right key is currently depressed bool isRightKeyPressed = false;
gameDrawer is the GameDrawer object we're using. isLeftKeyPressed and isRightKeyPressed will keep track of whether the left and right keys are currently being pressed.
//Starts at 0, then increases until it reaches 2 * PI, then jumps back to 0 and //repeats. Used to have the camera angle slowly change. float rotationVar = 0;
If you watch the program, you'll notice that the camera angle is actually changing very slowly. The rotationVar variable will be used to make this happen. rotationVar starts at 0, then climbs to 2 * PI, then jumps back down to 0 and repeats.
void cleanup() { delete gameDrawer; cleanupGameDrawer(); }
The cleanup function just has to delete gameDrawer and call cleanupGameDrawer.
void handleKeypress(unsigned char key, int x, int y) { switch (key) { case '\r': //Enter key if (gameDrawer->isGameOver()) { gameDrawer->startNewGame(2.2f, 20); } break; case 27: //Escape key cleanup(); exit(0); } }
The enter key, '\r', starts a new game if none is currently in progress.
void handleSpecialKeypress(int key, int x, int y) { switch (key) { case GLUT_KEY_LEFT: isLeftKeyPressed = true; if (isRightKeyPressed) { gameDrawer->setPlayerCrabDir(0); } else { gameDrawer->setPlayerCrabDir(1); } break; case GLUT_KEY_RIGHT: isRightKeyPressed = true; if (isLeftKeyPressed) { gameDrawer->setPlayerCrabDir(0); } else { gameDrawer->setPlayerCrabDir(-1); } break; } }
The left and right keys don't have ASCII equivalents, so we can't handle them in the handleKeypress function. We need a new function called "handleSpecialKeypress", which takes an integer key rather than a character key. A key of GLUT_KEY_LEFT indicates the left key, while GLUT_KEY_RIGHT indicates the right key. The function just updates isLeftKeyPressed and isRightKeyPressed and sets the direction the human player's crab is going.
void handleSpecialKeyReleased(int key, int x, int y) { switch (key) { case GLUT_KEY_LEFT: isLeftKeyPressed = false; if (isRightKeyPressed) { gameDrawer->setPlayerCrabDir(-1); } else { gameDrawer->setPlayerCrabDir(0); } break; case GLUT_KEY_RIGHT: isRightKeyPressed = false; if (isLeftKeyPressed) { gameDrawer->setPlayerCrabDir(1); } else { gameDrawer->setPlayerCrabDir(0); } break; } }
This function will be called whenever a special key is released. Like handleSpecialKeypress, this function will update isLeftKeyPressed and isRightKeyPressed and set the direction of the human player's crab.
void initRendering() {
glEnable(GL_DEPTH_TEST);
glEnable(GL_COLOR_MATERIAL);
glEnable(GL_LIGHTING);
glEnable(GL_NORMALIZE);
glEnable(GL_CULL_FACE);
glShadeModel(GL_SMOOTH);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
initGameDrawer();
}
This is the initRendering function.
void handleResize(int w, int h) { glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0, (double)w / (double)h, 0.02, 5.0); }
Note that in handleResize, we're indicating not to draw anything closer to the camera than 0.02 units or farther from the camera than 5 units.
void drawScene() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.5f, -0.3f, -1.8f); glRotatef(50, 1, 0, 0); glRotatef(180, 0, 1, 0); //This makes the camera rotate slowly over time glTranslatef(0.5f, 0, 0.5f); glRotatef(3 * sin(rotationVar), 0, 1, 0); glTranslatef(-0.5f, 0, -0.5f); gameDrawer->draw(); glutSwapBuffers(); }
In drawScene, we translate a little, then rotate by 50 degrees to have a camera angle between an overhead view (90 degrees) and a straight-on view (0 degrees). Then, we translate to the center of the board and rotate a little using rotationVar. The amount of rotation will oscillate slowly between -3 and 3. Then, we translate back to the corner of the board and call draw on the GameDrawer.
void update(int value) { gameDrawer->advance((float)TIMER_MS / 1000); rotationVar += 0.2f * (float)TIMER_MS / 1000; while (rotationVar > 2 * PI) { rotationVar -= 2 * PI; } glutPostRedisplay(); glutTimerFunc(TIMER_MS, update, 0); }
The update function advances the GameDrawer and increases the rotationVar variable a little, decreasing it if it exceeds 2 * PI.
int main(int argc, char** argv) { srand((unsigned int)time(0)); //Seed the random number generator //... gameDrawer = new GameDrawer(); //... glutSpecialFunc(handleSpecialKeypress); glutSpecialUpFunc(handleSpecialKeyReleased); //... }
The main function is mostly familiar stuff. We seed the random number generator and set gameDrawer to be a new GameDrawer object. However, there are two new function calls: glutSpecialFunc and glutSpecialUpFunc, which let GLUT know the functions that we want to use to handle when a special key is pressed or released.
And now, we're done! We've made a fairly cool 3D game, the culmination of all the OpenGL we've learned.
And they all lived happily ever after. THE END.
- Summary
- *
- Text version
- *
- Exercises
- *
- Download source