Trouble with the canonical game loop

Sage
Posts: 1,199
Joined: 2004.10
Post: #1
So, a while back folks helped me with some rendering issues for my foliage system -- that help is greatly appreciated.

One comment by DoG has nagged at me, however (http://idevgames.com/forum/showpost.php?...stcount=30)

Quote:I find it a bit odd that the performance on the X1600 and GF8600 is about the same. I have the impression that you are doing something very wonky syncing drawing to your physics simulation.

I realize that at some point my engine has gone from buttery smooth to kind of herky-jerky. And furthermore, at some point I've gone from solid VBL-synced frame rates ( 60, 30, 15... ) to non VBL synced rates, like 20, or 40, etc.

I know my context is VBL synced. The first thing I checked was that some absurd regression didn't un-sync my context. So I have to assume that something's wonky about my game loop.

In a nutshell, the story is this: When my framerates match up to VBL sync rates ( 60, 30, 15 ) my gameplay is smooth as silk. When my framerates don't match up, I get very jerky motion.

Also, I've read the articles on game loops -- I even gave a stab at the loop that's been linked to here at iDevGames a hojillion times -- http://www.sacredsoftware.net/tutorials/...tion.xhtml

Code:
void
DisplayDelegate::update( void )
{
    if ( !_animating )
    {
        lerr << "DisplayDelegate::update - update shouldn't ever be called on a non-animating DisplayDelegate" << std::endl;
        return;
    }

    switch( _gameLoop )
    {
        case GameLoop::SGFGameLoop:
        {
    
            //
            //  Update time and find out how much time
            //  has elapsed since last call to ::update
            //  
            //  Then add whatever was left over from last iteration.
            //

            _currentTime = realtime::now();
            
            double dt = _currentTime - _lastTime,
                   interval = double( _time.interval );
                  
            _lastTime = _currentTime;

            double availableTime = dt + _timeRemainder;
            
            if ( availableTime < interval )
            {
                _timeRemainder += dt;
            }
            else
            {
                double steps = availableTime / interval;
                int numSteps = lrint( steps );
                
                for ( int i=0; i < numSteps; i++ )
                {
                    dispatchStep();
                }

                //
                //  Increment remainder
                //

                _timeRemainder = double(steps - double( numSteps )) * interval;
            }
            
            dispatchDisplay();
            break;
        }
        
        case GameLoop::SacredSoftwareGameLoop:
        {
            /*  
                From:
                http://www.sacredsoftware.net/tutorials/Animation/TimeBasedAnimation.xhtml
                            
                #define MAXIMUM_FRAME_RATE 120
                #define MINIMUM_FRAME_RATE 15
                #define UPDATE_INTERVAL (1.0 / MAXIMUM_FRAME_RATE)
                #define MAX_CYCLES_PER_FRAME (MAXIMUM_FRAME_RATE / MINIMUM_FRAME_RATE)
                
                void runGame() {
                    static double lastFrameTime = 0.0;
                    static double cyclesLeftOver = 0.0;
                    double currentTime;
                    double updateIterations;
                    
                    currentTime = GetCurrentTime();
                    updateIterations = ((currentTime - lastFrameTime) + cyclesLeftOver);
                    
                    if (updateIterations > (MAX_CYCLES_PER_FRAME * UPDATE_INTERVAL)) {
                        updateIterations = (MAX_CYCLES_PER_FRAME * UPDATE_INTERVAL);
                    }
                    
                    while (updateIterations > UPDATE_INTERVAL) {
                        updateIterations -= UPDATE_INTERVAL;
                        
                        updateGame(); // Update game state a variable number of times
                    }
                    
                    cyclesLeftOver = updateIterations;
                    lastFrameTime = currentTime;
                    
                    drawScene(); // Draw the scene only once
                }              
            */
            
            const double MaxFrameRate = _time.targetStepsPerSecond,
                         MinFrameRate = 30,
                         UpdateInterval = _time.interval,
                         MaxCyclesPerFrame = ( MaxFrameRate / MinFrameRate );
            
            _currentTime = realtime::now();
            double updateIterations = ((_currentTime - _lastTime) + _cyclesLeftOver);
            
            updateIterations = std::min( updateIterations, MaxCyclesPerFrame * UpdateInterval );

            while( updateIterations > UpdateInterval )
            {
                updateIterations -= UpdateInterval;
                dispatchStep();
            }
            
            _cyclesLeftOver = updateIterations;
            _lastTime = _currentTime;
            
            dispatchDisplay();
            break;
        }
    }
}

void
DisplayDelegate::dispatchStep( void )
{
    _time.now += _time.interval;

    preStep( _time );
    step( _time );
    postStep( _time );
    
    _time.stepsExecuted++;      
}

void
DisplayDelegate::dispatchDisplay( void )
{
    glViewport( 0, 0, _contextSize.x, _contextSize.y );

    preDisplay( _time );
    display( _time );
    postDisplay( _time );
    
    _time.framesRendered++;
}

Finally, this is driven by an NSTimer with a time interval of zero. The timer blocks on [[NSOpenGLContext currentContext] flushBuffer] -- which itself blocks on VBL sync. So this method shouldn't ever get called more than 60 times per second.

--------------------

SGFGameLoop gives me the herky-jerky motion when framerates don't land on VBL sync boundaries. But I get a rock solid steps per second, no matter what -- e.g., if I ask for 120 steps per second, I get it, no matter how slow the current framerate is.

SacredSoftwareGameLoop is buttery smooth ( nice! ) but the stepping rate suffers when framerate goes down. So, if I ask for 120hz update rate, it only actually runs at 120hz when the rendering load is very light. As soon as I'm rendering a complex scene, my update hz goes down to 80 or 60 or so, and the gameplay slows accordingly.

Note, since I'm using ODE physics, a fixed timestep is critical -- as such, it's imperative that if I ask for 120 hz, I get 120 hz.

This post is long enough already -- I apologize. Any suggestions would be greatly appreciated!
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #2
The first thing I'd do is try dropping MinFrameRate = 30, down to like 4. In my experience, there's no reason to limit it higher than that. If you do, it throttles back your update dramatically when FPS gets low. The lower limit gives as much room as possible to the update, even when graphics are dog slow. Once everything is down to 4 fps, the user already knows intuitively that it's over-taxed and is not surprised when it gets worse from there as the simulation itself starts jerking and slowing down.
Quote this message in a reply
Sage
Posts: 1,482
Joined: 2002.09
Post: #3
I do more or less the same sort of game loop in my own games and it works really well. Though like AnotherJake said to avoid the game slowing down, I give it a pretty generous maximum frameskip of 10 frames or so.

I've never went out of my way to make sure that the game ran at an even factor of 60 and I think it's mostly fine. Everyone seems to have a different opinion on that though. What is even more effective is to interpolate the drawing based on the fixed update loop. That will give you a much smoother appearing result IMO but is tricky to implement.

Also, 20 is as much of a factor of 60 as 30 and 15 are. Wink

Scott Lembcke - Howling Moon Software
Author of Chipmunk Physics - A fast and simple rigid body physics library in C.
Quote this message in a reply
Member
Posts: 440
Joined: 2002.09
Post: #4
Skorche Wrote:What is even more effective is to interpolate the drawing based on the fixed update loop.
I want to emphasize this point - You might have read this already but grab the example source from here: http://gafferongames.com/game-physics/fi...-timestep/ and pay close attention to the interpolation step. Drawing will lag up to one frame but this will smooth things out regardless of how many fixed steps you've run during the frame.
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #5
On that topic: I think Bullet physics has an interpolation utility too. I haven't tried it yet though.
Quote this message in a reply
DoG
Moderator
Posts: 869
Joined: 2003.01
Post: #6
I do things a bit differently, maybe simpler:

http://www.pasteit4me.com/24004

(assumes simTime initially equal to time)

I don't care about minimum framerate in that code, as I assume physics is always able to catch up. One could opt to break out of the while (simTime < time) loop after N steps, to slow down gameplay when the framerate is low, but I mostly assume it's the rendering's fault Wink

That code executes as many steps as were required for the previous frame. If there is a large jitter in the frame execution times, it might not work well, but I think it has fewer pathological cases and artifacts than more complex time advancement methods.

Your code does
Code:
_timeRemainder = double(steps - double( numSteps )) * interval;
and the Sacredsoftware code does
Code:
updateIterations = ((_currentTime - _lastTime) + _cyclesLeftOver);
and these are things I really don't get Smile

I mean, why all this magic?
Quote this message in a reply
Moderator
Posts: 3,570
Joined: 2003.06
Post: #7
DoG Wrote:I do things a bit differently, maybe simpler:

http://www.pasteit4me.com/24004

(assumes simTime initially equal to time)

Nice! Looks like it should work equally as well since it's doing the same thing: using more or less steps to catch up to, or make sure it isn't updated more than real time. I just gave it a try in some of my own stuff and it seems to fit in and work perfectly. Simplifying it for my use:

Code:
    double time = CFAbsoluteTimeGetCurrent();
    static double simTime = 0.0;
    if (simTime == 0.0)
        simTime = time;
    
    while (simTime < time)
    {
        [self update];
        simTime += dt; // dt is fixed time step
    }
    [self Draw];

It certainly does cut down on all the extra drama. One can still set a minimum frame rate to start skipping updates too.
Quote this message in a reply
Sage
Posts: 1,199
Joined: 2004.10
Post: #8
I'm going to give this new approach a shot!

That being said, per the suggestion of lowering the minimum framerate that did do the trick, the sacred software loop now holds at 120 solid -- but I still get the stuttering. My suspicion is that the stuttering happens when the current framerate doesn't divide equally into the stepping rate and leftover updates pile up and have to be executed en masse.
Quote this message in a reply
Moderator
Posts: 1,560
Joined: 2003.10
Post: #9
Nice, that's a good bit simpler. Might have to update my tutorial... The only concern I'd have would be running out of floating point precision after it ran for long enough, but with a double, it'd take a reeeeeaaally long time...
Quote this message in a reply
Sage
Posts: 1,199
Joined: 2004.10
Post: #10
Well, for what it's worth this simpler game loop consistently gives me a stepping rate consistently higher than the target -- e.g., 135 instead of 120.
Quote this message in a reply
Member
Posts: 440
Joined: 2002.09
Post: #11
TomorrowPlusX Wrote:My suspicion is that the stuttering happens when the current framerate doesn't divide equally into the stepping rate and leftover updates pile up and have to be executed en masse.
This is one situation where frame interpolation will help. Then there's the opposite, where you're drawing frames and not stepping at all.

@DoG - That's certainly a cleaner approach, though I'd think you'll still want to cap dt to some maximum value, say 1/10 sec (and throw real time out the window) if stuff is really bogging down. Also, if you're using CFAbsoluteTimeGetCurrent make sure dt is always >=0. It's rare, but external clock syncs can give you a negative delta, and a potentially large one at that. That should also ease any worries of numerical overflows.
Quote this message in a reply
Member
Posts: 87
Joined: 2006.08
Post: #12
Frank C. Wrote:@DoG - That's certainly a cleaner approach, though I'd think you'll still want to cap dt to some maximum value, say 1/10 sec (and throw real time out the window) if stuff is really bogging down. Also, if you're using CFAbsoluteTimeGetCurrent make sure dt is always >=0. It's rare, but external clock syncs can give you a negative delta, and a potentially large one at that. That should also ease any worries of numerical overflows.

CFAbsoluteTime isn't the time the user sees, and is not impacted by time zones, etc. Absolute time should _never_ go backwards.
Quote this message in a reply
Member
Posts: 440
Joined: 2002.09
Post: #13
Frogblast Wrote:CFAbsoluteTime isn't the time the user sees, and is not impacted by time zones, etc. Absolute time should _never_ go backwards.

I'm just going by the Apple docs for CFAbsoluteTimeGetCurrent: http://developer.apple.com/documentation...rence.html

Quote:Repeated calls to this function do not guarantee monotonically increasing results. The system time may decrease due to synchronization with external time references or due to an explicit user change of the clock.

I also experienced negative deltas first hand on the iPhone (though that may have been a bug in an early OS). But either way IMHO, it can't hurt to check for and fix negative deltas even if they should never happen.
Quote this message in a reply
Moderator
Posts: 1,560
Joined: 2003.10
Post: #14
Frank C. Wrote:I'm just going by the Apple docs for CFAbsoluteTimeGetCurrent: http://developer.apple.com/documentation...rence.html

Yep, looks like wall time. Use mach_absolute_time for a monotonically increasing value.
Quote this message in a reply
Member
Posts: 440
Joined: 2002.09
Post: #15
ThemsAllTook Wrote:Yep, looks like wall time. Use mach_absolute_time for a monotonically increasing value.
I do, but I'm still paranoid enough to check for negative deltas Wink
Quote this message in a reply
Post Reply 

Possibly Related Threads...
Thread: Author Replies: Views: Last Post
  iPhone game loop agreendev 2 4,052 Jul 27, 2010 11:32 AM
Last Post: AnotherJake
  Proper game update loop in Cocoa? LoneIgadzra 13 6,301 May 30, 2008 05:49 AM
Last Post: ThemsAllTook
  Controlling Game Loop Nick 2 3,086 Jul 21, 2007 07:12 PM
Last Post: Nick
  Good Game Loop with Physics Units blobbo 6 4,550 Oct 14, 2005 02:59 AM
Last Post: unknown
  Game Loop in Cocoa robmcq 9 5,914 Sep 27, 2005 10:49 AM
Last Post: akb825