Proper frame rate management

Started by
4 comments, last by eFoDay 14 years, 5 months ago
For the past year or so, I have been putting together a rendering engine for a 2D game engine that I am attempting to create using SDL. I have spent most of my time optimizing for performance, reorganizing code into an easily reusable form, and adding image, label, animation, shape, button, panel, check box, track bar, etc. classes. Now, I have reached an impasse: proper frame rate management. Over the course of creating the rendering engine, I have tried several methods for controlling frame rate: - I've tried capping the frame rate through timing frame and delaying to match the target FPS:
int ticks = SDL_GetTicks();

while( !quit ) {
  mainLoop();
}

if( ( SDL_GetTicks() - ticks < 1000 / FRAMES_PER_SECOND ) ) {
  SDL_Delay( ( 1000 / FRAMES_PER_SECOND ) - (SDL_GetTicks() - ticks) );
}
But this only caused a strange stuttering problem where an object would move smooth for a bit, then kinda stutter, and then would move smooth again, and the process would repeat. - I've using frame independent movement. Basically, I did this:

void moveObjects( float speed_factor ) {
  obj->x += vel * speed_factor;
}
 
int main() {
  float ticks = SDL_GetTicks();

  while( !quit ) {
    moveObjects( (SDL_GetTicks() - ticks) / 1000.f ); 
    ticks = SDL_GetTicks();
    render();
  }
}
This fixed the problem, but I HATE the fact that I have to multiply all velocities by a speed_factor. It causes problems in the way I want to program and I noticed that it caused the objects that were moving to not always end up in the same place even when the condition should be the same... - I also tried a method that I thought up that kinda worked, but kinda didn't:

while( !quit ) {
  //if it has been >= the interval_ since the loop was last run, run it
  if( SDL_GetTicks() - ticks_ >= interval_ ) {
    // over_interval = SDL_GetTicks() - ticks_ - interval_
    // ticks = SDL_GetTicks() - (over_interval - ToMultiple(over_interval, interval_))
    //       = SDL_GetTicks() - SDL_GetTicks() + ticks_ + interval_ + ToMultiple(over_interval, interval_))
    //       = ticks_ + interval_ + ToMultiple(over_interval, interval_))

    ticks_ += interval_ + ToMultiple( SDL_GetTicks()-ticks_-interval_, interval_ );

    mainLoop();
  }
}
This method runs the main loop only after a certain amount of time has passed. It is similar to the first method for capping the fps. This caused the stuttering problem only with any fps cap that wasn't 25. 25 fps ran somewhat smooth, but every once in a while, the stuttering problem would rear its ugly head again. Please, someone give me a way to manage the frame rate without multiplying a factor by all velocities and without capping by timing the frame and delaying!
Advertisement
I'm afraid you must do one of those. A way that I think works well is the following:
while(currentGameTime+timePerFrame < currentRealTime) { processOneGameFrame(); currentGameTime += timePerFrame;}drawGameFrame();

This will advance the game in fixed time-steps, always putting the game-state at the closest time before the current real time. This solves the problem of advancing the game too fast, if your loop runs too fast, as well as letting the game catch up by calculating two frames at the same time if something causes a slight lag in the main loop.
You separate proccess() and draw() from each other, where process only moves objects, and draw only draws them. This way if your draw() is too expensive it will automatically only run every other update too, making your game always run at the same speed.

This can always lead to some stuttering. If you advance your game at 25 FPS, and the screen refreshes at 60Hz, then most of the time you will advance the game every second refresh, but sometimes it will take 3 refreshes, which can cause slight jumps.
If that is not acceptable, then the solution is to modify draw() to take the time since the last frame, and multiply with that factor in draw() instead. This is good since it can remove the stuttering, but it won't mess with your game updates, so results will always be predictable.

For example:
while(currentGameTime+timePerFrame < currentRealTime) { processOneGameFrame(); currentGameTime += timePerFrame;}drawGameFrame((float)(currentRealTime-currentGameTime)/timePerFrame);...void drawGameFrame(float progressTowardsNextFrame) { estimatedPosition = currentPosition + (nextPosition - currentPosition) * progressTowardsNextFrame;  drawCharacter(player, estimatedPosition);}

Here you actually calculate that estimated non-exact position, and draw the character at that position. That will interpolate nicely, but since it's recalculated each frame and only used for drawing, it never affects the actual game state or causes precision bugs. You get all the good parts of frame-independent animation, while retaining precision and determinism.
Why in the world you you want to cap your rendering frame rate? Your *simulation* should run at a fixed step, independently of your rendering, which should run as fast as possible.
The way my engine works makes it hard for the rendering and the simulation to be separated.

Here are the basics my engine:
- It's a class hierarchy of CObject->CEventHandler->CObjectHandler
- CObject controls position, dimensions, visibility, and a parent object
- Objects that only want the basic necessities to draw to a parent inherit from here
- All CObjects must have a drawTo( CCanvas* ); function.

- CEventHandler, for the moment, handles mouse events tied to the object
- Objects that also want to be considered in the event poll inherit from here

- CObjectHandler handles the drawing of child objects to itself
- Objects that also want to render a scene on themselves inherit from here
- includes a drawChildren( CCanvas*, CRect ); function.

- CParent_Interface - all parent objects must inherit from this interface as well
- CWindow - a class outside the hierarchy, inherits from CParent_Interface, and controls the window related functions.

So, here is what a basic program might look like:
int main() {  CWindow *window = new CWindow( 800, 600, e_bpp::BPP_32 );  window->setClearColor( 0, 0, 255 );  CImage *image = new CImage( window ); // image is set to render on the window  image->setW( 100 );                   // width is set  image->setH( 100 );                   // height is set  //etc.  CPanel *panel = new CPanel( window ); // panel is set to render on the window  panel->setX( 200 );                   // x is set  panel->setY( 200 );                   // y is set  panel->setW( 200 );                   // width is set  panel->setH( 200 );                   // height is set  CUnit *player = new CUnit( panel ); // player is set to render on the panel  while( !quit ) {    window->clear();    // move objects and do other stuff ...        window->drawChildren();    window->flip();  }  delete( window ); //handles shutdown and deletion of surfaces and objects  }

Now, heres what's going on behind the scenes:
// CWindow::drawChildren() calls all its children's drawTo( CCanvas* ) functions.// The drawTo function is responsible for updating the object, then drawing it void CWindow::drawChildren() {  CRect draw_rect = (CRect){0, 0, getW(), getH()};  for( unsigned int x = 0; x < numChildren(); ++x ) {    if( Overlaps( draw_rect, getChild(x)->getRect() ) ) {      getChild(x)->drawTo( canvas_ );    }  }}// CPanel updates by filling a the rect, drawing bevels and then drawing children// to itself, then drawing itself to the destination canvasint CPanel::drawTo( CCanvas *canvas ) {  if( isVisible() && canvas != NULL && canvas_ != NULL ) {    canvas_->fillRect( NULL, fill_color_ );    if( bevel_outer_ == LOWERED ) {      canvas_->drawBevelLowered();    } else if( bevel_outer_ == RAISED ) {      canvas_->drawBevelRaised();    }    if( bevel_inner_ == LOWERED ) {      canvas_->drawBevelLowered(1, 1);    } else if( bevel_inner_ == RAISED ) {      canvas_->drawBevelRaised(1, 1);    }    //draw children    CRect draw_rect = (CRect){0, 0, getW(), getH()};    drawChildren( canvas_, draw_rect );    return canvas->blit( getX(), getY(), canvas_ );  }  return 0;}

Those two examples only deal with updating the graphics, but I also wanted to tie in the unit mechanic closely with the graphics pipeline for improved performance:
int CUnit::drawTo( CCanvas* canvas ) {  //update CUnit, meaning its states, inventory, etc, and draw it to the canvas.}

My thinking is that if I already have one giant for loop that loops through all possible objects that are going to be rendered, why not include the game logic in there so I don't have to create a second giant loop to process game logic, slowing the engine down? Am I thinking about this wrong?

Above all else, I want my rendering engine to be a union of performance and quality. My engine can render over 10000 10x10 SDL_Surfaces without dropping below 30 fps, doesn't take much memory to run, and is simple to use. The only thing that really bothers me is this damn stuttering problem ><...
It shouldn't slow anything down significantly, but you'd have to try it. Using a profiler to determine what part of your program takes the most time is a very good idea if you want to analyze such matters. Try AMD CodeAnalyst for example, which is free (you don't need an AMD processor), and integrates with VC++.

Not wanting to change everything is very understandable though, but without matching animation to the monitor refreshrate you won't be able to completely remove stuttering. You could pick a very common refreshrate of 60 Hz, and use 30 or 60 FPS as the gametime, but there will always be people that use different update frequencies.
Using separate logic and drawing makes sense in my opinion. What if you pause the game for example?
With that method you only have to stop updating. The same goes for slow-motion or temporary slow-downs where you can skip drawing frames without skipping updates and similar.
I havn't used this code but maybe it will help you.
I found it at jnrdev.

while (!done){	framestart = SDL_GetTicks();	//handle messages	while(SDL_PollEvent(&event)){		switch(event.type){			case SDL_QUIT:				done = true;			break;			default:			break;		}	}	if(keystates[SDLK_ESCAPE])		//quit?		done = true;	//update objects	//draw everything	//double buffering -> flip buffers	SDL_Flip(screen);	//keep framerate constant at 1000/WAITTIME fps	while((SDL_GetTicks()-framestart) < WAITTIME);}

This topic is closed to new replies.

Advertisement