Sunday, August 18, 2013

Moving through time

Before I start, I know it's been a long time since my last post. Over the next couple days I'm going to write a series of posts about what I've been working on these last two weeks. So without further ado, here is the first one:

While I was coding in the last couple of weeks, I noticed that every time I came back to the main game from a debug window, the whole window hung for a good 6 seconds. After looking at my run() loop for a bit, I realized what the problem was. When I returned from the debug window, the next frame would have a massive deltaTime, which in turn caused a huge frame delay. This was partially a problem with how I had structured my frame delay calculation, but in the end, I needed a way to know when the game was paused, and to modify my deltaTime value accordingly.

To solve the problem, I came up with a pretty simple Clock class that tracks time, allows pausing, (and if you really wanted scaling/reversing):
/* Class for handling frame to frame deltaTime while keeping track of time pauses/un-pauses */
class Clock {
public:
    Clock(OSystem *system);

private:
    OSystem *_system;
    uint32 _lastTime;
    int32 _deltaTime;
    uint32 _pausedTime;
    bool _paused;

public:
    /**
     * Updates _deltaTime with the difference between the current time and
     * when the last update() was called.
     */
    void update();
    /**
     * Get the delta time since the last frame. (The time between update() calls)
     *
     * @return    Delta time since the last frame (in milliseconds)
     */
    uint32 getDeltaTime() const { return _deltaTime; }
    /**
     * Get the time from the program starting to the last update() call
     *
     * @return Time from program start to last update() call (in milliseconds)
     */
    uint32 getLastMeasuredTime() { return _lastTime; }

    /**
     * Pause the clock. Any future delta times will take this pause into account.
     * Has no effect if the clock is already paused.    
     */
    void start();
    /**
     * Un-pause the clock.
     * Has no effect if the clock is already un-paused.    
     */
    void stop();
};

I'll cover the guts of the functions in a bit, but first, here is their use in the main run() loop:
Common::Error ZEngine::run() {
    initialize();

    // Main loop
    while (!shouldQuit()) {
        _clock.update();
        uint32 currentTime = _clock.getLastMeasuredTime();
        uint32 deltaTime = _clock.getDeltaTime();

        processEvents();

        _scriptManager->update(deltaTime);
        _renderManager->update(deltaTime);

        // Update the screen
        _system->updateScreen();

        // Calculate the frame delay based off a desired frame time
        int delay = _desiredFrameTime - int32(_system->getMillis() - currentTime);
        // Ensure non-negative
        delay = delay < 0 ? 0 : delay;
        _system->delayMillis(delay);
    }

    return Common::kNoError;
}

And lastly, whenever the engine is paused (by a debug console, by the Global Main Menu, by a phone call, etc.), ScummVM core calls pauseEngineIntern(bool pause), which can be overridden to implement any engine internal pausing. In my case, I can call Clock::start()/stop()
void ZEngine::pauseEngineIntern(bool pause) {
    _mixer->pauseAll(pause);

    if (pause) {
        _clock.stop();
    } else {
        _clock.start();
    }
}

All the work of the class is done by update(). update() gets the current time using getMillis() and subtracts the last recorded time from it to get  _deltaTime. If the clock is currently paused, it subtracts off the amount of time that the clock has been paused. Lastly, it clamps the value to positive values.
void Clock::update() {
    uint32 currentTime = _system->getMillis();

    _deltaTime = (currentTime - _lastTime);
    if (_paused) {
        _deltaTime -= (currentTime - _pausedTime);
    }

    if (_deltaTime < 0) {
        _deltaTime = 0;
    }

    _lastTime = currentTime;
}

If you wanted to slow down or speed up time, it would be a simple matter to scale _deltaTime. You could even make it negative to make time go backwards. The full source code can be found here and here.

Well that's it for this post. Next up is a post about the rendering system. Until then, happy coding!

-RichieSams

No comments:

Post a Comment