The Main Loop


The main difference between games and most other applications is that games are constantly busy doing something in the background. Other applications will patiently wait until you move the mouse or mash keys on the keyboard before they do anything. If games did this they would be as boring as Microsoft Word or Excel. The main loop of a game should accomplish the following tasks until the player quits the game or deactivates the window:

  • Update your game logic

  • Render and present the scene

We'll start by taking an example of a classic Win32 message pump and build it up until it works for games. Taken straight from a DirectX 9 sample, the simplest game message pump looks like this:

 MSG msg; ZeroMemory( &msg, sizeof(msg) ); while( msg.message!=WM_QUIT ) {    if( PeekMessage( &msg, NULL, OU, OU, PM_REMOVE ) )    {      TranslateMessage( &msg );      DispatchMessage( &msg );    }    else      MyRender(); } 

Assume for a moment that the MyRender() method does nothing more than render and present the frame. You'll also have to assume that any game logic update occurs only as a result of messages appearing in the message queue. The problem with this message pump is that there's nothing in the code to change the game state if there are no messages in the queue. If you changed the game state only as a result of receiving messages, you would only see animations happen if you moved the mouse. A game with that kind of architecture would probably help hard core gamers get a little exercise, but I wouldn't suggest it.

Windows provides a message that seems like a good solution to this problem: WM_TIMER. This Win32 message can be sent at definite intervals. Using the Win32 SetTimer() API, you can cause your application to receive these WM_TIMER messages or you can specify a callback function. For those programmers like me who remember the old Windows 3.1 days, WM_TIMER was the only way games could get a semblance of background processing. Windows 3.1 was a cooperative multitasking system, which meant that the only time your application got CPU time was if it had a message to process and no other app was hogging the message pump.

The biggest problem with using WM_TIMER is resolution. Even though you specify WM_TIMER calls down to the millisecond, the timer doesn't actually have millisecond accuracy and you are not guaranteed to be called in the exact intervals you'd hope. Since Win32 operating systems are multitasking we can happily choose a better alternative. To see this, let's make a few changes to the main loop code:

 MSG msg; ZeroMemory( &msg, sizeof(msg) ); while( msg.message!=WM_QUIT ) {    // Part 1: Do idle work until a message is seen in the message queue    while (!PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE))    {       // call MyMainLoop while in bIdle state       MyMainLoop();    }    // Part 2: Pump messages until the queue is empty    do    {       if (GetMessage(&msg, NULL, NULL, NULL))       {         TranslateMessage(&msg);         DispatchMessage(&msg);       }    } while (::PeekMessage(&msg), NULL, NULL, NULL, PM_NOREMOVE)); } 

This is a much better main loop because it alternates between emptying the message queue and doing idle work, which is defined as anything that happens while the player isn't doing anything: game logic, AI, and rendering.

This code could be optimized but I'll leave that as an exercise for you. If you want to see a better version of this main loop take a look at the source code for CWinThread::Run() in the MFC library. It does a pretty fair job of filtering out certain messages that should not affect the state of the user interface, such as redundant WM_MOUSEMOVE messages. Still, the code above is good enough for government work.

Let's take a look at the inside of the MyMainLoop() method. Those of you coding in MFC will put this code into an overloaded CMainFrame::OnIdle():

 bool MyMainLoop() {    if ( !m_bInitialized || m_bMinimized )       return false;    HRESULT hr;    if( m_bActive )    {       // MyProcessNextFrame is the call that processes game logic       if( FAILED( hr = MyProcessNextFrame() ) )       {         My ReinitializeDisplay();         return false;       }       Sleep(2); // Pause a bit to keep from sucking all the CPU       return true;    }    else    {       // Note! Never ever paint if the app isn't active.       // Turn off audio, since the boss doesn't like game music.       MyPauseAudio();       // Go to sleep until a message appears in the queue       WaitMessage();       return false;    }    if (m_bQuitting)    {       PostMessage(WM_CLOSE, 0, 0);       return ret;    }    return false; } 

The first predicate in the main loop handles the trivial case where the main loop should return instantly. It is possible that the main loop will run before the game is fully initialized. In Windows, there are plenty of messages that get pumped before you are really ready to run your main loop, WM_CREATE is a great example. If you want to see all of them sometime, I suggest you run Spy++ and see for yourself.

Another trivial case is easy to fathom; if the window is minimized you shouldn't do anything. This is true for most games, but perhaps a case could be made for massively multiplayer games that support background activity of some kind. Perhaps you'll need to write some code that will maximize the window automatically upon some event or trigger. It's purely up to you.

This main loop has two branches, which run depending on whether the window is active. An active game should do three things: call the business end of the game code, MyProcessNextFrame(), check to see if the display needs to be reinitialized, and finally call Sleep(). MyProcessNextFrame() is what you'll code to change your game state and render the display, and you can assume that the only reason it will fail and return control back to the main loop is upon a draw failure. It's totally possible that in the middle of this call the player will do something crazy like change their desktop settings or switch to fullscreen mode, invalidating the display. You'll call the Win32 API Sleep() for a really short time, two milliseconds in fact, to relinquish some CPU time to other applications. Otherwise, your game will constantly peg the CPU at 100%, making running anything else, including a debugger, a pain in the ass.

If the player Alt-Tabs away to look at email or something, the window will go inactive and you should do a little homework to make your game Windows friendly. The first thing you should do is pause your audio system. If someone is playing a game at the office and the boss walks in the door the last thing that needs to happen after an Alt-Tab is some grunt warrior in your game yelling, "Who's your daddy!" An Alt-Tab should bury your game instantly, thus the necessity of pausing your audio system. When the game is reactivated, the audio system can pick up where it left off.

The call to the Win32 API WaitMessage() guarantees that the game won't get any more attention from the process scheduler until a message is inserted into the message queue. Essentially it instructs Windows to let this application lie dormant until something wakes it up, such as a mouse click or another window being dragged over the inactive window. This is friendly Windows application behavior, as it allows your game to relinquish its stranglehold on the CPU when the player wants to activate another application.

The code inside MyProcessNextFrame() decouples the processing of game logic and updating the screen. Rendering the display is extremely time-consuming and it makes little sense to update the screen at a high refresh rate. A good rule of thumb says that games shouldn't need to refresh their screens any more than 60fps. Why? That happens to coincide with the default scan rate of most monitors, and it turns out that humans can't really perceive a framerate any faster than about 22fps.

 #include <mmsystem.h> const unsigned int SCREEN_REFRESH_RATE(1000/60); HRESULT MyProcessNextFrame() {    // The wise C++ programmer would put these nasty static variables in       // a nice class.       static DWORD currTick = 0;        // time right now       static DWORD lastUpdate = 0;      // previous time       static DWORD lastDraw = 0;        // last time the game rendered       static bool runFullSpeed = false; // set to true if you want to run full speed       // Figure how much time has passed since the last time       currTick = timeGetTime();       MyUpdateGameLogic(currTick - lastUpdate);       lastUpdate = currTick;       // It is time to draw ?       if( runFullSpeed || ( (currTick - lastDraw) > SCREEN_REFRESH_RATE) )       {          if (S_OK == MyPaint())       {             // record the last successful paint             lastDraw = currTick;       }    }     return S_OK; } 

The call to timeGetTime() returns the current system time in milliseconds. There are more accurate timers available in Intel based machines, but the Windows multimedia timer is an excellent workhorse timer. The call to MyUpdateGame() is made with the number of milliseconds that have passed since the last game loop. Any time dependant game logic, such as the distance an object should move given a certain speed in the game world, will use this value as the master clock. The call to MyPaint() is made within a predicate that limits the frame rate to SCREEN_REFRESH_RATE. A boolean value, runFullSpeed, can be set to true if you are interested in watching your game run without the "governor."

The funny part of all this is that most console games only worry about this last method, and simply stick it into a while loop. Console games are never inactive and they never have to worry about working and playing well with other apps like Microsoft Word. This is yet another case where I shake my head at producers that think programming Windows games is somehow trivial compared to console games.

The guts of MyUpdateGameLogic() will take up the rest of this chapter, so for clarity I'll skip that for a moment and show you how to render and present your display.

Rendering and Presenting the Display

The method MyPaint() attempts to draw the next frame, get the results to the front buffer, and handle any problems if a failure occurs:

 HRESULT MyPaint() {    HRESULT hr;    if( FAILED( hr = MyDraw() ) || FAILED( hr = MyPresent() )    {      // insert code to handle drawing problems here such as mode changes      return hr;    }     return S_OK; } 

Every game's draw routine will look something like this:

 //Draw everything HRESULT MyDraw() const {    HRESULT result = S_OK;    for(MyDrawableList::iterator i=pDrawList->begin(); i!=pDrawList->end(); ++i)    {      result = (*i)->Draw();      if(result != S_OK)        return result;    }    return result; } 

I've implemented my draw list in STL. Drawable objects can be anything from a background sprite to an off-screen surface that is the destination surface of a render target. Each drawable object knows how to draw itself via the implementation of the Draw() method. If you think there's plenty missing from this little example, you're right. First, the incredibly tricky task of managing and sorting the draw list is handled in the game update code. As drawable objects come into and out of view, the object will get added to or removed from the draw list. One of the hardest tasks in all of game programming is minimizing the size and sorting of this list. One more thing: Some games actually use a tree structure instead of a list. Secondly, the code to actually draw the objects themselves is completely dependent on the object. These topics are covered in the graphics chapters.

MyDraw() composes a new frame in a back buffer. The contents are either copied to the front buffer or the front and back buffers are flipped, assuming the appropriate video hardware is installed. This process is commonly called presenting, thus the name for the next function, MyPresent():

 HRESULT MyPresent() {     HRESULT hr;     while( 1 )     {       // Windowed mode - nothing special - full BLIT       if ( m_bWindowed )       {          m_pDD->WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN, NULL);          hr = m_pddsFrontBuffer->Blt(             &m_rcWindow,             m_pddsBackBuffer,             NULL, DDBLT_WAIT, NULL );       }       else // Fullscreen modes       {          // There are modals up and fullscreen, wait and blit directly          // Fullscreen BLIT          if ( ! m_bFlipping )          {            m_pDD->WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN, NULL);            hr = m_pddsFrontBuffer->Blt(               CRect( 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT ),               m_pddsBackBuffer,               NULL, DDBLT_WAIT, NULL );          }          else          {            // Fullscreen FLIP            hr = m_pddsFrontBuffer->Flip( NULL, 0 );          }     } if ( FAILED( hr ) )    TRACE( _T("Present() failed to FLIP/BLIT m_surface.\n" ) ); if( hr == DDERR_SURFACELOST ) {    if (FAILED (hr = MyRestore() ) )    {       return hr;    } } else if( hr != DDERR_WASSTILLDRAWING )    {      return  hr;    } } return hr; } 

If the game is not running a flipping front and back buffer, all you need to do is wait for the vertical blank signal and copy the back buffer to the front buffer. If the video hardware supports flipping, waiting for the vertical blank is not necessary since the video card won't flip until the vertical blank is signaled.

As always, if the call to Blt() or Flip() fails, you'll want to restore your drawing surfaces since it is likely that something like a mode change has occurred. If the call fails because another draw is still in progress, the whole process repeats.

Updating the Game State

Anything that's not input or output falls into this huge category of game logic. This could include AI, physics, animations, triggers, etc. Games tend to handle an enormous amount of seemingly autonomous entities that come to life, stomp around the game world, and die off. Each of these game objects can affect the life cycle of other objects, such as a missile colliding and destroying an enemy vehicle. Back in the dark ages, circa 1991, each major subsystem of the game had a handler function.

 void MyUpdateGameLogic(unsigned int ms) {    PollInputDevices();    HandleUserInput(ms);    CalculateAI(ms);    ReticulateSplines(ms);    RunPhysicsSimulation(ms);    PokeAudioSystem(ms);    UpdateDisplayList(ms); } 

Each of these subsystems was called in a simple linear fashion. The internals of each function were completely customized, but generally they manipulated lists of objects and ran some code on each one, sometimes changing the members of the lists in the process.

This design wasn't very flexible. For example, if the audio system is disabled there is no reason to call PokeAudioSystem(). Perhaps the ReticulateSplines() call needs to happen at a different frequency than once per main loop. A more general system could be easily devised, that is based on cooperative multitasking.

Cooperative multitasking is a mechanism where each process gets a little CPU time in a round robin fashion. It's called cooperative because each process is responsible for releasing control back to the calling entity. If a process goes into an infinite loop the entire system will hang. The tradeoff for that weakness is that the system is simple to design and extremely efficient.

Imagine a simple class called CProcess with a single virtual method, OnUpdate():

 class CProcess { public:    virtual void OnUpdate(const int deltaMilliseconds); }; 

You could create objects inheriting from this class and stick them in a master process list. Every game loop, your code could traverse this list and call OnUpdate() for each object:

 typedef std::list<CProcess*> ProcessList; ProcessList g_ProcessList; void UpdateProcesses(int deltaMilliseconds) {    ProcessList::iterator i = g_ProcessList.begin();    while ( i != m_ProcessList.end() )    {      CProcess* p = *i;      p->OnUpdate( deltaMilliseconds );      ++i ;    } } 

The contents of the OnUpdate() overload could be anything. It could move the object on a spline, it could monitor the contents of a buffer stream and update it accordingly, it could run some AI code. It could render your screen, or monitor user interface objects like screens and buttons. At the highest level, your top-level function could look something like this:

 void main() {    if (CreateProcesses())    {      RunProcesses();    }    ShutdownProcesses(); } 

It may sound crazy, but Ultima VIII's main loop looked almost exactly like that, give or take a few lines.

There are a few wrinkles to this wonderful design that you should know about. If creating a system to handle your main loop were so easy as all that that I wouldn't bother devoting so much time to it. The first big problem comes when one process's OnUpdate() can destroy other processes, or even worse cause a recursive call to indirectly cause itself to be destroyed. Think of the likely code for a hand grenade exploding. The OnUpdate() would likely query the game object lists for every object in a certain range, and then cause all those objects to be destroyed in a nice fireball. The grenade object would be included in the list of objects in range, wouldn't it?

The solution to this problem involves some kind of reference counting system, or maybe a smart pointer. The SmartPtr class in Chapter 3 solves this problem well, and it will be used in the next section.

A Simple Cooperative Multitasker

A good process class should contain some additional data members and methods to make it interesting and flexible. There are as many ways to create this class as there are programmers, but this should give you a good start. There are three classes in this nugget of code:

  • class SmartPtr: A smart pointer class. The source code for this class was presented in Chapter 3. If you can't remember what a smart pointer is you'd better go back now and review.

  • class Cprocess: A base class for processes. You'll inherit from this class and redefine the OnUpdate() method.

  • class CprocessManager: This is a container and manager for running all your cooperative processes.

Here's the header file:

 #ifndef CPROCESS_H #define CPROCESS_H #include <list> #include "SmartPtr.h" ////////////////////////////////////////////////////////////////////// // Enums ////////////////////////////////////////////////////////////////////// // This process type enumeration is obviously subject to changes // based on the game design, for example, we are assuming the game will // have separate behaviors for voice, music, and sound effects // when in actuality, this engine will play all sound processes the same way enum PROCESS_TYPE {    PROC_NONE,    PROC_WAIT,    PROC_SPRITE,    PROC_CONTROL,    PROC_SCREEN,    PROC_MUSIC,    PROC_SOUNDFX,    PROC_GAMESPECIFIC }; ////////////////////////////////////////////////////////////////////// // Flags ////////////////////////////////////////////////////////////////////// static const int PROCESS_FLAG_ATTACHED           = 0x00000001; ////////////////////////////////////////////////////////////////////// // CProcess Implementation ////////////////////////////////////////////////////////////////////// class CProcess {    friend class CProcessManager; protected:    int                      m_iType;      // type of process running    bool              m_bKill;             // tells manager to kill and remove    bool              m_bActive;    bool              m_bPaused;    bool              m_bInitialUpdate;    // initial update?    SmartPtr<CProcess>       m_pNext; private:    unsigned int      m_uProcessFlags; public:    CProcess(int ntype, unsigned int uOrder = 0);    virtual ~CProcess(); public:    virtual bool             IsDead(void) const { return(m_bKill);};    virtual void             Kill();    virtual int              GetType(void) const { return(m_iType); };    virtual void             SetType(const int t) { m_iType = t; };    virtual bool             IsActive(void) const { return m_bActive; };    virtual void             SetActive(const bool b) { m_bActive = b; };    virtual bool             IsAttached()const;    virtual void             SetAttached(const bool wantAttached);    virtual bool             IsPaused(void) const { return m_bPaused; };    // call to pause a process    virtual void             TogglePause() {m_bPaused = !m_bPaused;}    bool                     IsInitialized()const { return ! m_bInitialUpdate; };    SmartPtr<CProcess> const GetNext(void) const { return(m_pNext);}    virtual void             SetNext(SmartPtr<CProcess> nnext);    virtual void             OnUpdate(const int deltaMilliseconds);    virtual void             OnInitialize(){}; private:    CProcess();                      //disable default construction    CProcess(const CProcess& rhs);   //disable copy construction }; inline void CProcess::OnUpdate( const int deltaMilliseconds ) {    if ( m_bInitialUpdate )    {       OnInitialize();       m_bInitialUpdate = false;    } } ////////////////////////////////////////////////////////////////////// // ProcessList is a list of smart CProcess pointers. ////////////////////////////////////////////////////////////////////// typedef std::list<SmartPtr<CProcess> > ProcessList; ////////////////////////////////////////////////////////////////////// // CProcessManager is a container for CProcess objects ////////////////////////////////////////////////////////////////////// class CProcessManager { public:    void UpdateProcesses(int deltaMilliseconds);    void DeleteProcessList();    bool IsProcessActive( int nType );    void Attach( SmartPtr<CProcess> pProcess );    bool HasProcesses(); protected:    ProcessList      m_ProcessList; private:    void Detach( SmartPtr<CProcess> pProcess ); }; }; #endif 

The source code for the CProcess base class and the CProcessManager follows:

 #include "stdafx.h" #include "CProcess.h" //////////////////////////////////////////////// // CProcess constructor // CProcess::CProcess( int ntype, unsigned int uOrder ) :    m_iType( ntype ),    m_bKill( false ),    m_bActive( true ),    m_uProcessFlags( 0 ),    m_pNext( NULL ),    m_bPaused( false ),    m_bInitialUpdate( true ) { } //////////////////////////////////////////////// // CProcess destructor - //   Note: always call Kill(), never delete. // CProcess::~CProcess() {    if (m_pNext)    {       m_pNext = NULL;    } } //////////////////////////////////////////////// // CProcess::Kill() - marks the process for cleanup // void CProcess::Kill() {    m_bKill=true; } //////////////////////////////////////////////// // CProcess::SetNext - sets a process dependancy // // A->SetNext(B)  means that B will wait until A is finished // void CProcess::SetNext(SmartPtr<CProcess> nnext) {    if (m_pNext)    {       m_pNext = NULL;    }    m_pNext = nnext; } /////////////////////////////////////////////////////////// // CProcess attachement methods // IsAttached() - Is this process attached to the manager? // SetAttached() - Marks it as attached. Called only by the manager. // bool CProcess::IsAttached() const {    return (m_uProcessFlags & PROCESS_FLAG_ATTACHED) ? true : false; } void CProcess::SetAttached(const bool wantAttached) {    if(wantAttached)    {      m_uProcessFlags |= PROCESS_FLAG_ATTACHED;    }    else    {      m_uProcessFlags &= ~PROCESS_FLAG_ATTACHED;    } } /////////////////////////////////////////////////////////// // CProcessManager::Attach - gets a process to run // void CProcessManager::Attach( SmartPtr<CProcess> pProcess ) {    m_ProcessList.push_back(pProcess);    pProcess->SetAttached(true); } /////////////////////////////////////////////////////////// // CProcessManager::Detach // - Detach a process from the process list, but don't delete it // void CProcessManager::Detach( SmartPtr<CProcess> pProcess ) {    m_ProcessList.remove(pProcess);    pProcess->SetAttached(false); } //////////////////////////////////////////////////// // CProcessManager::IsProcessActive //  - Are there any active processes of this type? // bool CProcessManager::IsProcessActive( int nType ) {    for(ProcessList::iterator i=m_ProcessList.begin();       i !=m_ProcessList.end(); ++i)    {       // Check for living processes.  If they are dead, make sure no children       // are attached as they will be brought to life on next cycle.       if ( ( *i )->GetType() == nType &&          ( ( *i )->IsDead() == false || ( *i )->m_pNext ) )         return true;    }    return false; } //////////////////////////////////////////////////// // CProcessManager::HasProcesses //  - Are there any processes at all? // bool CProcessManager::Has Processes() {    return !m_ProcessList.empty(); } //////////////////////////////////////////////////// // CProcessManager::DeleteProcessList //  - run through the list of processes and detach them // void CProcessManager::DeleteProcessList() {    for(ProcessList::iterator i = m_ProcessList.begin();       i != m_ProcessList.end(); )    {       Detach(* (i++) );    } } //////////////////////////////////////////////////////// // CProcessManager::DeleteProcessList //  - run through the list of processes and update them // void CProcessManager::UpdateProcesses(int deltaMilliseconds) {    ProcessList::iterator i = m_ProcessList.begin();    SmartPtr<CProcess> pNext(NULL);    while ( i != m_ProcessList.end() )    {      SmartPtr<CProcess> p = *i;      ++i;      if ( p->IsDead() )       {         // Check for a child process and add if exists         pNext = p->GetNext();         if ( pNext )         {            p->SetNext(SmartPtr<CProcess>(NULL));            Attach( pNext );         }         Detach( p );      }      else if ( p->IsActive() && !p->IsPaused() )      {        p->OnUpdate( deltaMilliseconds );      }    } } 

Most of the methods are self-explanatory. When you overload CProcess::OnUpdate() in the derived class, you'll call CProcess::Kill() on itself to mark the process for termination and cleanup—a process marked to be killed is a dead process. I'll show you a couple of examples in a moment.

Note the use of the SmartPtr class throughout. This is an excellent example of using smart pointers in a class that uses an STL list. Any reference to a SmartPtr <CProcess> object is managed by the smart pointer class, ensuring that the process object will remain in memory as long as there is a valid reference to it. The moment the last reference is cleared or reassigned, the process memory is finally freed. That's why the CProcessManager has a list of SmartPtr <CProcess> instead of a list of CProcess pointers.

CProcessManager::Attach() implements the insertion of a new process into an ordered process list. CProcessManager::UpdateProcesses() deserves some special attention. Recall that nearly 100% of the game code could be inside various overloads of CProcess::OnUpdate(). This game code can, and will cause game processes and objects to be deleted, all the more reason that this system uses smart pointers.

These classes and the classes that inherit from CProcess can run your entire game. Assuming we use the code from MyProcessNextFrame() described previously, it can use the CProcessManager class by changing one line of code:

 #include <mmsystem.h> const unsigned int SCREEN_REFRESH_RATE(1000/30); HRESULT MyProcessNextFrame() {    // Figure how much time has passed since the last time    m_dwCurrTick = timeGetTime();    // THIS LINE IS DIFFERENT! Call CProcessManager::UpdateProcesses()    g_pProcessManager->UpdateProcesses(m_dwCurrTick - m_dwLastUpdate);    m_dwLastUpdate = m_dwCurrTick;    // It is time to draw ?    if( m_runFullSpeed || ( (m_dwCurrTick - m_dwLastDraw) > SCREEN_REFRESH_RATE) )    {       if (S_OK == MyPaint())       {          // record the last successful paint          m_dwLastDraw = m_dwCurrTick;       }    }    return S_OK; } 

You could take the process manager a few steps further. Ultima VIII and Origin's Crusader: No Remorse had an extremely robust and complicated process management system that allowed processes to depend on the completion or failure of other processes. A process with a dependent would signal the waiting process when it could become active. It was possible to create code in Ultima VIII that looked like this:

 CWalkProcess *walk = new CWalkProcess(avatar, door); CAnimProcess *openDoor = new CAnimProcess(OPEN_DOOR, avatar, door); CAnimProcess *drawSword = new CAnimProcess(DRAW_WEAPON, avatar, sword); CCombatProcess *goBerserk = CCombatProcess(BERSERK, avatar); walk->then(openDoor)->then(drawSword)->then(goBerserk); 

It is easy to see that this code would begin a sequence of events that started with the Avatar walking to the door and finally going berserk with his sword out. The best part of this design was that if something got in the Avatar's way and abruptly ended the original walk animation, all the other processes would be cancelled.

A Tale from the Pixel Mines

One of the worst bugs I ever had the pleasure of finding was a bug in the core of the Ultima VIII process manager. Ultima VIII processes could attach their OnUpdate() calls to a real time interrupt, which was pretty cool. Animations and other events could happen smoothly without worrying about the exact CPU speed of the machine. The process table was getting corrupted somehow, and no one was sure how to find it as the bug occurred completely randomly—or so we thought. After tons of QA time and late nights we eventually found that jumping from map to map made the problem happen relatively frequently. We were able to track the bug down to the code that removed processes from the main process list. It turned out that the real time processes were accessing the process list at the same moment the list was being changed. Thank goodness we weren't on multiple processors; we never would have found it.

Examples of Classes that Inherit from CProcess

A very simple example of a useful process using this cooperative design is a wait process. This process is useful for inserting timed delays as the code example here shows:

 /////////////////////////////////////////////////////////////// // CWaitProcess /////////////////////////////////////////////////////////////// class CWaitProcess : public CProcess { protected:    unsigned int      m_uStart;    unsigned int      m_uStop; public:    CWaitProcess(CProcess* pParent, unsigned int iNumMill );    virtual void OnUpdate(const int deltaMilliseconds); }; CWaitProcess::CWaitProcess(CProcess* pParent, unsigned int iNumMill ) :    CProcess( PROC_WAIT, 0, pParent ),    m_uStart( 0 ),    m_uStop( iNumMill ) { } void CWaitProcess::OnUpdate( const int deltaMilliseconds ) {    CProcess::OnUpdate( deltaMilliseconds );    if ( m_bActive )    {       m_uStart += deltaMilliseconds;       if ( m_uStart >= m_uStop )         Kill();    } } 

Create an instance of CWaitProcess in this way:

 {    SmartPtr<CProcess> wait(new CWaitProcess(3000));    processManager.Attach(wait); } 

Take note of two things. First, you don't just "new up" a CWaitProcess and attach it to the CProcessManager. You have to use the SmartPtr template to manage CProcess objects. The second thing you'll notice is that the object, wait, is declared in a local scope. If you don't do this, the smart pointer will retain a reference as long as wait exists, and you probably don't want that, since the CProcessManager will manage the process object from here on out.

The wait process will hang out for three seconds and kill itself off. By itself it's a little underwhelming. Let's assume you've defined another process, called CKaboomProcess. You can then create a nuclear explosion with a three-second fuse, without a physics degree:

 {    // Open a local scope, create some processes,    // and set up their linkage. Since CProcess uses    // smart pointers to manage their life, we use a    // local scope to declare them    // The wait process will stay alive for three seconds    SmartPtr<CProcess> wait(new CWaitProcess(3000));    processManager.Attach(wait);    // The CKaboomProcess will wait for the CWaitProcess    // Note - it is not attached    SmartPtr<CProcess> kaboom(new CKaboomProcess());     wait->SetNext(kaboom); } 

The CProcess::SetNext() method sets up a simple dependency between the CWaitProcess and the CKaboomProcess. CKaboomProcess will remain inactive until the CWaitProcess is killed.

More Uses of CProcess Derivatives

Every updatable game object can inherit from CProcess. This includes display objects like screens, sprites, or animated characters. User interface objects such as buttons, edit boxes, or menus can inherit from CProcess. Audio objects such as sound effects, speech, or music make great use of this design because of the dependency and timing features.




Game Coding Complete
Game Coding Complete
ISBN: 1932111751
EAN: 2147483647
Year: 2003
Pages: 139

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