Timing Is EverythingThe next topic we're going to cover is timing. Although it may seem unimportant, timing is crucial in a video game. Without timing and proper delays a game can run too fast or too slow and the illusion of animation is completely lost. If you recall, back in Chapter 1, "Journey into the Abyss," I mentioned that most games run about 30 fps (frames per second), but I never alluded to how to keep this timing constant. In this section you'll learn some techniques to track time and even send time-based messages. Later in the book you'll see how these ideas are used over and over to keep frame rate solid and you'll see how to augment parametric animation and physics on slow systems that can't sustain high frame rates. First, though, take a look at the WM_TIMER message. The WM_TIMER MessageThe PC has a built-in timer that can be very accurate (in the microsecond range), but because we're programming in Windows, it's not a good idea to muck with the timer ourselves. Instead, we'll use the timing functions built into Windows (which are built upon the actual hardware timer). The cool thing about this approach is that Windows virtualizes the timer into an almost infinite number of virtual timers. Thus, from your point of view, you can start and receive many messages from a number of timers, even though there's only one physical timer on most PCs. When you create a timer you set the ID of the timer along with the delay. The timer will begin to send messages to your WinProc() at the specified interval. Take a look at Figure 4.10 to see the data flow of some timers. Each timer sends WM_TIMER messages when its elapsed time has passed. You tell one timer from another when processing the WM_TIMER message with the timer ID (which you set when you create the timer). With that in mind, let's take a look at the function to create a timer—SetTimer(): UNIT SetTimer(HWND hWnd, // handle to parent window UINT nIDevent, // timer id UINT nElapse, // time delay in milliseconds TIMERPROC lpTimerFunc); // timer callback Figure 4.10. Message flow for the WM_TIMER message.To create a timer you need:
With these three things, you're in business. However, the last parameter takes a little explanation. lpTimerFunc() is a callback function just like WinProc() is, hence, you can create a timer that calls a function at some specified interval instead of processing it in the WinProc() via WM_TIMER messages. It's up to you, but I usually use the WM_TIMER messages and leave the TIMERPROC set to NULL. You can create as many timers as you wish, but remember that they all take up resources. If the function fails, it will return 0. Otherwise, SetTimer() returns the timer ID you sent to create the timer with. The next question is how to tell one timer from another. The answer is that you interrogate the wparam when the WM_TIMER message is sent; it contains the timer ID that you originally created the timer with. As an example, here's how you would create two timers, one with a 1.0 second delay and the other with a 3.0-second delay: #define TIMER_ID_1SEC 1 #define TIMER_ID_3SEC 2 // maybe do this in WM_CREATE SetTimer(hwnd, TIMER_ID_1SEC, 1000,NULL); SetTimer(hwnd, TIMER_ID_3SEC, 3000,NULL); Notice that the delays are in milliseconds. In other words, 1000 milliseconds equals 1.0 seconds and so forth. Moving on, here's the code you would need to add to your WinProc() to process the timer messages: case WM_TIMER: { // what timer fired? switch(wparam) { case TIMER_ID_1SEC: { // do processing here } break; case TIMER_ID_3SEC: { // do processing here } break; default:break; } // end switch // let windows know we handled the message return(0); } break; Finally, when you're done with a timer, you can kill it with KillTimer(): BOOL KillTimer(HWND hWnd, // handle of window UINT uIDEvent ); // timer id Continuing with the example, you might want to kill all the timers in the WM_DESTROY message, as shown here: case WM_DESTROY: { // kill timers KillTimer(hwnd, TIMER_ID_1SEC); KillTimer(hwnd, TIMER_ID_3SEC); // terminate application or whatever... PostQuitMessage(0); } break; WARNING Even though timers may seem free and abundant, PCs aren't Star Trek computers. Timers use resources and should be used sparingly. Make sure to kill any timer that you don't need anymore during run-time. As a working example of using timers, take a look at DEMO4_6.CPP on the CD. It creates three timers with different times and then prints out when each timer changes. Finally, although timers take time delays in milliseconds, they are hardly millisecond-accurate. Don't expect your timers to be more accurate than 10–20 milliseconds. If you need more accuracy, there are methods, such as using the Win32 High Performance timers or using the Pentium Real-Time hardware counters based on the RDTSC assembly language instruction. Low-Level TimingAlthough creating timers is at least one way to keep track of time, the technique suffers from a few faults: First, timers send messages, and second, timers aren't that accurate. Finally, in most game loops you want to force the main body of the code to run at a specific frame rate and no higher; this is achieved by locking the frame rate via timing code. Timers aren't very good at this. What's really needed is a way to query a system clock of sorts and then perform differential tests to see how much time has elapsed. The Win32 API has such a function, and it's called GetTickCount(): DWORD GetTickCount(void); GetTickCount() returns the number of milliseconds since Windows was started. That may not seem useful as an absolute reference, because you have none, but it's perfect as a differential reference. All you have to do at the top of any code block that you want to time is query the current tick count and then at the end of the loop query again, and take the difference. Whammo, you have the time difference in milliseconds. For example, here's how you would make sure that a chunk of code runs at exactly 30 fps or with a delay of 1/30fps = 33.33 milliseconds: // get the starting time DWORD start_time = GetTickCount(); // do work, draw frame, whatever // now wait until 33 milliseconds has elapsed while ((GetTickCount() - start_time) < 33); That's what I'm talking about, baby! Of course, sitting in a busy loop is a waste of time performing the while() logic, but you can always branch off and test every now and then, so you don't waste cycles. The point is that with this technique you can force time constraints on chunks of code. NOTE Obviously, if your PC can't run at 30 fps, the loop will take longer. However, if during a free run of your code the loop ran from 30–100 fps, the preceding code would lock it to 30 fps always. That's the point! As an example, take a look at DEMO4_7.CPP on the CD. It basically locks the frame rate to 30 fps and updates a little screen saver with lines on each frame. Following is the code from the WinMain() that does the work: // get the dc and hold onto it hdc = GetDC(hwnd); // seed random number generator srand(GetTickCount()); // endpoints of line int x1 = rand()%WINDOW_WIDTH; int y1 = rand()%WINDOW_HEIGHT; int x2 = rand()%WINDOW_WIDTH; int y2 = rand()%WINDOW_HEIGHT; // intial velocity of each end int x1v = -4 + rand()%8; int y1v = -4 + rand()%8; int x2v = -4 + rand()%8; int y2v = -4 + rand()%8; // enter main event loop, but this time we use PeekMessage() // instead of GetMessage() to retrieve messages while(TRUE) { // get time reference DWORD start_time = GetTickCount(); // test if there is a message in queue, if so get it if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { // test if this is a quit if (msg.message == WM_QUIT) break; // translate any accelerator keys TranslateMessage(&msg); // send the message to the window proc DispatchMessage(&msg); } // end if // is it time to change color if (++color_change_count >= 100) { // reset counter color_change_count = 0; // create a random colored pen if (pen) DeleteObject(pen); // create a new pen pen = CreatePen(PS_SOLID,1, RGB(rand()%256,rand()%256,rand()%256)); // select the pen into context SelectObject(hdc,pen); } // end if // move endpoints of line x1+=x1v; y1+=y1v; x2+=x2v; y2+=y2v; // test if either end hit window edge if (x1 < 0 || x1 >= WINDOW_WIDTH) { // invert velocity x1v=-x1v; // bum endpoint back x1+=x1v; } // end if if (y1 < 0 || y1 >= WINDOW_HEIGHT) { // invert velocity y1v=-y1v; // bum endpoint back y1+=y1v; } // end if // now test second endpoint if (x2 < 0 || x2 >= WINDOW_WIDTH) { // invert velocity x2v=-x2v; // bum endpoint back x2+=x2v; } // end if if (y2 < 0 || y2 >= WINDOW_HEIGHT) { // invert velocity y2v=-y2v; // bum endpoint back y2+=y2v; } // end if // move to end one of line MoveToEx(hdc, x1,y1, NULL); // draw the line to other end LineTo(hdc,x2,y2); // lock time to 30 fps which is approx. 33 milliseconds while((GetTickCount() - start_time) < 33); // main game processing goes here if (KEYDOWN(VK_ESCAPE)) SendMessage(hwnd, WM_CLOSE, 0,0); } // end while // release the device context ReleaseDC(hwnd,hdc); // return to Windows like this return(msg.wParam); } // end WinMain Other than the timing aspect of the code, there is some other logic that you should take some time to review: the collision logic. You'll notice that there are two ends of the line segment, each with a position and velocity. As the segment moves, the code tests whether it has collided with the edge of the window client area. If so, the segment is bounced off the edge, creating the illusion of a bouncing line. TRICK If you just want to delay your code, use a Win32 API function called Sleep(). Just send it the time delay in milliseconds you wish to delay and the function will. For example, to delay 1.0 second, you would say Sleep(1000). |