Creating Games in C++(c) A Step-by-Step Guide
Authors: Conger D., Little R.
Case Study: Floating-Point Numbers and Gamespaces
It's time to see floating-point numbers in action. To see how they work, you'll do a case study that simulates the firing of a cannon. This simulation is more like real games than the example programs shown so far because it uses floating-point numbers for all of its calculations. The objects in the simulation will also be positioned using floating-point numbers.
As you've already seen, computer screens use integers rather than floating-point numbers. So if all of the simulation's calculations and object positions are in floating-point numbers, how do you connect that to integer locations on the screen?
The answer is that you use a gamespace.
What is a Gamespace?
A gamespace is a virtual world that all of a game's objects exist in. The gamespace can be any size . It can use any unit of measure. So, for instance, programmers who like the English system can use inches, feet, yards, and miles. Programmers who prefer the metric system can define their gamespaces using centimeters, meters , and kilometers.
In real games, gamespaces are almost always defined using floating-point numbers. This enables them to do much more accurate and realistic physics calculations. In the case study of the cannon, the program must perform calculations that simulate the movement of a cannonball. If it were to use only integers, it would probably not look very realistic because of inaccuracies in the physics calculations. Instead, the cannon simulation creates a gamespace that uses vectors containing floating-point numbers. This enables more accurate calculations than integer vectors.
When your game uses a gamespace, it must convert the locations of objects from floating-point game coordinates into integer screen coordinates. This is illustrated in Figure 9.4 .
Figure 9.4. The gamespace on the left contains the game's objects. The images of the objects are drawn in the screen space at the right.
Figure 9.4 shows that the gamespace for the cannon simulation is 10,560 feet (2 miles) wide. The screen resolution is set so that the screen is 800 pixels wide. The position of the cannonball that's in flight is defined in the gamespace with floating-point numbers. The same is true of the cannon's position. During each frame, the position of the cannon and cannonball must be converted to integer screen coordinates so that the cannon and the ball can be rendered at the correct locations on the screen.
Another common name for game coordinates is world coordinates because the gamespace can be called a world.
To convert from gamespace coordinates to screen space coordinates, you use the formulas shown in Figure 9.5 .
Figure 9.5. Converting coordinates from the gamespace to the screen space.
In the equations in Figure 9.5, the term x SS stands for the x value in the screen space. The term x GS is the x value in the gamespace. Likewise, y SS and y GS represent the y values in the screen space and the gamespace, respectively.
As the first equation in Figure 9.5 shows, you convert x values in the gamespace to screen space by dividing the width of the screen in pixels by the width of the gamespace. In our cannon example, the height of the gamespace is in feet (10,560.0 feet to be exact). Next , you multiply the result by the x coordinate in the gamespace.
Similarly, you convert y values in the gamespace to screen space by dividing the height of the screen in pixels by the width of the gamespace. In the cannon example, the width of the gamespace is 7920.0 feet. You multiply the result of the division by the y value in the gamespace.
I've defined the gamespace for the cannon simulator in English units (feet). However, if you're serious about game programming, I strongly encourage you to become familiar with the metric system if you're not already. You'll find that almost all physics calculations are easier using the metric system.
Converting from screen space coordinates to gamespace coordinates uses the formula in Figure 9.6 .
Figure 9.6. Converting coordinates from the screen space to the gamespace.
As with the equation in Figure 9.5, the term y SS in Figure 9.6 stands for the y value in the screen space. The term y GS is the y value in the gamespace. The terms x SS and x GS represent the x values in the screen space and the gamespace.
Writing a Vector Class that Uses Floating-Point Numbers
Most gamespaces define the positions and movements of objects using floating-point numbers for greater accuracy. This requires the use of vectors containing floating-point numbers. LlamaWorks2D provides a floating-point vector class called vectorf Listing 9.1 gives the class definition for the vectorf class.
Listing 9.1. The vectorf class
As you can see, the vectorf class is exactly the same as the vector class, except that it uses floating-point numbers rather than integers.
Cannonshoot: A Gamespace in Action
With an introduction to gamespaces and the vectorf class, you're ready to use floating-point numbers in a game-like simulation. As mentioned previously, this program simulates a cannon firing a cannonball. For the physics calculations that determine where the cannonball is and how it moves, the program uses floating-point numbers and vectors containing floating-point numbers.
Introducing the Cannonshoot Class
We'll call this program CannonShoot. It consists of a game class, named cannonshoot , that is stored in a header file called CannonShoot.h. The functions for the cannonshoot are in the file CannonShoot.cpp.
Listing 9.2 gives the definition of the cannonshoot class. This class resembles the game classes for programs in previous chapters. By now, lines 112 of Listing 9.2 should look quite familiar.
You'll find the source files for the CannonShoot program on the CD in the folder \Source\Chapter09\Prog_09_01. There's an executable version of the program in the folder \Source\Chapter09\Bin. To play the simulation, press the A key to arm the cannon and the F key to fire. Press Q to quit.
Listing 9.2. The cannonshoot class
There are a couple of noteworthy items in Listing 9.2. First, the cannonshoot class contains a function called GameCoordsToScreenCoords() . This function converts vectors in the gamespace to vectors in the screen space. Using vectors enables this function to convert both x,y coordinates and velocity vectors to values in the screen space. Figure 9.7 demonstrates why.
Figure 9.7. Points can be specified using vectors or x,y coordinates.
In Figure 9.7, the point P is specified as a pair of x,y coordinates and as a vector. The vector p points from the origin of the gamespace to the point P. The x and y components of vector p are the same as the x,y coordinates of point P. This works for screen space as well. No matter what coordinate space you're using, you can specify a point as a vector.
The ability to specify points as vectors means that the CannonShoot program can call the GameCoordsToScreenCoords() function to convert either velocity vectors or points into their corresponding screen space values.
Another noteworthy aspect of the cannonshoot class is the fact that this game class is in a header file rather than the .cpp file that implements its member functions. Various functions in the game call the GameCoordsToScreenCoords() function. Therefore, to make GameCoordsToScreenCoords() available to all of the game's functions, it's put into a .h file that can be included in any of the game's .cpp files.
Your game code will usually be simpler if you define all of your game's objects in the gamespace.
Notice also that the cannonshoot class contains an object of type cannon and one of type sprite . The cannon object is, of course the cannon that this program simulates. The sprite object represents the ground. There's a potential problem here. The x,y coordinates of the cannon object are defined in the gamespace. The x,y coordinates of the sprite object are defined in screen space. I wrote the simulation this way to demonstrate that it is possible to use both spaces in your game class. Even though this technique is all too common in games, I strongly urge you not to use it. If your game is very complex, having some objects in screen space and others in the gamespace can get very confusing. Also, it typically forces your game to do multiple conversions of coordinates back and forth between the two spaces. These are unnecessary if you just define all of your objects in the gamespace.
Implementing the Cannonshoot Class
The implementation of the cannonshoot class is in the file CannonShoot.cpp. The functions in this file fall generally into three categories: object initialization, frame update and rendering, and coordinate conversion.
Initializing a cannonshoot object
Initializing a cannonshoot object is similar to initializing the game classes in previous chapters. However, there are some important differences, as Listing 9.3 illustrates.
Listing 9.3. The OnAppLoad and InitGame() functions of the cannonshoot class
After including the files it needs, CannonShoot.cpp creates a cannonshoot game object on line 5. It also declares its message map on lines 79.
The OnAppLoad() function begins on line 11 of Listing 9.3. Notice that this simulation only supports screen resolutions of 800x600. It can use either 32 or 24 bits of color . However, it forces the number of pixels to 600 rows of 800 per row. The conversion of gamespace to screen coordinates is much easier if both the gamespace and the screen space have fixed sizes.
The InitGame() function, which starts on line 32, sets the size of the gamespace on lines 3637. The gamespace is 10,560 feet wide (2 miles) and 7,920 feet tall. I chose a width of 2 miles rather arbitrarily. The height is based on the width. I used the proportions of the screen space to find the height of the gamespace. If you divide the height of the screen space by its width, you get a ratio (3/4, in case you're interested). I set the height of the gamespace so that it has the same ratio as the screen space. That is, if you divide 7,920 by 10,560, you get 3/4.
Making the gamespace and screen space proportional to each other is not required for your game. However, it makes the game's calculations easier to program. If they are not proportional, you have to scale all of your points and vectors in the gamespace so that they will fit properly in the screen space.
On lines 4145 of Listing 9.3, the InitGame() function loads the bitmap for the ground. If it loads properly, InitGame() sets the ground's position on lines 4950. Remember, the x and y values shown here are in screen space.
Next, the InitGame() function loads the cannon's image. As you'll see later in this chapter, the cannon comes complete with a cannonball (no extra charge). Therefore, when InitGame() loads the cannon's image, it also loads the cannonball's image.
On lines 5758, InitGame() sets the position of the cannon in the gamespace. The origin of the gamespace is in the upper-left corner, exactly the same as the screen space. I did that to keep the coordinate conversion process simple. Most games set the origin of their gamespaces to the lower-left corner. As a result, they have to invert all the y values in the game when they draw things to the screen. That isn't hard to doI just didn't want to throw too much at you at once.
After InitGame() positions the cannon, it loads the sounds the cannon uses and then returns.
Updating and Rendering a Frame
When the cannonshoot object updates a frame of animation, it doesn't actually do much. Most of the work is accomplished by the cannon and cannonball classes. Rendering is equally straightforward, as shown in Listing 9.4 .
Listing 9.4. The UpdateFrame() , RenderFrame() , and OnKeyDown() functions for the cannonshoot class
The UpdateFrame() function, which begins on line 1 of Listing 9.4, first tells the cannon to update itself. Updating the cannon also updates the cannonball. However, the simulation must determine whether or not the cannonball hit the ground. On line 5, the UpdateFrame() function calls the cannon::CannonBall() function to obtain a reference to the cannonball object. Recall that functions normally return copies of objects. When a function returns a reference, it returns the actual object rather than a copy. The UpdateFrame() function needs a reference to the cannon's cannonball object because it must change the status of the cannonball if the cannonball hit the ground.
On lines 67, UpdateFrame() uses an if statement to see if the ball is in flight and it hit the ground. If the ball hit the ground, the UpdateFrame() function calls the cannon::BallHit() function. As you'll see shortly, the BallHit() function plays a sound and erases the cannonball when the ball hits the ground.
Updates occur in this simulation whenever the user presses particular keys on the keyboard. The OnKeyDown() function responds to those keystrokes. It begins on line 28 of Listing 9.4. When the user presses the A key on the keyboard, it arms the cannon. Lines 3641 show that the OnKeyDown() function generates a random number between 0.0 and 1.0. It then passes that number to the cannon class's ArmCannon() function. This number is actually a percentage. If, for instance, the number is 0.5, the cannon fires the cannonball with 50% of its maximum charge. If the number is 0.75, it fires the ball with 75% of the maximum charge, and so forth.
The cannon fires when the user presses the F key. If the user presses Q, the simulation ends.
Rendering, as usual, is extremely easy. The Render() function, beginning on line 16, simply calls the Render() function for the sprite class to draw the ground. It then invokes the Render() function for the cannon class to draw the cannon and the cannonball.
Converting Floating-Point Gamespace Coordinates to Integer Screen Coordinates
The final function in CannonShoot.cpp is used to convert coordinates in the gamespace to screen space coordinates. It's shown in Listing 9.5 .
Listing 9.5. The cannonshoot class's GameCoordsToScreenCoords() function
This function uses the formulas presented back in Figure 9.5. In both calculations, the GameCoordsToScreenCoords() function uses the C++ (int) statement to tell the compiler to convert the answers from floating-point numbers to integers. It does this by lopping off everything in the answer to the right of the decimal point.
Creating a Cannonball
To help make the cannon simulator easier to present, I should discuss the cannonball class before the cannon class. Although the order of things may seem counterintuitive, it simplifies the presentation quite a bit.
First, I'll cover the design of the cannonball class. Because we're now working with gamespaces, the design of the cannonball class is not as simple as you might think. After examining the design of the cannonball class, I'll discuss how it's implemented.
Design Considerations for the Cannonball Class
Because the cannon simulator defines its objects in a gamespace rather than screen space, designing the cannonball class is different than the example programs of previous chapters. I'll illustrate these differences by presenting the cannonball class in Listing 9.6 .
Listing 9.6. The cannonball class
The first and most important difference is that the cannonball class does not use inheritance. Instead of being derived from the sprite class, the cannonball class contains a sprite object. The definition of the cannonball's sprite object is shown on line 43 of Listing 9.6.
You may ask why the cannonball class does not use inheritance. If the cannonball class was derived from the sprite class, programs could call all of the sprite member functions using a cannonball object. That would enable a program to directly set the location of a cannonball object in screen space. Because cannonball objects keep track of where they are in both the gamespace and screen space, it would be possible for the object to think it's in two different places at once! You prevent that by avoiding inheritance for game objects that are defined in a gamespace.
If a game's object is defined in a gamespace, that object should contain a sprite object. It should not inherit from the sprite class.
Another difference between previous game objects and the cannonball class is that it defines its own versions of many of the functions in the sprite class. Avoiding inheritance means that programs can't use a cannonball object to call sprite functions. Therefore, the cannonball class must define its own versions of them, as shown in Listing 9.7 .
Listing 9.7. The inline functions for the cannonball class
Listing 9.7 shows only the inline functions for the cannonball class. All of these functions except the overloaded X() and Y() functions call the equivalent functions from the sprite class. The X() and Y() functions perform their operations on data members that are defined in the cannonball class. If you look back at Listing 9.6, you'll see that the x and y members defined on line 45 are of type float . The cannonball class uses these data members to store the cannonball's position in the gamespace. It uses the x and y members defined in the sprite class to keep track of where the cannonball's image is in screen space.
Before we move on I want to point out one more feature of the cannonball class. In Listing 9.6, you can see that not only does the cannonball class define its own x and y members, it also defines its own velocity vector. As you can see from line 44 of Listing 9.6, the velocity vector is of type vectorf , which is a vector that contains floating-point numbers. The cannonball class uses this data member to keep track of the cannonball's movement in the gamespace.
Simulating a Cannonball in Flight
It's time for a bit of floating-point math and physics. Specifically, the cannonball class must simulate how a cannonball moves when it's shot through the air. The trajectory of a cannonball is a parabola. Figure 9.8 displays what this looks like.
Figure 9.8. A cannonball moves in a parabolic trajectory.
Figure 9.8 demonstrates that the effect of gravity on the cannonball in flight is to bend the ball's trajectory from a straight line into a parabola. So the question is, do you know how to solve parabolic equations?
Actually, you don't need to know math that advanced for this simulation. Parabolic equations aren't necessary. Instead, you can use simple vector addition. Figure 9.9 shows how.
Figure 9.9. Using vector addition to calculate the cannonball's path .
Figure 9.9 shows the cannonball in four different positions along its trajectory. The velocity vector always points straight ahead in the direction the ball is moving. This is the upper vector shown for each position of the ball. Gravity points down. To simulate the effect of gravity, all your game has to do is add a gravity vector to the ball's velocity vector at each position along the trajectory. The result is a new vector that becomes the cannonball's actual course. This is the lower vector shown for each position of the ball.
Repeatedly adding the gravity vector causes the course of the ball to change. To be specific, it bends the trajectory into a parabola. You can use this technique to simulate the effect of gravity on any projectile close to the earth's surface. It works for cannonballs, arrows, bullets, baseballs, or whatever else you want to simulate. The exception to this is if the object is powered , as rockets are. That calls for more complex math and physics. But for the simple objects that many games simulate, you can use vector addition to calculate their courses. Listing 9.8 demonstrates how the CannonShoot program uses this technique to simulate the movement of the cannonball.
Listing 9.8. The cannonball::Move() function
The function in Listing 9.8 is called each time the cannon is updated. It defines a variable of type vectorf on line 4. The definition uses the C++ keyword static to keep this variable around for as long as the program runs. Recall that variables in functions go away as soon as the function ends. If the function gets called again, its variables are re-created. When you add the keyword static to a variable declaration, it tells the program that this variable should continue to exist after the function ends. However, no other function can access it.
The first time the Move() function in Listing 9.8 is called, it initializes the variable gravity by setting its x component to 0.0 and its y component to 32.0. This creates a floating-point vector that points straight down at 32 feet/second/second (32 ft/s 2 ). When this function is called again, it does not have to do the initialization again because the variable gravity is declared static .
If the cannonball has been fired from the cannon, the statement on line 8 adds the gravity vector to the velocity vector. Lines 1011 then use the velocity vector to calculate the next position of the cannonball in the gamespace. On lines 1314, the Move() function calls the cannonshoot::GameCoordsToScreenCoords() function to convert the ball's position into coordinates. Lines 1617 call the sprite::X() and sprite::Y() functions to set the position of the cannonball's image on the screen.
If you're using the metric system (a good idea) rather than English units, the downward acceleration of gravity is 9.8 meters/second/second (9.8 m/s 2 ). Therefore, you set the y component of your gravity vector to 9.8.
The cannonball class's ballInFlight member, which is tested on line 6, is set in one of the cannonball class's two overloaded Velocity() functions. Listing 9.9 gives the code for both of them.
Listing 9.9. The cannonball class's Velocity() functions
The CannonShoot program calls the first Velocity() function shown in Listing 9.9 to set the cannonball's velocity. In addition to setting the velocity vector, which is named velocity , the Velocity() function uses an if statement beginning on line 8. The if statement checks to see if the velocity is nonzero. It does this by calling the vectorf class's MagnitudeSquared() function rather than its Magnitude() function. The MagnitudeSquared() function is more efficient because it doesn't have to find any square roots. This doesn't affect the accuracy of the test. If the velocity is not zero, then the square of the velocity is also not zero. So if the test on line 8 evaluates to TRue , then the statement on line 10 sets the variable ballInFlight to TRue . In other words, if the cannonball is moving, it's in flight. If it's sitting still, it's not.
If the cannonball hits something, such as the ground, it should react to that event. That reaction is shown in Listing 9.10 .
Listing 9.10. The cannonball::BallHit() function
In this simulation, the cannonball stops moving if it hits something. Therefore, the BallHit() function sets its velocity vector to zero. It also plays a thumping sound and sets the ballInFlight member to false .
That's really all there is to the workings of the cannonball class. Let's move on and examine how the cannon class operates.
Creating a Cannon
Like the cannonball, the cannon is defined in the gamespace. As a result, it does not inherit from the sprite class. Also like the cannonball class, the cannon class contains members called x and y that keep track of the cannon's position in the gamespace. Here's the definition of the cannon class.
Listing 9.11. The cannon class
Now that you've seen the cannonshoot and cannonball classes, the cannon class should be straightforward. It provides functions for loading the images of the cannon and cannonball, as well as loading the sound associated with the cannon. In addition, it has overloaded X() and Y() functions that get and set the x and y components of the cannon's position in the gamespace.
The cannon::ArmCannon() function was discussed earlier, when I presented the cannonshoot::OnKeyDown() function. It sets the cannon class's charge member, which represents the amount of gunpowder in the cannon. The charge member must contain a value greater than 0.0 and less than or equal to 1.0. It is a percentage of the maximum velocity the cannonball can attain. To get the maximum velocity, the program calls the cannon::BallMaxVelocity() function. Listing 9.12 presents the code for this function.
Listing 9.12. The cannon::BallMaxVelocity() function
The only thing the BallMaxVelocity() function does is return the value 2500.0. This is a technique for defining constants that go with classes. Recall that previous chapters demonstrated that you can also use statements such as enum or const to define constants. However, the enum statement is for groups of related constants and the const statement can't be used inside a class definition. An easy way to define constants that have nothing to do with each other is to make them return values of inline functions. Because the compiler performs code substitutions for inline functions, it adds no overhead to define constants this way.
In addition, defining constants as the return value of a function makes it easy to define constants that are objects. For example, the cannon class contains a function called BallStartPosition() . The code for this function appears in Listing 9.13 .
Listing 9.13. The cannon::BallStartPosition() function
Listing 9.13 shows that the BallStartPosition() function always returns the same value. The value it returns is of type pointf . The pointf class is provided by LlamaWorks2D and represents a point in any 2D space. It contains floating-point numbers. So each time that the program calls the BallStartPosition() function, the BallStartPosition() function returns the starting position of the cannonball. The starting position is relative to the upper-left corner of the cannon in the gamespace.
Arming and Firing the Cannon
Before the cannon can fire a cannonball, the CannonShoot program must call the cannon::ArmCannon() function. This sets the initial velocity of the cannonball.
After the cannon is armed, it can be fired by calling the cannon::FireCannon() function. Listing 9.14 provides the code for both of these functions.
Listing 9.14. The functions for arming and firing the cannon
The ArmCannon() function uses assertions on lines 45 of Listing 9.14. The assertions crash the program if the value of the powderCharge parameter is not correct. The powderCharge parameter must contain a value that is greater than 0.0 and less than or equal to 1.0. If it does not, it is a programmer error that should be caught before the game is released. Assertions make it easy to catch the error because a program crash is hard to miss .
If the assertions indicate that the value of the powderCharge parameter is correct, ArmCannon() sets the charge member. It also sets the cannonArmed member to true before it exits.
The FireCannon() function starts on line 13 of Listing 9.14. If the cannon is armed, it sets the starting position of the cannonball on lines 1720. Recall that the starting position of the ball is relative to the upper-left corner of the cannon. Therefore, it must be added to the cannon's current position. That's done on lines 1920.
On line 25 of Listing 9.14, the FireCannon() function gets a unit vector that points in the direction that the cannon is aimed. You may remember from chapter 5, "Function and Operator Overloading," that a unit vector is a vector of length 1. The cannonball's maximum velocity is a floating-point number (also called a scalar). By multiplying the maximum velocity by the unit vector (see line 26), the FireCannon() function gets a vector that points in the direction the cannon is aimed and is set to the cannonball's maximum velocity. Line 26 also multiplies the result by the value in charge . Because charge contains a percentage, this multiplication scales the vector's magnitude to a percentage of the maximum velocity. On line 27, the FireCannon() function sets the cannonball's resulting velocity.
Next, the FireCannon() function plays the sound of the cannon firing on line 29. If there is an error when the sound is played , FireCannon() tHRows an exception. This is a way of indicating an error.
Before exiting, the FireCannon() function sets cannonArmed to false so the cannon cannot be fired again until it is rearmed. It also sets charge to 0.0 for the same reason.
Updating and Rendering a Cannon
Because the cannonball class does so much of the work during a frame, updating and rendering the cannon is fairly simple. Listing 9.15 gives the code for the cannon::Update() and cannon::Render() functions.
Listing 9.15. The functions for updating and rendering a cannon
Listing 9.15 shows that each time the cannon is updated, it sets the position of its image in screen space. If the cannonball has been fired, the cannon also moves the cannonball.
When it is time to render the cannon, the cannon first renders its own image. If the cannonball is in flight, the cannon also renders the cannonball.
Creating Games in C++(c) A Step-by-Step Guide
Authors: Conger D., Little R.