A Game Called Ping


It's time to put all the information presented in this book so far into practice by writing Ping. As you may have guessed, this game is a knockoff of Pong, the granddaddy of all video games. Chances are fairly good that you're not old enough to remember Pong, but I am. I remember being rather excited over it when it first came out. When kids today see it, they usually just laugh at how sad it is compared to today's games. Nevertheless, our games still do all of the same basic tasks that Pong did. Therefore, writing Ping provides a straightforward introduction to building games.

Introducing Ping

Ping is a simple two-player game modeled on ping-pong. The ball is served randomly to the left or right player. When the ball moves toward a player's paddle, the player moves the paddle up or down to block the progress of the ball. If the ball hits the paddle, it bounces back toward the other side. If not, it passes off the edge of the screen. When it leaves the screen, the player on the opposite side scores a point. Whenever a player scores a point, a small red dot appears on that player's side of the screen near the bottom. If the ball hits the top or bottom of the area of play, it bounces.

Now that we've defined the rules of the game, let's build it.

Writing Ping

Implementing Ping takes a surprising amount of code. As a result, I'll show pieces of the game in several code listings to make it all a bit more comprehensible.

The game Ping has three classes in it. The first is the game class. It, and its member functions, are stored in the file Ping.cpp. In addition, Ping has a ball class and a paddle class. There are a pair of files for each of these two classes.

The major steps that Ping must perform when it starts are:

  • Create the game's objects.

  • Initialize the program.

  • Initialize the game.

  • Initialize a level.

  • Create the message map.

  • Update a frame.

  • Render a frame.

  • Clean up a level.

  • Clean up the game.

Creating the Game's Objects

The game object for Ping is defined in the class ping in the file Ping.cpp. This class is only moderately more complex than the my_game class in Listing 4.3 in chapter 4. Listing 7.1, which gives the first portion of Ping.cpp, shows the definition of the ping class.

Listing 7.1. The ping class and its message map

 1    #include "LlamaWorks2d.h" 2 3    using namespace llamaworks2d; 4 5    #include "Ball.h" 6    #include "Paddle.h" 7 8    class ping : public game 9    { 10   public: 11       bool OnAppLoad(); 12       bool InitGame(); 13       bool InitLevel(); 14       bool UpdateFrame(); 15       bool RenderFrame(); 16 17       ATTACH_MESSAGE_MAP; 18 19       bool OnKeyDown( 20           keyboard_input_message &theMessage); 21 22       bool BallHitPaddle(); 23 24       void SetBallStartPosition(); 25       void SetBallStartDirection(); 26 27       bool DoLevelDone(); 28       bool DoGameOver(); 29 30   private: 31       ball theBall; 32       paddle leftPaddle; 33       paddle rightPaddle; 34       sprite theTrophy; 35 36       sprite scoreMarker; 37       int player1Score; 38       int player2Score; 39 40       bitmap_region areaOfPlay; 41   }; 42 43 44   CREATE_GAME_OBJECT(ping); 45 46   START_MESSAGE_MAP(ping) 47       ON_WMKEYDOWN(OnKeyDown) 48   END_MESSAGE_MAP(ping) 

The first thing to notice is that the #include statements for Ball.h and Paddle.h on lines 56 both appear after the using statement on line 3. This is because they both use the llamaworks2d namespace. Many C++ programmers do not like this style. They prefer to keep all of their #include statements together. Generally, I agree that is a better idea. If that is your preference as well, you can copy the using statement on line 3 to the beginning of Ball.h and Paddle.h. I showed this style of definition in this program because you will encounter it when looking at code written by other game programmers.

The ping class has 11 member functions. Several of them are required by the game engine. As you might expect, the engine provides reasonable default functions if you don't write your own.

The functions OnAppLoad(), InitGame(), InitLevel(), UpdateFrame(), RenderFrame(), DoLevelDone(), and DoGameOver() are all required member functions that the ping class inherits from the llamaworks2d::game class. The ping class overrides the versions of these functions it inherits so that they can be customized for this game.

On line 17, the ping class attaches its message map. The prototype for its one and only message-handling function is given on lines 1920. Because of the message map, which appears on lines 4648, the game engine automatically calls the OnKeyDown() function whenever a player presses a key on the keyboard. I'll discuss this in more detail shortly.

The three functions on lines 2225 are called by the other functions in Ping.cpp. They are used to implement some of the game logic.

Lines 3040 of Listing 7.1 show the private member data for the ping class. The first three are ball and paddle objects. I'll show the definition for these in a moment. Line 34 declares a sprite member variable called theTrophy. You saw how to use sprite objects in chapter 4. When a player wins the game, it displays a trophy on the winner's side of the screen.

Tip

You'll find detailed information on Windows messages on the Web at www.msdn.microsoft.com.


The ping class also uses a sprite object for its score markers. Ping displays them across the bottom of the screen to show the player's current score. The scores themselves are kept in the member variables defined on lines 3738. Lastly, the ping class defines a variable that specifies the portion of the screen that it uses to play the game.

On lines 4648, the ping class defines its message map. The purpose of the message map is to connect specific messages sent by Windows to functions in the game. For example, the macro on line 47 connects the Windows message WM_KEYDOWN to the OnKeyDown() function. There are literally hundreds, if not thousands, of different messages that Windows can send to your game.

Factoid

Packaging data into an easy-to-use object is often called "cooking" the data. LlamaWorks2D takes "raw" input messages and "cooks" for you.


Windows sends your game a WM_KEYDOWN message whenever the player presses a key on the keyboard. The message contains an identifier for the key that was pressed. These identifiers are defined by LlamaWorks2D in the file LW2DInputDevice.h.

When Windows sends a WM_KEYDOWN message to your game, it sends the message in a very "raw" form. Those new to Windows programming often find it hard to extract the information they need from the message. To make things easier, LlamaWorks2D automatically packages the message into a keyboard_input_message object, which is also defined in LW2DInputDevice.h. Your game uses the member functions of the keyboard_input_message class to extract the message's information.

Because input from the WM_KEYDOWN message is in a very raw form, it does not, for instance, send your game an 'A' or 'a' character when the player presses the A key. Instead, it gets the key code KC_A, which is defined by LlamaWorks2D. Microsoft calls this a virtual key code. You can find more information on virtual key codes at www.msdn.microsoft.com.

If you want your game to receive characters, rather than key codes, LlamaWorks2D helps you out. It defines a special macro called ON_WMCHAR() that connects the Windows WM_CHAR message to a function. The WM_CHAR message provides characters rather than key codes.

You may wonder why it's worth bothering to use key codes rather than characters. The answer is that many keystrokes are not in the set of characters that generate a Windows WM_CHAR message. For example, the up and down arrows do not generate a WM_CHAR message. They only generate WM_KEYDOWN messages. In general, games respond to the WM_CHAR message when the player is typing in a string of characters. During gameplay, they use WM_KEYDOWN messages instead.

One word of warning before we move on: LlamaWorks2D does not let you put the macros ON_WMKEYDOWN() or ON_WMCHAR() more than once in a message map. You can actually write them in more than once and your program will compile without an error. However, only the first use of these macros will work.

Mapping Other Windows Messages

LlamaWorks2D enables you to connect, or map, any Windows message to any function you want. To make this happen, use the LlamaWorks2d ONMESSAGE() macro. For example, if you want to map the message WM_ACTIVATE to a function in your game class named OnActivate(), you can do so by putting the following statement in your game's message map:

 ONMESSAGE(WM_ACTIVATE, OnActivate) 


This statement tells the game engine to automatically call the OnActivate() function any time the game receives a WM_ACTIVATE message.

The first parameter to the ONMESSAGE() macro must always be the Windows message you want map. The second parameter is the name of the function to map the message to.

Warning

It is possible to set a variable of an enumerated type to a value other than the ones specified in the enumeration. For instance, there is a way to set a bounce_direction variable to 10. However, you should not do this because it can cause unexpected results in programs. LlamaWorks2D is a good example. It expects all variables of type bounce_direction to be assigned one of the values on the list on lines 910 of Listing 7.2. If you manage to assign it a value not in the list, LlamaWorks2D will not know what to do with that value. It is likely to cause the game engine to crash.


In addition to its game class, Ping must create a ball object and two paddle objects. Both the ball and paddle classes are derived through inheritance from the llamaworks2d::sprite class. Listing 7.2 provides the code for the ball class.

Listing 7.2. The ball class

 1    #ifndef BALL_H 2    #define BALL_H 3 4    class ball : public sprite 5    { 6    public: 7        enum bounce_direction 8        { 9            X_DIRECTION, 10           Y_DIRECTION 11       }; 12   public: 13       void Bounce( 14       bounce_direction dir); 15   }; 16 17   #endif 

The ball class is extremely simple because the sprite class does most of the actual work. The only thing the ball class really needs to do is to bounce the ball whenever the program tells it to. Everything else the ball class needs to do is handled by the sprite class.

On 711 of Listing 7.2, the ball class defines an enumerated type. C++ provides enumerated types as a way of creating a type that can only be set to a specific set of values. It also lets you define what the values are that variables of that type can be set to. In this case, the name of the type is bounce_direction. Variables of type bounce_direction can only be set to the values X_DIRECTION and Y_DIRECTION.

The bounce_direction type is used for the parameter of the public member function Bounce(). The prototype for Bounce() is on lines 1314. The code for the Bounce() function appears in Listing 7.3.

Listing 7.3. Bouncing the ball

 1    void ball::Bounce( 2        bounce_direction dir) 3    { 4        vector ballVelocity = Movement(); 5        switch (dir) 6 { 7            case X_DIRECTION: 8               ballVelocity.X(ballVelocity.X() * -1); 9            break; 10 11           case Y_DIRECTION: 12               ballVelocity.Y(ballVelocity.Y() * -1); 13           break; 14      } 15      Movement(ballVelocity); 16    } 

The Bounce() function in Listing 7.3 begins by calling the sprite::Movement() function to get the ball's movement vector. It uses a switch statement with two cases. Bounce() executes the case statement on lines 79 whenever the parameter dir is equal to X_DIRECTION. That case statement bounces the ball in the x direction by multiplying the x component of its movement vector by -1. So if the ball was moving left, it will move right after the statement on line 8 gets executed. If it was moving right, it will move left after the multiplication on line 8.

Factoid

Programmers often say that classes like ball are a specialization of sprite. A sprite is a generic concept. Many things can be sprites. On the other hand, a ball is a specific thing. In a game, a ball is a specific or special type of sprite. Base classes are often generic, while derived classes are typically specializations.


If the dir parameter is equal to Y_DIRECTION, Bounce() executes the case statement on lines 1113. The statement on line 12 bounces the ball in the y direction.

That's all there is to the ball class. Inheritance makes it remarkably simple.

Like the ball class, the paddle class derives from sprite. The primary difference between a sprite and a paddle is that paddles in Ping can only move up and down. Listing 7.4 illustrates this difference.

Listing 7.4. The paddle class

 1    #ifndef PADDLE_H 2    #define PADDLE_H 3 4 5    class paddle : public sprite 6    { 7    public: 8        bool Move(); 9        void Movement( 10       vector direction); 11       vector Movement(); 12   }; 13 14   inline vector paddle::Movement() 15   { 16       return (sprite::Movement()); 17   } 18 19   #endif 

As this listing shows, the paddle class has three member functions that control how paddles move. The Move() and Movement() functions, whose prototypes appear on lines 811, override the same functions in the sprite class. The code for one of the Movement() functions is given on lines 1417. All it does is call the corresponding function in the sprite class. Since the sprite class already provides a Movement() function that gets the movement vector, you may wonder why it's included at all. It's there because of the compiler. The Dev-C++ compiler, which is based on the Gnu C++ compiler (GCC), requires that if you override one overloaded function, you must overload all functions of the same name. The paddle class must override the Movement() function that sets the movement vector (the one whose prototype is on lines 910). Because it overrides that version of Movement(), it must override all versions of Movement() in the sprite class. The sprite class contains two Movement() functions, so the paddle class must contain either zero or two Movement() functions.

Note

Not all compilers require that you override all base class functions of the same name.


The code for the rest of the paddle class's member functions is given in Listing 7.5.

Listing 7.5. Controlling a paddle's movement

 1    bool paddle::Move() 2    { 3        bool moveOK = true; 4 5    vector velocity = Movement(); 6    Y(Y() + velocity.Y()); 7    velocity.Y(0); 8    Movement(velocity); 9 10       return (moveOK); 11   } 12 13 14   void paddle::Movement( 15       vector direction) 16   { 17       ASSERT(direction.X()==0); 18       sprite::Movement(direction); 19   } 

Normally sprites move in both the x and y directions. The paddle::Move() function moves the paddle only in the y direction on line 6. There is no possibility of it moving in the x direction.

Notice that the Move() function sets the paddle's movement vector to 0 on line 7. If it did not do this, the paddle would keep moving. In Ping, the paddle should only move when the player presses one of the appropriate keys. Setting the movement vector to 0 means that the only way the paddle can move in the next frame is if the player presses a key again. That's exactly as it should be.

The paddle::Movement() function, which begins on line 14, does two things. First, it uses a macro defined in LlamaWorks2D to make sure that no attempt is being made to move the paddle in the x direction. The ASSERT() macro takes a condition as its only parameter. On line 17, the condition tests to see whether the x value of the direction vector is 0. If it is, all is well. If that assertion is not equal to true, then the program crashes.

What?!!?

Tip

Professional programmers use assertions a lot in their programs. I strongly recommend that you do the same.


Yes, it's true. ASSERT() deliberately causes the program to crash if the assertion is not true. If the x value of the direction vector is not 0, then you made a programming error. You want to find all programmer errors before you release your game for sale. Assertions help you do that. You can't possibly miss it when your game crashes completely, so it's very easy to find errors with assertions.

Assertions are for debugging. Hopefully, they will help you catch your programming errors before you sell your game. When you're ready to release your game for sale, you remove all assertions. You don't have to go through and take them out yourself. Instead, uncomment the #define statement on line 5 of LlamaWorks2D.h and then recompile your program. A #define statement is a special type of command recognized by the C++ preprocessor, which is part of the compiler. The #define statement on line 5 of LlamaWorks2D.h tells the compiler that you don't want debugging stuff in your code. When you uncomment the #define statement, all assertions are automatically removed. It's a rather slick trick commonly used by C++ programmers.

Warning

Never use assertions for anything other than programmer errors. If you utilize them to handle such problems as input errors, players won't like your game much.


Initializing the Program

As previous chapters mentioned, initializing a Windows program is not for the faint of heart. For the most part, LlamaWorks2D handles the initialization for you. However, LlamaWorks2D calls your game class's OnAppLoad() function after the program starts but before it creates the program's window. This gives you the opportunity to control how the initialization takes place. Listing 7.6 gives the OnAppLoad() function for the ping class.

Listing 7.6. The OnAppLoad() function

 1    bool ping::OnAppLoad() 2    { 3        bool initOK = true; 4 5        // 6        // Initialize the window parameters. 7        // 8        init_params lw2dInitiParams; 9        lw2dInitiParams.openParams.fullScreenWindow = true; 10       lw2dInitiParams.winParams.screenMode. AddSupportedResolution( 11           LWSR_1024X768X32); 12       lw2dInitiParams.winParams.screenMode. AddSupportedResolution( 13           LWSR_1024X768X24); 14       lw2dInitiParams.winParams.screenMode. AddSupportedResolution( 15           LWSR_800X600X32); 16       lw2dInitiParams.winParams.screenMode. AddSupportedResolution( 17           LWSR_800X600X24); 18       lw2dInitiParams.openParams.millisecondsBetweenFrames = 0; 19 20       // This call MUST appear in this function. 21       theApp.InitApp(lw2dInitiParams); 22 23       return (initOK); 24    } 

This function sets the game to run in a full-screen window on line 9. Next, it states the screen resolutions it supports on lines 1017. The init_params variable declared on line 8 has a member that contains information about initializing Windows when the program starts up. Part of that information is a collection of screen resolutions that the game supports. That collection is kept in screenMode, which is an object of type screen_mode. The screen_mode class has a member function called AddSupportedResolution(). Each time your game calls AddSupportedResolution(), it passes in a value of type screen_resolution. The screen_resolution type contains a list of values that specify screen resolutions that LlamaWorks2D recognizes. Table 7.1 shows the available screen resolutions.

Table 7.1. Llamaworks2D Can Set The Screen to These Resolutions

Value

Resolution

LWSR_UNKNOWN

The resolution is unknown or not specified.

LWSR_640X480X24

Sets the screen to 640 X 480 X 24. Poor image quality.

LWSR_640X480X32

Sets the screen to 640 X 480 X 32. Poor image quality.

LWSR_800X600X24

Sets the screen to 800 X 600 X 24. Good image quality.

LWSR_800X600X32

Sets the screen to 800 X 600 X 32. Good image quality.

LWSR_1024X768X24

Sets the screen to 1024 X 768 X 24. Very good image quality.

LWSR_1024X768X32

Sets the screen to 1024 X 768 X 32. Very good image quality.

LWSR_1152X864X24

Sets the screen to 1152 X 864 X 24. Excellent image quality.

LWSR_1152X864X32

Sets the screen to 1152 X 864 X 24. Excellent image quality.


The resolutions given in Table 7.1 are the most common ones that games use. The first number in the resolution is the number of horizontal pixels. The second is the number of vertical pixels. The third number is the number of bits per pixel. So the value LWSR_800X600X32 sets the screen to a resolution of 800 pixels across and 600 pixels tall with 32 bits per pixel.

Factoid

The most common screen resolution for games today is 800 X 600 X 24. It is supported on virtually all computers currently in use.


The OnAppLoad() function in Listing 7.6 adds four supported screen resolutions. Most games support multiple resolutions. The reason is that you cannot guarantee that a player's screen supports the resolution you most want to use. Therefore, your game has to be able to find an acceptable resolution that the player's computer does support. LlamaWorks2D lets you add as many screen resolutions as it offers. When it attempts to set the screen resolution, it starts with the first resolution your OnAppLoad() function gives it. If that one doesn't work, it goes to the next one in the list, and so on, until it reaches the end of the list. If it can't find a supported resolution, the engine displays an error message saying that the game can't run on the player's computer.

The vast majority of computers that people are using today support the resolutions offered by LlamaWorks2D. If a player's computer doesn't support any of those resolutions, chances are their computer is far too old to run your game.

Initializing the Game

Initializing Ping is a more complex task than you might think. To initialize Ping, the program must do the following tasks:

1.

Set the size of the playable area.

2.

Load the ball's image.

3.

Initialize all of the ball's member data except the position and movement vector.

4.

Load the bitmap for the paddles.

5.

Initialize all of the paddles' member data except the position and movement vectors.

6.

Load the bitmap for the score markers.

7.

Load the bitmap for the trophy.

All of this initialization is handled by the ping class's InitGame() function. As you may recall from chapter 4, LlamaWorks2D calls InitGame() automatically. Listing 7.7 shows InitGame().

Listing 7.7. The ping::InitGame() function

 1    bool ping::InitGame() 2    { 3        bool initOK = true; 4        bitmap_region boundingRect; 5 6        areaOfPlay.top = 0; 7        areaOfPlay.bottom = theApp.ScreenHeight() - 20; 8        areaOfPlay.left = 0; 9        areaOfPlay.right = theApp.ScreenWidth(); 10 11       theBall.BitmapTransparentColor(color_ rgb(0.0f,1.0f,0.0f)); 12       initOK = 13           theBall.LoadImage( 14               "ball2.bmp", 15               image_file_base::LWIFF_WINDOWS_BMP); 16 17       if (initOK==true) 18       { 19           srand((unsigned)time(NULL)); 20 21           boundingRect.top = 0; 22           boundingRect.bottom = theBall.BitmapHeight(); 23           boundingRect.left = 0; 24           boundingRect.right = theBall.BitmapWidth(); 25           theBall.BoundingRectangle(boundingRect); 26       } 27       else 28       { 29           ::MessageBox( 30               NULL, 31               theApp.AppError().ErrorMessage().c_str(), 32               NULL, 33               MB_OK | MB_ICONSTOP | MB_SYSTEMMODAL); 34 35       } 36 37       if (initOK==true) 38       { 39           leftPaddle.BitmapTransparentColor( 40               color_rgb(0.0f,1.0f,0.0f)); 41           initOK = 42               leftPaddle.LoadImage( 43                   "paddle.bmp", 44                   image_file_base::LWIFF_WINDOWS_BMP); 45           if (!initOK) 46           { 47               ::MessageBox( 48                   NULL, 49                   theApp.AppError().ErrorMessage().c_str(), 50                   NULL, 51                   MB_OK | MB_ICONSTOP | MB_SYSTEMMODAL); 52 53           } 54       } 55 56      if (initOK==true) 57      { 58          scoreMarker.LoadImage( 59               "marker.bmp", 60               image_file_base::LWIFF_WINDOWS_BMP); 61          if (!initOK) 62          { 63              ::MessageBox( 64                  NULL, 65                  theApp.AppError().ErrorMessage().c_str(), 66                  NULL, 67                  MB_OK | MB_ICONSTOP | MB_SYSTEMMODAL); 68 69          } 70      } 71 72      if (initOK) 73      { 74          theTrophy.BitmapTransparentColor(color_ rgb(0.0f,1.0f,0.0f)); 75          initOK = 76              theTrophy.LoadImage( 77                   "trophy.bmp", 78                   image_file_base::LWIFF_WINDOWS_BMP); 79      } 80 81      if (initOK==true) 82      { 83          boundingRect.top = 3; 84          boundingRect.left = 10; 85          boundingRect.bottom = 63; 86          boundingRect.right = 22; 87          leftPaddle.BoundingRectangle(boundingRect); 88 89          rightPaddle = leftPaddle; 90 91          player1Score = player2Score = 0; 92          InitLevel(); 93      } 94 95      return (initOK); 96    } 

The InitGame() function begins by specifying the portion of the screen that is used for playing the game. The game does not use the entire screen, as Figure 7.2 shows.

Figure 7.2. The area of play is marked by the black rectangle.


The black rectangle in Figure 7.2 indicates the limits of the playable area. The game specifies the area of play because it needs a place to put the score markers. By limiting the portion of the screen that can be used for playing, the game leaves a blank area across the bottom of the screen where it can place score markers.

On lines 1115 of Listing 7.7, the InitGame() function sets the transparent color of the ball's bitmap and then loads it. If the bitmap is properly loaded, InitGame() calls the C++ Standard Library function srand() to seed the random number generator. The C++ Standard Library contains a function named rand() that generates random numbers. Ping uses it to set the initial speed and direction of the ball. When Ping starts up, it calls the srand() function to seed the random number generator with the current time. This helps ensure that the numbers that rand() generates are truly random.

Lines 2125 set the bounding rectangle of the ball. This is used to detect when the ball collides with the boundaries of the area of play or with the paddles. On lines 3944, InitGame() sets the transparent color of the left paddle's bitmap and loads it. If it is loaded properly, InitGame() loads the bitmap for the score marker on lines 5860. On lines 7478, InitGame() loads the bitmap for the trophy.

The final initialization that InitGame() performs on the left paddle is to set its bounding rectangle. It's at this point that InitGame() does something very tricky. Line 89 sets the right paddle equal to the left paddle. What this does is copy information in leftPaddle into rightPaddle. That includes integer values it contains such as the size of the bounding rectangle. However, unlike the integer values in the object, the paddle class uses a tool called a pointer that does not let the bitmap image be copied from paddle to paddle. Instead, they share the bitmap.

That's right. Both the left paddle and the right paddle share the very same bitmap image. This is a very cool trick that saves lots of memory. Pointers are extremely powerful tools that we'll cover in chapter 11, "Pointers." It is very common for games to share bitmaps between objects. If they couldn't, their graphics would take up a lot more memory and therefore the individual levels would be significantly smaller.

The InitGame() function finishes up by setting both players' scores to 0 and calling the InitLevel() function to initialize the first level of the game.

Initializing a Level

By comparison to initializing the game, initializing an individual level of Ping is considerably simpler. The llamaworks2d::game class contains a function named InitLevel() that you can override to initialize your levels. The game engine calls InitLevel() automatically at the beginning of each level. The tasks that this function performs are:

  1. Sets the initial position of the paddles

  2. Sets the initial position of the ball

  3. Sets the ball's initial movement vector

Listing 7.8 provides the InitLevel() function for the ping class.

Listing 7.8. The ping::InitLevel() function.

 1    bool ping::InitLevel() 2    { 3        bool initOK = true; 4        bitmap_region boundingRect = 5            leftPaddle.BoundingRectangle(); 6 7            leftPaddle.X(5); 8            int paddleHeight = boundingRect.bottom - boundingRect.top; 9            int initialY = 10               ((areaOfPlay.bottom - areaOfPlay.top)/2) - 11               (paddleHeight/2); 12           leftPaddle.Y(initialY); 13 14           int bitmapWidth = rightPaddle.BitmapWidth(); 15           int initialX = areaOfPlay.right - bitmapWidth - 5; 16           rightPaddle.X(initialX); 17           rightPaddle.Y(initialY); 18 19           SetBallStartPosition(); 20           SetBallStartDirection(); 21 22           LevelDone(false); 23           return (initOK); 24   } 

On line 4 of Listing 7.8, the InitLevel() function declares the variable boundingRect that it uses to temporarily hold the bounding rectangle of the left paddle. It sets the x position of the left paddle on line 7. Next, it calculates the height of the paddle's bounding rectangle. On lines 911, it uses the height to calculate the paddle's y position. The calculation centers the paddle halfway between the top and bottom of the area of play. InitLevel() sets the left paddle's y position on line 12.

At the beginning of each level, the y position of both paddles is the same. They are both set halfway down the area of play. So on line 17, InitLevel() uses the value it calculated on lines 911 as the y value of the right paddle. The x position is 5 pixels to the left of the right edge of the area of play. That value is calculated on line 15 and passed to the right paddle on line 16.

The InitLevel() function ends by calling functions to calculate and set the initial position, speed, and direction of the ball, and by telling the llamaworks2d::game class that the level is not done.

Listing 7.9 gives the functions that the ping class uses to set the ball's starting position, speed, and direction.

Listing 7.9. Setting the initial values of the ball

 1    void ping::SetBallStartPosition() 2    { 3        int startX = rand(); 4 5        while (startX >= areaOfPlay.right) 6        { 7            startX /= 10; 8        } 9 10       int startY = rand(); 11 12       while (startY >= areaOfPlay.bottom) 13       { 14           startY /= 10; 15       } 16 17       theBall.X(startX); 18       theBall.Y(startY); 19   } 20 21   void ping::SetBallStartDirection() 22   { 23       int xDirection = rand(); 24 25       while (xDirection > 10) 26       { 27           xDirection /= 10; 28       } 29 30       if (xDirection < 3) 31       { 32           xDirection = 3; 33       } 34 35       int yDirection = rand(); 36 37       while (yDirection > 10) 38       { 39           yDirection /= 10; 40       } 41 42       if (yDirection < 3) 43       { 44           yDirection = 3; 45       } 46 47       if (theBall.X() >= (areaOfPlay.right - areaOfPlay. left)/2) 48       { 49           xDirection *= -1; 50       } 51 52       if (theBall.Y() >= (areaOfPlay.bottom - areaOfPlay. top)/2) 53       { 54           yDirection *= -1; 55       } 56 57       theBall.Movement(vector(xDirection, yDirection)); 58   } 

Listing 7.9 contains the functions SetBallStartPosition() and SetBallStartDirection(). The SetBallStartPosition() function begins by calling the C++ Standard Library rand() function to generate a random number. The loop on lines 58 test the random number to see if it's larger than the right edge of the area of play. If it is, the loop divides the number by 10. Because this is an integer division, there is no factional part in the answer. Everything in the answer that is to the right of the decimal point is thrown away. This process essentially throws away the rightmost digit of the random number. It keeps throwing away the rightmost digit until the number is less than or equal to the right edge of the area of play. The result is used for the ball's x position.

Next, SetBallStartPosition() generates a random number for the ball's y position. While the value of the y position is larger than the bottom of the area of play, SetBallStartPosition() divides the value by 10. By the time that the SetBallStartPosition() function gets to lines 17 and 18, it has generated x and y values that are guaranteed to be within the area of play. Therefore, it stores the x and y values in the ball.

The SetBallStartDirection() function uses a similar approach to calculate the ball's starting direction and speed. On line 23 it generates a random number for the x value of the ball's movement vector. The loop on lines 2528 ensures that the x value is less than or equal to 10. If the x value is less than 3, the if statement on lines 3033 set it to 3. The process then repeats on lines 3545 for the vector's y value.

Because the rand() function only generates random numbers greater than zero, the x and y components of the ball's movement vector are always positive. Therefore, the vector points down and to the right. If the ball is on the right half of the area of play, the statement on line 49 points the ball's x direction toward the left. If the ball is in the lower half of the screen, the statement on line 54 reverses its y direction so that it points up. The SetBallStartDirection() function ends by setting the ball's movement vector using the values it calculated.

Handling Messages

As you saw earlier in this chapter, the ping class's message map connects the Windows message WM_KEYDOWN to the OnKeyDown() function. As a result, every time a player presses a key on the keyboard, LlamaWorks2D calls the OnKeyDown() function, which is shown in Listing 7.10.

Listing 7.10. The message handler

 1    bool ping::OnKeyDown( 2        keyboard_input_message &theMessage) 3    { 4        vector paddleDirection; 5 6        switch (theMessage.keyCode) 7        { 8            case KC_UP_ARROW: 9                paddleDirection.Y(-15); 10               rightPaddle.Movement(paddleDirection); 11           break; 12 13           case KC_DOWN_ARROW: 14               paddleDirection.Y(15); 15               rightPaddle.Movement(paddleDirection); 16           break; 17 18           case KC_A: 19               paddleDirection.Y(-15); 20               leftPaddle.Movement(paddleDirection); 21           break; 22 23           case KC_Z: 24               paddleDirection.Y(15); 25               leftPaddle.Movement(paddleDirection); 26           break; 27       } 28       return (false); 29   } 

Every time you write a function that handles the WM_KEYDOWN message, you must make it return a bool value. Recall that the ping class is derived from the llamaworks2d::game class provided by LlamaWorks2D. It is actually possible to make a game class that is derived from ping. In fact, you can use inheritance with game classes as much as you want. For example, you can have a class that is derived from a class that is derived from a class that is derived from game. In such a situation, you might want to pass the message up the chain of inheritance. To do so, have your message handling function return true. If you do not need to pass the message up the inheritance tree, your message-handling function should return false.

Tip

Your game classes will generally be derived from the llamaworks2d::game class. If so, their message-handling functions do not need to pass the message up the inheritance tree to the base class for handling. That means they should return the value false.


The ping class is derived from the game class. The game class doesn't do anything with keystrokes, so there's no sense in passing them along. You can if you want to, but it accomplishes nothing. Therefore, the OnKeyDown() function for the ping class returns false.

To find out which key was pressed, the OnKeyDown() function uses a C++ switch statement. The switch statement does the same thing as a group of chained if-else statements. If the key code equals the values in the case statements on lines 8, 13, 18, or 23, OnKeyDown() executes the commands between the case and break statements. If it doesn't equal any of those values, OnKeyDown() skips to the end of the switch statement and continues from there.

The case statement on line 8 is selected when the key code equals KC_UP_ARROW. It sets the y component of the right paddle's movement vector to -15. That makes the paddle move up 15 pixels on the next frame.

Likewise, the case statement on line 13 gets selected when the key code is KC_DOWN_ARROW. It sets the right paddle so that it moves down 15 pixels on the next frame.

The case statements beginning on lines 18 and 23 set the left paddle so that it moves 15 pixels up and down, respectively.

In general, your message-handling function processes all keystrokes it receives through a WM_KEYDOWN message in the manner shown here. It uses a switch statement to figure out which key was pressed, and then updates the game's current state.

Updating a Frame

The paddles and ball in the Ping game purposely do not have a lot of smarts built into them. For instance, the ball "knows" how to move and bounce, but it doesn't know when to bounce. The paddles know they move up and down, but they don't know how far. Neither the paddles nor the ball know that the ball bounces off the paddles. Why implement these objects with so little smarts?

Many arcade games use balls and paddles. The ball and paddle classes could easily be modified to work in those games. You'll do yourself a big favor if you make the objects in your games as reusable as possible. It saves a lot of time if you can reuse objects from previous games you wrote.

To help make your objects reusable, you put the code that enforces the rules of the game into your game class. If you don't, the objects you write become highly dependent on each other. For instance, programming the ball to bounce off paddles means that the ball class always has to be used with the paddle class. You can't reuse the ball class in a game that doesn't have paddles. Instead, you have to rewrite the ball class from scratch. If you put the code that enforces the rules of the game into your game class, the ball just has to know how to move and bounce. Lots of games have balls that move and bounce, so a ball class written like this is very handy to have around.

The ping::UpdateFrame() function demonstrates how to put the game logic in a game class. It tells the ball and paddles when to move. It then tests for conditions such as the ball reaching an edge of the area of play or hitting a paddle. When UpdateFrame() discovers that one of events has occurred, it tells the ball and paddles how to react. Listing 7.11 gives the code for the UpdateFrame() function.

Listing 7.11. The ping::UpdateFrame() function

 1    bool ping::UpdateFrame() 2    { 3        bool updateOK = true; 4 5        if (!GameOver()) 6        { 7            theBall.Move(); 8            rightPaddle.Move(); 9            leftPaddle.Move(); 10 11           if (BallHitPaddle()) 12           { 13               theBall.Bounce(ball::X_DIRECTION); 14           } 15 16           if (theBall.X()+theBall.BitmapWidth() < areaOfPlay.left) 17           { 18               player1Score++; 19 20           if (player1Score >= 5) 21           { 22               GameOver(true); 23           } 24           else 25           { 26               LevelDone(true); 27           } 28       } 29       else if (theBall.X() > areaOfPlay.right) 30       { 31           player2Score++; 32 33           if (player2Score >= 5) 34           { 35               GameOver(true); 36           } 37           else 38           { 39               LevelDone(true); 40           } 41       } 42 43       if (theBall.Y() <= areaOfPlay.top) 44       { 45           theBall.Y(1); 46           theBall.Bounce(ball::Y_DIRECTION); 47       } 48       else if (theBall.Y() + 49           theBall.BoundingRectangle().bottom >= 50           areaOfPlay.bottom) 51       { 52           theBall.Y(areaOfPlay.bottom - 53           theBall.BoundingRectangle().bottom - 1); 54           theBall.Bounce(ball::Y_DIRECTION); 55       } 56 57       if (leftPaddle.Y() < areaOfPlay.top) 58       { 59           leftPaddle.Y(0); 60       } 61       else if (leftPaddle.Y() + leftPaddle. BitmapHeight() > 62           areaOfPlay.bottom) 63       { 64           leftPaddle.Y( 65               areaOfPlay.bottom - leftPaddle. BitmapHeight()); 66       } 67 68       if (rightPaddle.Y() < areaOfPlay.top) 69       { 70           rightPaddle.Y(0); 71       } 72       else if (rightPaddle.Y() + rightPaddle. BitmapHeight() > 73           areaOfPlay.bottom) 74       { 75           rightPaddle.Y( 76                areaOfPlay.bottom - rightPaddle. BitmapHeight()); 77       } 78     } 79 80     return (updateOK); 81   } 

The first task of the UpdateFrame() function is to test whether the game is over. If it is, UpdateFrame() doesn't perform the update. If the game is still in progress, it moves the ball and the paddles. Next, UpdateFrame() calls a function named BallHitPaddle(). (The BallHitPaddle() is presented next in Listing 7.12we'll examine it in a moment.) If the BallHitPaddle() function indicates that the ball hit a paddle, the UpdateFrame() function calls the ball class's Bounce() function on line 13 to bounce the ball in the x direction.

Note

When you use a value from an enumerated type, you generally have to specify the class it's defined in, followed by two colons, followed by the enumerated value. The exception is when you use the value inside the member functions of the class the enumerated type is defined in.


Next, UpdateFrame() checks whether the ball went off the left edge of the area of play on line 16. If so, the player on the right (player 1) scores. UpdateFrame() increments player 1's score on line 18. If player 1 has scored more than five goals, player 1 is the winner and the game is over. The UpdateFrame() lets the game engine know that the game is over by calling the game::GameOver() function and passing it the value TRue. If player 1 has not won, then UpdateFrame() lets the game engine know that the current level (match) is over by calling LevelDone(), which is also a member of the llamaworks2d::game class.

If the ball did not go off the left edge of the area of play, the UpdateFrame() function tests to see if it went off the right edge on line 29. If it did, player 2 scored a point. UpdateFrame() checks to determine whether player 2 won the game. If so, it calls the game::GameOver() function to let LlamaWorks2D know the game is done. If the game is not yet over, UpdateFrame() invokes LevelDone() to tell the game engine that it's time for another level (match).

Lines 4347 bounce the ball in the y direction if it hits the top of the area of play. Lines 4855 bounce it when it hits the bottom. The if statement on lines 5766 keep the left paddle from going off the top or bottom of the area of play. The if statement on lines 6877 do the same for the right paddle.

The BallHitPaddle() function, which was called on line 11 of Listing 7.11, performs collision detection. It appears in Listing 7.12.

Listing 7.12. Did the ball hit a paddle?

 1    bool ping::BallHitPaddle() 2    { 3        bool hitPaddle = false; 4        int paddleLeft, paddleRight, paddleTop, paddleBottom; 5        int ballLeft, ballRight, ballTop, ballBottom; 6 7        ballLeft = theBall.X() + theBall.BoundingRectangle(). left; 8        ballRight = theBall.X() + theBall. BoundingRectangle().right; 9        ballTop = theBall.Y() + theBall.BoundingRectangle(). top; 10       ballBottom = theBall.Y() + theBall. BoundingRectangle().bottom; 11 12       paddleRight = 13           leftPaddle.X() + leftPaddle.BoundingRectangle(). right; 14       paddleTop = 15           leftPaddle.Y() + leftPaddle.BoundingRectangle(). top; 16       paddleBottom = 17           leftPaddle.Y() + leftPaddle.BoundingRectangle(). bottom; 18 19       bool leftEdge = (ballLeft <= paddleRight) ? true: false; 20 21       bool topEdge = 22           ((ballTop >= paddleTop) && 23            (ballTop <= paddleBottom)) ? 24            true:false; 25 26       bool bottomEdge = 27           ((ballBottom >= paddleTop) && 28            (ballBottom <= paddleBottom)) ? true:false; 29 30       if ((leftEdge) && ((topEdge) || (bottomEdge))) 31       { 32           theBall.X(paddleRight + 1); 33           hitPaddle = true; 34       } 35       else 36       { 37           paddleLeft = 38               rightPaddle.X() + rightPaddle. BoundingRectangle().left; 39           paddleTop = 40               rightPaddle.Y() + rightPaddle. BoundingRectangle().top; 41           paddleBottom = 42               rightPaddle.Y() + 43               rightPaddle.BoundingRectangle().bottom; 44 45           bool rightEdge = (ballRight >= paddleLeft) ? true:false; 46 47           topEdge = 48               ((ballTop >= paddleTop) && 49                (ballTop <= paddleBottom)) ? true:false; 50 51           bottomEdge = 52               ((ballBottom >= paddleTop) && 53                (ballBottom <= paddleBottom)) ? true:false; 54 55           if ((rightEdge) && ((topEdge) || (bottomEdge))) 56           { 57               theBall.X(paddleLeft - theBall.BitmapWidth() - 1); 58               hitPaddle = true; 59           } 60       } 61       return (hitPaddle); 62    } 

The BallHitPaddle() function first calculates where the left, right, top, and bottom edges of the ball's bounding rectangle are on the screen. It also finds the right, top, and bottom of the left paddle on lines 1217. On lines 1934 it uses the positions of the ball and left paddle to find out if there was a collision between the two. Let's take a close look at how that happens.

On line 19, the BallHitPaddle() function uses the C++ conditional operator, which is made up of the symbols ? and :, to test for an overlap between the left edge of the ball's bounding rectangle and the right edge of the paddle's bounding rectangle. The conditional operator tests the condition, which I've put in the parentheses before the question mark. If the condition is true, the conditional operator evaluates to its true expression, which is between the question mark and the colon. If the condition is false, the conditional operator evaluates to its false condition, which is between the colon and the semicolon. In this case, the values it evaluates to are true or false. The conditional operator is a C++ shorthand for an if statement. If you were to write the statement on line 19 using an if, it would look like the following code:

 if (ballLeft <= paddleRight) {     leftEdge = true; } else {     leftEdge = false; } 


Lines 2124 use a conditional operator to determine if the top of the ball is between the top and bottom of the left paddle. On lines 2628, BallHitPaddle() tests whether the bottom of the ball is between the top and bottom of the paddle. BallHitPaddle() uses the results of these tests in the if statement on line 30. If the left edge of the ball's bounding rectangle is past the right edge of the left paddle, and if either the top or the bottom of the ball is between the top and bottom of the paddle, a collision occurs between the ball and the paddle. This type of collision is shown in Figure 7.3.

Figure 7.3. The ball hit the left paddle.


You can see that the left edge of the ball's bounding rectangle has moved past the right edge of the paddle's bounding rectangle. The top edge of the ball's bounding rectangle is between the top and bottom of the paddle. In this situation, the if statement on line 30 is true and BallHitPaddle() executes the statements on lines 3233. If not, BallHitPaddle() jumps down to the else statement on line 35.

On lines 3743, the BallHitPaddle() function finds the location on the screen of the right paddle's bounding rectangle. It uses a conditional operator to determine if the top edge of the ball is between the top and bottom of the right paddle on lines 4749. The conditional operator on lines 5153 test to determine whether the bottom edge of the ball's bounding rectangle is between the top and bottom of the right paddle.

Beginning on line 55, BallHitPaddle() tests to see if the right edge of the ball has gone past the left edge of the right paddle, and if the top or bottom edge of the ball is between the top and bottom of the right paddle. If so, the ball hit the right paddle. In that case, BallHitPaddle() adjusts the position of the ball on line 57 to ensure that it is one pixel to the left of the right paddle. It then sets a Boolean variable indicating that the ball hit the paddle.

Approximation in Games

Ping uses a very simple method of bouncing a ball. It just reverses the sign of the movement vector in the direction of the bounce. This is not an accurate simulation of a bouncing ball. But for Ping, it's a good enough approximation.

It's often the case that professional game programmers substitute approximations into their games when they can. The approximation makes the game look as real as necessary, without incurring the performance decrease that would result from an accurate simulation of the relevant physics.

Approximation is a very good technique to use when you can. But be careful not to overuse it. If your approximations are too simple for the game you're writing, it makes the game look unrealistic.

So basically, use approximation, but don't overuse it. If your approximation looks right in your game, then it is right. If it looks unrealistic, you need to use better physics.


Before we move on, it's worth noting that professional programmers seldom break their conditions up as I've done in Listing 7.12. For example, on lines 1928 I repeatedly used the conditional operator to perform three tests. The results of the tests are stored in variables of type bool and then used in the condition of the if statement. I did that to make each condition more readable. Professional programmers generally just put all of them into the if statement's condition. It sometimes makes for very complicated conditions that can be difficult for new programmers to read. If you're not comfortable attempting to build large, complex conditions in if statements, then

Rendering a Frame

During each frame of animation, Ping must render all of its visual elements. This includes both paddles, the ball, and the score markers. As you've already seen, all of the visual elements in Ping are implemented as objects of type sprite or as objects derived from the sprite class. Ping can render these objects fairly easily using the sprite class's Render() function. As a result, Ping's RenderFrame() function is quite a bit simpler than its UpdateFrame() function. Listing 7.13 gives the RenderFrame() function.

Tip

If you find statements using the conditional operator to be difficult to read, you can use an if-else statement instead.


Listing 7.13. Rendering is the easy part.

 1    bool ping::RenderFrame() 2    { 3       theBall.Render(); 4       rightPaddle.Render(); 5       leftPaddle.Render(); 6 7       scoreMarker.Y(areaOfPlay.bottom + 5); 8       int i=1; 9       while (i<=player1Score) 10      { 11          scoreMarker.X( 12              areaOfPlay.right - 13              (scoreMarker.BitmapWidth() + 5) * i); 14          scoreMarker.Render(); 15          i++; 16      } 17 18      i=1; 19      while (i<=player2Score) 20      { 21          scoreMarker.X( 22              areaOfPlay.left + 23              (scoreMarker.BitmapWidth() + 5) * i); 24          scoreMarker.Render(); 25          i++; 26      } 27 28      return true; 29   } 

The RenderFrame() function renders the ball and paddles on lines 35 of Listing 7.13. On line 7, it sets the y position of all of the score markers it is about to display. All score markers are rendered 5 pixels below the bottom of the area of play.

Note

When there are parentheses in an equation, the program always evaluates the contents of the parentheses first.


The while loop on lines 916 renders the score markers for player 1. The equation on lines 1213 can be a little confusing unless you start with the part in the parentheses. The first thing that the program does when evaluating this equation is to add 5 to the width of the score marker's bitmap. The extra 5 pixels are added to provide a bit of blank space between each score marker. Next, the program multiplies the answer by the loop counter variable i. It then subtracts the resulting number from the right boundary of the area of play. This gives an x position for the score marker.

On lines 1926, the RenderFrame() function uses another while loop to position the score markers for player 2. Because player 2 operates the left paddle, the RenderFrame() function positions the score markers to the right of the left edge of the area of play.

That's really all there is to rendering. It's pretty straightforward.

Cleaning Up a Level

At the end of a level, the UpdateFrame() function calls the sprite::LevelDone() function to tell the game engine that the level is finished.

Each time the game engine goes through its message loop, it tests to see if the level is done. If so, the game automatically calls the DoLevelDone() function for your game class. You can have the DoLevelDone() function do any processing you want done when a level is finished. Listing 7.14 shows the DoLevelDone() function for the ping class.

Listing 7.14. Time for a nap

 1    bool ping::DoLevelDone() 2    { 3        ::Sleep(2000); 4        return (true); 5    } 

When a level (match) finishes, the score is updated by the UpdateFrame() function and an additional score marker is displayed on the screen by the RenderFrame() function. The only thing that DoLevelDone() does when a level finishes is pause for 2,000 milliseconds (2 seconds). This gives the players a chance to get themselves ready for the next match.

DoLevelDone() pauses by calling the Windows Sleep() function. Sleep() takes an integer number of milliseconds as its only parameter. When 2 seconds expires, the DoLevelDone() function returns and the new match starts.

Cleaning Up the Game

If a player wins, the UpdateFrame() function invokes the game::GameOver() function. This tells the game engine that the game is done. In response, the engine calls the DoGameOver() function for your game class. Listing 7.15 provides the code for the ping::DoGameOver() function.

Listing 7.15. Say bye-bye.

 1    bool ping::DoGameOver() 2    { 3        bool noError = true; 4 5        if (player1Score >= 5) 6        { 7            theTrophy.X(areaOfPlay.right * 3/4); 8            theTrophy.Y(areaOfPlay.bottom/2); 9        } 10       else 11       { 12           theTrophy.X(areaOfPlay.right * 1/4); 13           theTrophy.Y(areaOfPlay.bottom/2); 14       } 15 16       theApp.BeginRenderNow(); 17       theTrophy.Render(); 18       theApp.EndRenderNow(); 19 20       ::Sleep(4000); 21 22       game::DoGameOver(); 23 24       return (noError); 25   } 

At the end of a game, the DoGameOver() function uses the if-else statement on lines 514 to determine who the winner was. It sets the x and y coordinates of the trophy so that the trophy appears on the winner's side of the screen.

Next, DoGameOver() calls the BeginRenderNow() function. BeginRenderNow() is a function provided by the game engine's application object. It also has a corresponding EndRenderNow() function, which is called on line 18 of Listing 7.15. The BeginRenderNow() and EndRenderNow() functions provide you with a way of rendering to the screen even when your game is not in the main message processing loop. Your game should never call BeginRenderNow() or EndRenderNow() from the RenderFrame() function. There is no need, and it will make your game crash.

Good places to call the BeginRenderNow() and EndRenderNow() functions are in the InitLevel(), DoLevelDone(), and DoGameOver() functions. Every time your game calls BeginRenderNow(), it must also call EndRenderNow() when the rendering is done. If it does not, you'll find out quite soon because your game will crash.

In between BeginRenderNow() and EndRenderNow(), your game can do any rendering it needs to. You can have your game enter a loop that displays a long animation, plays music, or whatever else you would like. In the case of Ping, the DoGameOver() function invokes the trophy's Render() function to display the trophy. After it calls EndRenderNow(), DoGameOver() invokes the Windows Sleep() function to pause for four seconds.

The last thing that the ping::DoGameOver() function does is to call the game::DoGameOver() function to shut down the game. Your DoGameOver() function should always call the game::DoGameOver() function before it ends because game::DoGameOver() sends a message to Windows to tell it to end the program.

Warning

If your DoGameOver() function doesn't call game::DoGameOver(), you'll have to write code to post a quit message to Windows yourself.




Creating Games in C++(c) A Step-by-Step Guide
Creating Games in C++: A Step-by-Step Guide
ISBN: 0735714347
EAN: 2147483647
Year: N/A
Pages: 148

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