Controlling Games with the Keyboard and Mouse - Building the UFO Example
(Page 7 of 8 )
In order to really get a feel for how keyboard and mouse input works in games, it's helpful to work through a complete example. The remainder of this chapter focuses on an example called UFO, which is a suitable follow-up to the Crop Circles example from Chapter 3. Although this program isn't technically a game, it's by far the closest thing you've seen to a game yet. It involves a flying saucer that you control with the keyboard and/or mouse. You're able to fly the flying saucer around a bitmap background image. Perhaps most important is the fact that the UFO program demonstrates how good of a feel you can create for game controls. More specifically, the arrow keys on the keyboard are surprisingly responsive in the UFO program.
Although you haven't really learned about animation yet, the UFO example makes use of animation to allow you to fly the flying saucer. Fortunately, the program is simple enough that you can get by without knowing the specifics about animation. All you really need to know is that you can alter the position of a bitmap image to simulate movement on the screen. This occurs thanks to the game engine, which redraws the bitmap every game cycle. So, by altering the position of an image and redrawing it repeatedly, you create the effect of movement. The UFO example reveals how this task is accomplished, as well as how the keyboard and mouse fit into the picture.
Note - You learn the details of how animation works in Chapter 9, "Making Things Move with Sprite Animation."
Writing the Program Code
The header file for the UFO example lays the groundwork for the meat of the program, which carries out the details of the flying saucer animation and user input. Listing 5.1 contains the code for the UFO.h header file, which declares global variables used to control the flying saucer.
Listing 5.1 The UFO.h Header File Declares Global Variables Used to Keep Track of the Flying Saucer
#pragma once
//————————————————————————————————-
// Include Files
//————————————————————————————————-
#include <windows.h>
#include "Resource.h"
#include "GameEngine.h"
#include "Bitmap.h"
//————————————————————————————————-
// Global Variables
//————————————————————————————————-
HINSTANCE g_hInstance;
GameEngine* g_pGame;
const int g_iMAXSPEED = 8;
Bitmap* g_pBackground;
Bitmap* g_pSaucer;
int g_iSaucerX, g_iSaucerY;
int g_iSpeedX, g_iSpeedY;
The first thing of interest in this code is the g_iMAXSPEED constant, which establishes the maximum speed of the flying saucer. The speed of the flying saucer is how many pixels it can travel in a given direction in each game cycle. So, the value of the g_iMAXSPEED constant means that the flying saucer can never travel more than 8 pixels in a horizontal or vertical direction in a game cycle.
The g_pBackground and g_pSaucer global variables store the two bitmaps used in the program, which correspond to a night sky background image and the flying saucer image. The remaining variables pertain to the flying saucer and include its XY position and XY speed. The XY position of the flying saucer is specified relative to the game screen. The XY speed, on the other hand, simply tells the program how many pixels the flying saucer should be moved per game cycle; negative values for the speed variables indicate that the flying saucer is moving in the opposite direction.
With the global variables in place, we can move on to the game functions. The first game function to consider is GameInitialize(), which creates the game engine and establishes the frame rate. The frame rate for the program is set to 30 frames per second. This is a relatively high frame rate for games, but it results in much smoother motion for the flying saucer, as you soon find out.
Next on the game function agenda is the GameStart() function, which creates and loads the flying saucer bitmaps, as well as sets the initial flying saucer position and speed (Listing 5.2).
Listing 5.2 The GameStart() Function Performs Startup Tasks for the UFO Example
void GameStart(HWND hWindow)
{
// Create and load the background and saucer bitmaps
HDC hDC = GetDC(hWindow);
g_pBackground = new Bitmap(hDC, IDB_BACKGROUND, g_hInstance);
g_pSaucer = new Bitmap(hDC, IDB_SAUCER, g_hInstance);
// Set the initial saucer position and speed
g_iSaucerX = 250 - (g_pSaucer->GetWidth() / 2);
g_iSaucerY = 200 - (g_pSaucer->GetHeight() / 2);
g_iSpeedX = 0;
g_iSpeedY = 0;
}
The GameStart() function is used to initialize data pertaining to the program, such as the bitmaps and other global variables. The flying saucer position is initially set to the middle of the game screen, and then the speed of the saucer is set to 0 so that it isn't moving.
You might think that a program with an animated flying saucer cruising over a background image would require a complex GamePaint() function. However, Listing 5.3 shows how this simply isn't the case.
Listing 5.3 The GamePaint() Function Draws the Background and Flying Saucer Bitmaps
void GamePaint(HDC hDC)
{
// Draw the background and saucer bitmaps
g_pBackground->Draw(hDC, 0, 0);
g_pSaucer->Draw(hDC, g_iSaucerX, g_iSaucerY, TRUE);
}
As the code reveals, the GamePaint() function for the UFO program is painfully simple; all the function does is draw the background and flying saucer bitmaps. The background bitmap is drawn at the origin (0, 0) of the game screen, whereas the flying saucer is drawn at its current position. Notice that TRUE is passed as the last argument to the Draw() method when drawing the flying saucer, which indicates that the saucer is to be drawn with transparency using the default transparent color (magenta).
Figure 5.1 shows the flying saucer bitmap image, including the transparent color filled around the saucer; I realize that you're seeing this image in black and white on the printed page, but you can either visualize the hot purple transparent color or open the Saucer.bmp file for yourself from the accompanying CD-ROM.

Figure 5.1-- The flying saucer bitmap image uses the default transparent color (magenta) in the filled area around the saucer to indicate where the image is transparent.
The GameCycle() function is a little more interesting than the others you've seen because it is actually responsible for updating the position of the flying saucer based on its speed. Listing 5.4 shows how this is accomplished in the code for the GameCycle() function.
Listing 5.4 The GameCycle() Function Updates the Saucer Position and then Repaints the Game Screen
void GameCycle()
{
// Update the saucer position
g_iSaucerX = min(500 - g_pSaucer->GetWidth(), max(0, g_iSaucerX + g_iSpeedX));
g_iSaucerY = min(320, max(0, g_iSaucerY + g_iSpeedY));
// Force a repaint to redraw the saucer
InvalidateRect(g_pGame->GetWindow(), NULL, FALSE);
}
The GameCycle() function updates the position of the flying saucer by adding its speed to its position. If the speed is negative, the saucer will move to the left and/or up, whereas positive speed values move the saucer right and/or down. The seemingly tricky code for setting the position must also take into account the boundaries of the game screen so that the flying saucer can't be flown off into oblivion. Granted, the concept of flying off the screen might sound interesting, but it turns out to be quite confusing! Another option would be to wrap the saucer around to the other side of the screen if it goes over the boundary, which is how games such as Asteroids solved this problem, but I opted for the simpler solution of just stopping it at the edges. After updating the position of the flying saucer, the GameCycle() function forces a repaint of the game screen to reflect the new saucer position.
The flying saucer is now being drawn and updated properly, but you still don't have a way to change its speed so that it can fly. This is accomplished first by handling keyboard input in the HandleKeys() function, which is shown in Listing 5.5.
Listing 5.5 The HandleKeys() Function Checks the Status of the Arrow Keys, Which Are Used to Control the Flying Saucer
void HandleKeys()
{
// Change the speed of the saucer in response to arrow key presses
if (GetAsyncKeyState(VK_LEFT) < 0)
g_iSpeedX = max(-g_iMAXSPEED, --g_iSpeedX);
else if (GetAsyncKeyState(VK_RIGHT) < 0)
g_iSpeedX = min(g_iMAXSPEED, ++g_iSpeedX);
if (GetAsyncKeyState(VK_UP) < 0)
g_iSpeedY = max(-g_iMAXSPEED, --g_iSpeedY);
else if (GetAsyncKeyState(VK_DOWN) < 0)
g_iSpeedY = min(g_iMAXSPEED, ++g_iSpeedY);
}
The HandleKeys() function uses the Win32 GetAsyncKeyState() function to check the status of the arrow keys (VK_LEFT, VK_RIGHT, VK_UP, and VK_DOWN) and see if any of them are being pressed. If so, the speed of the flying saucer is adjusted appropriately. Notice that the newly calculated speed is always checked against the g_iMAXSPEED global constant to make sure that a speed limit is enforced. Even flying saucers are required to stay within the speed limit!
Note - The GetAsyncKeyState() function is part of the Win32 API, and it provides a means of obtaining the state of any key on the keyboard at any time. You specify which key you're looking for by using its virtual key code; Windows defines virtual key codes for all the keys on a standard keyboard. Common key codes for games include VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN, VK_CONTROL, VK_SHIFT, and VK_RETURN.
If you thought handling keyboard input in the UFO program was easy, wait until you see how the mouse is handled. To make things a little more interesting, both mouse buttons are used in this program. The left mouse button sets the flying saucer position to the current mouse cursor position, whereas the right mouse button sets the speed of the flying saucer to 0. So, you can use the mouse to quickly get control of the flying saucer; just right-click to stop it and then left-click to position it wherever you want. Listing 5.6 shows the code for the MouseButtonDown() function, which makes this mouse magic possible.
Listing 5.6 The MouseButtonDown() Function Uses the Left and Right Mouse Buttons to Move the Flying Saucer to the Current Mouse Position and Stop the Flying Saucer, Respectively
void MouseButtonDown(int x, int y, BOOL bLeft)
{
if (bLeft)
{
// Set the saucer position to the mouse position
g_iSaucerX = x - (g_pSaucer->GetWidth() / 2);
g_iSaucerY = y - (g_pSaucer->GetHeight() / 2);
}
else
{
// Stop the saucer
g_iSpeedX = 0;
g_iSpeedY = 0;
}
}
The first step in this code is to check and see which one of the mouse buttons was pressed—left or right. I know, most PC mouse devices these days have three buttons, but I wanted to keep the game engine relatively simple, so I just focused on the two most important buttons. If the left mouse button was pressed, the function calculates the position of the flying saucer so that it is centered on the current mouse cursor position. If the right button was pressed, the speed of the flying saucer is set to 0.
Note - If you determine that supporting two mouse buttons isn't enough for the game engine, considering that three-button mouse devices are sometimes used, feel free to modify it on your own. It primarily involves changing the third argument of the MouseButtonDown() function so that it can convey more than two values—one for each of the three mouse buttons.
This chapter is from Beginning Game Programming, by Michael Morrison (Sams, ISBN: 0672326590). Check it out at your favorite bookstore today.
Buy this book now. |
Next: Testing the Finished Product >>
More PC Gaming Articles
More By Sams Publishing