- Graphics Essentials
- Examining Graphics in Windows
- Painting Windows
- Building the Crop Circles Example
- Summary
- Field Trip
Building the Crop Circles Example
At this point, you've seen bits and pieces of GDI graphics code, and you've learned how to carry out basic drawing operations with a variety of different graphics shapes. You've also learned how to tweak the appearance of those shapes by creating and using different pens and brushes. You're now ready to put what you've learned into a complete example program that demonstrates how to draw graphics in the context of a game. Okay, you're not really creating a game in this chapter, but you are using the game engine to draw some pretty neat graphics. The example I'm referring to is called Crop Circles, and its name comes from the fact that it displays a random series of connected circles similar to those that have mysteriously appeared on rural English farm land. In the case of the Crop Circles example, I can assure you there is no alien involvement.
The idea behind the Crop Circles example is to draw a line connected to a circle at a random location in each cycle of the game engine. Although the lines and circles are drawn outside of the GamePaint() function in response to a game cycle, it is still important to demonstrate how to draw within GamePaint() so that the drawing isn't lost when the window is minimized or hidden. For this reason, Crop Circles draws a dark yellow background in GamePaint() to demonstrate how graphics drawn in this function are retained even if the window must be repainted. The actual lines and circles are drawn in GameCycle(), which means that they are lost if the window is repainted. Let's take a look at how the code actually works for this example program.
Writing the Program Code
The fun begins in the Crop Circles example with the header file, CropCircles.h, which is shown in Listing 3.1. All the code for the Crop Circles example is available on the accompanying CD-ROM.
Listing 3.1 The CropCircles.h Header File Declares the Global Game Engine Pointer and the Rectangular Position of the Previous Circle that Was Drawn
#pragma once //- // Include Files //- #include <windows.h> #include "Resource.h" #include "GameEngine.h" //- // Global Variables //- GameEngine* g_pGame; RECT g_rcRectangle;
This code isn't too mysterious. In fact, the only real difference between this header and the one you saw for the Blizzard example in the previous chapter is the declaration of the g_rcRectangle global variable. This rectangle stores the rectangular dimensions of the previously drawn circle, which allow you to connect it to the next circle with a line. The end result is that the circles all appear to be interconnected, which is roughly similar to real-world crop circles.
Moving right along, remember that we're now taking advantage of the game engine to simplify a great deal of the work in putting together programs (games). In fact, all that is really required of the Crop Circles example is to provide implementations of the core game functions. Listing 3.2 contains the code for the first of these functions, GameInitialize().
Listing 3.2 The GameInitialize() Function Creates the Game Engine and Sets the Frame Rate to 1 Cycle Per Second
BOOL GameInitialize(HINSTANCE hInstance) { // Create the game engine g_pGame = new GameEngine(hInstance, TEXT("Crop Circles"), TEXT("Crop Circles"), IDI_CROPCIRCLES, IDI_CROPCIRCLES_SM); if (g_pGame == NULL) return FALSE; // Set the frame rate (yes, it's deliberately slow) g_pGame->SetFrameRate(1); return TRUE; }
The GameInitialize() function is responsible for creating the game engine and setting the frame rate for it. In this case, the frame rate is set at 1 cycle per second (frame per second), which is extremely slow by any game standard. However, this frame rate is fine for viewing the crop circles being drawn; feel free to raise it if you want to see the circles drawn faster.
Following up on GameInitialize() is GameStart(), which actually gets things going. Listing 3.3 shows the code for the GameStart() function.
Listing 3.3 The GameStart() Function Seeds the Random Number Generator and Establishes an Initial Rectangle for the First Circle
void GameStart(HWND hWindow) { // Seed the random number generator srand(GetTickCount()); // Set the position and size of the initial crop circle g_rcRectangle.left = g_pGame->GetWidth() * 2 / 5; g_rcRectangle.top = g_pGame->GetHeight() * 2 / 5; g_rcRectangle.right = g_rcRectangle.left + g_pGame->GetWidth() / 10; g_rcRectangle.bottom = g_rcRectangle.top + g_pGame->GetWidth() / 10; }
Any program that makes use of random numbers is responsible for seeding the built-in random number generator. This is accomplished with the call to srand(). You'll see this line of code in virtually all the example programs throughout the book because most of them involve the use of random numbers; random numbers often play heavily into the development of games. The remainder of the GameStart() function is responsible for setting the position and size of the initial rectangle for the first circle to be drawn. This rectangle is sized proportionally to the client area of the main program window and is positioned centered within the client area.
As I mentioned earlier, part of the Crop Circles example demonstrated the difference between drawing graphics in the GamePaint() function, as opposed to drawing them in GameCycle(). Listing 3.4 shows the code for GamePaint(), which in this case is responsible for drawing a solid colored (dark yellow) background behind the crop circles.
Listing 3.4 The GamePaint() Function Draws a Dark Yellow Background that Fills the Entire Client Area
void GamePaint(HDC hDC) { // Draw a dark yellow field as the background for the crop circles RECT rect; GetClientRect(g_pGame->GetWindow(), &rect); HBRUSH hBrush = CreateSolidBrush(RGB(128, 128, 0)); // dark yellow color FillRect(hDC, &rect, hBrush); DeleteObject(hBrush); }
The FillRect() function you learned about in this chapter is used to draw a dark yellow rectangle that fills the entire client area. Because the rectangle is being drawn in GamePaint(), it is not lost when the window is repainted. You can easily alter the color of the background by changing the RGB values of the solid brush used to fill the rectangle.
The GameCycle()function is where the actual crop circles are drawn, as shown in Listing 3.5.
Listing 3.5 The GameCycle() Function Randomly Alters the Size and Position of the Crop Circle and Then Draws It
void GameCycle() { RECT rect; HDC hDC; HWND hWindow = g_pGame->GetWindow(); // Remember the location of the last crop circle int iXLast = g_rcRectangle.left + (g_rcRectangle.right - g_rcRectangle.left) / 2; int iYLast = g_rcRectangle.top + (g_rcRectangle.bottom - g_rcRectangle.top) / 2; // Randomly alter the size and position of the new crop circle GetClientRect(g_pGame->GetWindow(), &rect); int iInflation = (rand() % 17) - 8; // increase or decrease size by up to 8 InflateRect(&g_rcRectangle, iInflation, iInflation); OffsetRect(&g_rcRectangle, rand() % (rect.right - rect.left) - g_rcRectangle.left, rand() % (rect.bottom - rect.top) - g_rcRectangle.top); // Draw a line to the new crop circle hDC = GetDC(hWindow); HPEN hPen = CreatePen(PS_SOLID, 5, RGB(192, 192, 0)); // light yellow color SelectObject(hDC, hPen); MoveToEx(hDC, iXLast, iYLast, NULL); LineTo(hDC, g_rcRectangle.left + (g_rcRectangle.right - g_rcRectangle.left) / 2, g_rcRectangle.top + (g_rcRectangle.bottom - g_rcRectangle.top) / 2); // Draw the new crop circle HBRUSH hBrush = CreateSolidBrush(RGB(192, 192, 0)); // lighter yellow color SelectObject(hDC, hBrush); Ellipse(hDC, g_rcRectangle.left, g_rcRectangle.top, g_rcRectangle.right, g_rcRectangle.bottom); ReleaseDC(hWindow, hDC); DeleteObject(hBrush); DeleteObject(hPen); }
The GameCycle() function is interesting in that it does a few things you haven't seen before. First of all, it uses two new Win32 functions, InflateRect() and OffsetRect(), to randomly alter the size and position of the new crop circle. A random inflation value is first calculated, which is in the range of -8 to 8. This value is then used as the basis for shrinking or growing the rectangular extents of the circle using the InflateRect() function. The rectangle is then offset by a random amount between -9 and 9 using the OffsetRect() function.
With the new crop circle size and position figured out, the GameCycle() function moves on to drawing a line connecting the previous circle to the new one. A 5-pixel wide, light yellow pen is created and selected into the device context prior to drawing the line. The line is then drawn using the familiar MoveToEx() and LineTo() functions.
After drawing a line connecting the new crop circle, you're ready to determine a new fill color for it; this is accomplished by creating a dark yellow, solid brush. You then simply select the brush into the device context and draw the crop circle with a call to the Ellipse() function. After drawing the circle, the device context is released, and the brush and pen are deleted.
Testing the Finished Product
Now that you've worked through the code for the Crop Circles example, I suspect that you're ready to see it in action. Figure 3.7 shows the program running with several crop circles in full view, but unfortunately, no alien presence.
Figure 3.7 The Crop Circles example uses interconnected circles to simulate the crop circle phenomenon.
If you recall from the code, the Crop Circles example is smart enough to redraw the dark yellow background if the window is minimized or resized, but it doesn't take into account redrawing the circles and lines. You can test this out by covering part of the window with another window and then revealing the window again. The portion of circles and lines not covered will be erased because of the repaint, but the part of the window that was visible will remain untouched. This redraw problem is not too difficult to fix. In fact, you solve the problem in the next chapter when you build a slideshow example program using bitmap images.