Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
101 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

The Task Pool

The 'task pool' is the term I use to refer to the group of tasks that the engine is running at any given time. There are certain tasks that run pretty much all of the time - 'system tasks' - such as the timer or input tasks. These system tasks are what we're going to look at now.

Timer

The global timer task will be responsible for working out how many seconds have passed since the last frame. We can use that number to scale things like physics code, so that things move at the same speed across different machines:

class CGlobalTimer : public ITask
{
public:
  AUTO_SIZE;
  
  static float dT;
  static unsigned long lastFrameIndex;
  static unsigned long thisFrameIndex;

  bool Start();
  void Update();
  void Stop();
};

bool CGlobalTimer::Start()
{
  thisFrameIndex=SDL_GetTicks();
  lastFrameIndex=thisFrameIndex;
  dT=0;
  return true;
}

void CGlobalTimer::Update()
{
  lastFrameIndex=thisFrameIndex;
  thisFrameIndex=SDL_GetTicks();
  dT=((float)(thisFrameIndex-lastFrameIndex))/1000.0f;
}

void CGlobalTimer::Stop()
{

}

SDL_GetTicks() returns the number of milliseconds since SDL_Init() was called, which we store in thisFrameIndex. To work out the elapsed time for this frame, we subtract the previous frame's value from that value, and divide by 1000 (to convert from milliseconds to seconds). The result is stored in a public static variable for easy access (so technically we should make the CGlobalTimer a Singleton, to prevent anyone creating more than one of it, but I didn't because multiple inheritance is something I wanted to avoid, if possible).

Sound

The sound task will initialize and shutdown the sound system, as well as pausing all active sounds when the task is paused. When pausing, we need to store which channels are actually active so we know which ones to unpause - the game itself might have paused some channels for it's own ends, and we don't want to accidentally unpause them.

class CSoundTask : public ITask
{
public:
  bool Start();
  void OnSuspend();
  void Update();
  void OnResume();
  void Stop();

  AUTO_SIZE;

protected:
  CMMPointer<CMMDynamicBlob<bool> > isPaused;
};

bool CSoundTask::Start()
{
  if(FALSE==FSOUND_Init(44100, 32, 0))return false;
  return true;
}

void CSoundTask::OnSuspend()
{
  //pause all channels, storing the pause state in the isPaused array
  //once the states are stored we can use FSOUND_ALL to pause all
  //channels the easy way
  int chCount=FSOUND_GetMaxChannels();
  isPaused=new CMMDynamicBlob<bool>(chCount);
  for(int i=0;i<chCount;i++)
  {
    if(FSOUND_IsPlaying(i))
    {  
      isPaused->buffer[i]=true;
    }else{
      isPaused->buffer[i]=false;
    }
  }
  FSOUND_SetPaused(FSOUND_ALL,TRUE);
}

void CSoundTask::Update()
{
  //we don't need to do anything, FMOD does it all for us :)
}

void CSoundTask::OnResume()
{
  //unpause all the flagged channels
  if(isPaused)
  {
    int chCount=FSOUND_GetMaxChannels();
    for(int i=0;i<chCount;i++)
    {
      if(isPaused->buffer[i])FSOUND_SetPaused(i,FALSE);
    }
    isPaused=0;
  }
}

void CSoundTask::Stop()
{
  FSOUND_Close();
}

Input

The input task has to get SDL to update it's internal input information, and then it has to read that information out. Again, we use public static variables for easy access (so again, I should make this a Singleton, but I haven't).Something to note is that SDL_GetKeyState returns a pointer to SDL's internal array - so we shouldn't free it ourselves.

class CInputTask : public ITask  
{
public:
  CInputTask();
  virtual ~CInputTask();

  bool Start();
  void Update();
  void Stop();

  static unsigned char *keys;
  static CMMPointer<CMMDynamicBlob<unsigned char> > oldKeys;
  static int keyCount;
  
  static int dX,dY;
  static unsigned int buttons;
  static unsigned int oldButtons;

  static bool inline curKey(int index) { return (keys[index]!=0); }
  static bool inline oldKey(int index) { return ((*oldKeys)[index]!=0); }

  //some helper functions to make certain things easier
  static bool inline keyDown(int index)
      { return ( curKey(index))&&(!oldKey(index)); }
  static bool inline keyStillDown(int index)
      { return ( curKey(index))&&( oldKey(index)); }
  static bool inline keyUp(int index)
      { return (!curKey(index))&&( oldKey(index)); }
  static bool inline keyStillUp(int index)
      { return (!curKey(index))&&(!oldKey(index)); }

  static bool inline curMouse(int button)
      { return (buttons&SDL_BUTTON(button))!=0; }
  static bool inline oldMouse(int button)
      { return (oldButtons&SDL_BUTTON(button))!=0; }

  static bool inline mouseDown(int button)
      { return ( curMouse(button))&&(!oldMouse(button)); }
  static bool inline mouseStillDown(int button)
      { return ( curMouse(button))&&( oldMouse(button)); }
  static bool inline mouseUp(int button)
      { return (!curMouse(button))&&( oldMouse(button)); }
  static bool inline mouseStillUp(int button)
      { return (!curMouse(button))&&(!oldMouse(button)); }

  AUTO_SIZE;
};

bool CInputTask::Start()
{
  keys=SDL_GetKeyState(&keyCount);
  oldKeys=new CMMDynamicBlob<unsigned char>(keyCount);
  dX=dY=0;
  SDL_PumpEvents(); SDL_PumpEvents();
  return true;
}

void CInputTask::Update()
{
  SDL_PumpEvents();

  oldButtons=buttons;
  buttons=SDL_GetRelativeMouseState(&dX,&dY);

  memcpy((unsigned char*)(*oldKeys),keys,sizeof(unsigned char)*keyCount);
  keys=SDL_GetKeyState(&keyCount);
}

void CInputTask::Stop()
{
  keys=0;
  oldKeys=0;
}

What's with the oldKeys and oldButtons members? At any given time, if you check a key in the keys array, all you'll know is if the key is down; not if it's just been pressed, or if it's being held down, or if it's just been released, and so on. By comparing it to it's previous state, oldKeys, we can quickly see if it's going down, going up, or staying put. Same goes for the mouse buttons. That's what all those inline functions are for you could write a separate 'input event' task which watches for those sorts of conditions and translates them into 'events' in a queue - a little more useful for things like text entry (because otherwise you just have to check every key, every frame).

Renderer

It's time we got something significant on screen. The VideoUpdate task will be responsible for starting up and shutting down the video system, along with swapping the screen buffers (because we're working with double buffers). It's also the first part of the engine to use the settings mechanism - we're going to have the screen mode (width, height, and BPP) registered as settings.

class CVideoUpdate : public ITask  
{
public:
  CVideoUpdate();
  virtual ~CVideoUpdate();
  AUTO_SIZE;

  static int scrWidth, scrHeight, scrBPP;
  static CMMPointer<Dator<int> > screenWidth, screenHeight, screenBPP;

  bool Start();
  void Update();
  void Stop();
};

bool CVideoUpdate::Start()
{
  assert(screenWidth && screenHeight && screenBPP);

  if(-1==SDL_InitSubSystem(SDL_INIT_VIDEO))
  {
    CLog::Get().Write(LOG_CLIENT,IDS_GENERIC_SUB_INIT_FAIL,
        "Video",SDL_GetError());
    return false;
  }
  SDL_GL_SetAttribute( SDL_GL_ALPHA_SIZE, 8 );
  SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 8 );
  SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 8 );
  SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 8 );
  SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );
  SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );

  int flags = SDL_OPENGL | SDL_ANYFORMAT | SDL_FULLSCREEN;

  if(!SDL_SetVideoMode(scrWidth, scrHeight, scrBPP, flags))
  {
    CLog::Get().Write(LOG_CLIENT, IDS_BAD_DISPLAYMODE,
        scrWidth, scrHeight, scrBPP, SDL_GetError());
    return false;
  }

  //hide the mouse cursor
  SDL_ShowCursor(SDL_DISABLE);

  return true;
}

void CVideoUpdate::Update()
{
  SDL_GL_SwapBuffers();
}

void CVideoUpdate::Stop()
{
  SDL_QuitSubSystem(SDL_INIT_VIDEO);
}

There. We also need to head back to the CSettingsManager, and add the following to CreateStandardSettings:

SETTING(int, CVideoUpdate::screenWidth,  CVideoUpdate::scrWidth,  "screenX");
SETTING(int, CVideoUpdate::screenHeight, CVideoUpdate::scrHeight, "screenY");
SETTING(int, CVideoUpdate::screenBPP,    CVideoUpdate::scrBPP,    "screenBPP");

Also, we add to DestroyStandardSettings:

CVideoUpdate::screenWidth  = 0;
CVideoUpdate::screenHeight  = 0;
CVideoUpdate::screenBPP  = 0;

The parameters for the SETTING macro are, in case you'd forgotten, the type, dator, variable to bind the dator to, and name for the setting within the manager. Finally, it's worth noting the static definitions of scrWidth/scrHeight/scrBPP:

int CVideoUpdate::scrWidth=800;
int CVideoUpdate::scrHeight=600;
int CVideoUpdate::scrBPP=16;

If no setting is given for screenX/screenY/screenBPP in the settings file or on the command line, no assignments will be made to the relevant dators and so scrWidth/scrHeight/scrBPP will not be changed from their initial values. Thus, set them up with your default values.



Putting it all together

Contents
  Entry Points
  The Task Pool
  Putting it all together

  Source code
  Printable version
  Discuss this article

The Series
  Part I
  Part II
  Part III
  Part IV
  Part V