JavaScript EditorFree JavaScript Editor     Ajax Editor 



Main Page
  Previous Section Next Section

Timing Is Everything

The 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 Message

The 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.

graphics/04fig10.gif

To create a timer you need:

  • The window handle

  • ID of choice

  • The time delay in milliseconds

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 Timing

Although 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).


      Previous Section Next Section
    



    JavaScript EditorAjax Editor     JavaScript Editor