Sprites, fixed timestep and interpolation

Started by
9 comments, last by tookie 7 years ago

Hello

After reading the famous article Fix your timestep, I'm trying to implement 2D sprite interpolation in my engine

This is my sprite state struct


// Sprite state for the interpolation
struct SpriteState
{
    Vec2  position;
    float angle;
}; 

My interpolation functions


// Return an interpolated position between previous and current state
// Scalar
inline auto calcInterpolation(float previous, float current, float alpha) -> float
{
    return current * alpha + previous * (1.0f - alpha);
}

// Vectors
inline auto calcInterpolation(const Vec2& previous, const Vec2& current, float alpha) -> Vec2
{
    return { current * alpha + previous * (1.0f - alpha) };
} 

My Sprite class looks like this


class Sprite
{
    public:

        // ctor
        Sprite(const Texture2D& texture, const AABB<int>& rect, const AnimationState& animState = {});

        /// just to test
        void useInterpolation(bool active = true);
        /// just to test

        void setPosition(const Vec2& pos);
        void setRotation(float angle);

        void update(float dt);
        void render(QuadRenderer& quadRenderer, float alpha);

    private:

        /// just to test
        bool m_usingInterpolation = true;
        /// just to test

        // Sprite states for the interpolation
        // (position and rotation)
        SpriteState m_currState, m_prevState;
        // Other transformations
        Transformations m_transform;
        // Animation state for this sprite
        AnimationState m_animState;

        // Sprite coordinates from the Texture Atlas
        AABB<int> m_spriteRect;
        // Normalized texture coordinates
        AABB<float> m_uvCoords;

        // Texture data
        const Texture2D& m_texture;
        // Sprite color
        Color m_color;
}; 

Sprite update and render functions


void Sprite::update(float dt)
{
    m_prevState = m_currState;
    /// TODO: if animated, handle animation
}

void Sprite::render(QuadRenderer& quadRenderer, float alpha)
{
    SpriteState state;

/// just to test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
if (m_usingInterpolation)
{
    // Handle interpolation
    state.position = calcInterpolation(m_prevState.position, m_currState.position, alpha);
    state.angle    = calcInterpolation(m_prevState.angle, m_currState.angle, alpha);
}
else
{
    // If not using interpolation, just take the current state
    state = m_currState;
}
/// just to test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

    // Update matrix
    m_transform.updateMatrix(state);

    // Create a rectangle with the sprite dimensions,
    // starting from the origin of the coordinate system
    AABB<float> aabb =
    {
        0.0f, 0.0f,
        static_cast<float>(m_spriteRect.width),
        static_cast<float>(m_spriteRect.height)
    };

    // Add this sprite to the renderer
    quadRenderer.transformQuad(aabb, m_uvCoords, m_transform.getMatrix(), m_texture, m_color);
}

Transformation code


// Update the internal matrix
void Transformations::updateMatrix(const SpriteState& state)
{
    // Baking a matrix
    // http://totologic.blogspot.com.ar/2015/02/2d-transformation-matrices-baking.html
    float angle  = math::degToRad(-state.angle);
    float cosine = static_cast<float>(std::cos(angle));
    float sine   = static_cast<float>(std::sin(angle));
    float sxc    = m_scale.x * cosine;
    float syc    = m_scale.y * cosine;
    float sxs    = m_scale.x * sine;
    float sys    = m_scale.y * sine;
    float tx     = -m_origin.x * sxc - m_origin.y * sys + state.position.x;
    float ty     =  m_origin.x * sxs - m_origin.y * syc + state.position.y;

    m_matrix =
    {
         sxc, sys, tx,
        -sxs, syc, ty,
         0.0f, 0.0f, 1.0f
    };
} 

And this is my game loop code


    const int updateFPS = 30;
    const float dt = 1.0f / updateFPS;
    float accumulator = 0.0f;

    Uint32 currentTime = SDL_GetTicks();

    // Main Loop
    bool close = false;

    while(!close)
    {
        // Process system events
        SDL_Event event;

        while(SDL_PollEvent(&event))
        {
            switch(event.type)
            {
                case SDL_QUIT:
                    close = true;
                    break;

                case SDL_KEYDOWN:
                    if (event.key.keysym.sym == SDLK_LEFT)
                    {
                        sprite1.moveH(-spriteVel * dt);
                        sprite2.moveH(-spriteVel * dt);
                    }

                    if (event.key.keysym.sym == SDLK_RIGHT)
                    {
                        sprite1.moveH(spriteVel * dt);
                        sprite2.moveH(spriteVel * dt);
                    }

                default:
                    break;
            }
        } // events


        // update
        // Get frame time in seconds
        const Uint32 newTime = SDL_GetTicks();
        float frameTime = static_cast<float>(newTime - currentTime) / 1000.0f;

        // Avoid "Spiral of death"
        // (That is what happens when your physics simulation
        // cannot keep up with the steps it’s asked to take)
        frameTime = std::min(frameTime, 0.25f);

        currentTime  = newTime;
        accumulator += frameTime;

        // Logic update
        while (accumulator >= dt)
        {
            sprite1.update(dt);
            sprite2.update(dt);
            accumulator -= dt;
        }

        // render
        float alpha = accumulator / dt;

        quadRenderer.begin();
        sprite1.render(quadRenderer, alpha);
        sprite2.render(quadRenderer, alpha);
        quadRenderer.end();
        swapBuffers();
   } 

Quad renderer is my sprite batcher. It works fine

I'm creating two sprites to test this code, I activated interpolation in the first one, to see the difference


    Sprite sprite1(playerAtlas, { 0, 0, 64, 64 });
    sprite1.setOrigin({ 32.0f, 63.0f });
    sprite1.setPosition({ 512.0f, 384.0f });
    sprite1.useInterpolation(true);

    Sprite sprite2(playerAtlas, { 0, 0, 64, 64 });
    sprite2.setOrigin({ 32.0f, 63.0f });
    sprite2.setPosition({ 512.0f, 318.0f });
    sprite2.useInterpolation(false); 

Now, the questions I have:

1) Is it ok to get the time as unsigned integers, or should I use something like std::chrono? SDL2 only returns time as Uint32

2) Why the author of the article is using this hardcoded number? if ( frameTime > 0.25 )

3) Is my system events handling code in the right place? or should it be inside the while (accumulator >= dt) code?

4) Why I dont see any difference at all between the two sprites? the one using the interpolation method and the other one

Thanks for reading... any tips for improving my code will be welcome

Advertisement

1. Should be fine. Why wouldn't it be?

2. This is used to cap the frameTime to at most 250ms to prevent the game loop from taking longer and longer to run as it tried to catch up since the previous frame. Why 250ms? Probably no specific reason. It 'felt right'. With it, the game will update the simulation in 250ms chunks for every frame regardless of how long it takes in real-time. If it takes 1 second of real time to run 250ms worth of simulation and draw one frame then you'll get 1 frame per second and 250 worth of simulation. The experience would be terrible but if it was temporary the player could get past it. It would be a brief stutter in gameplay. If it was more common then the player would have to get a better computer (or you have to write better code). There's no real reason you couldn't change that time to 0.125 or something else.

3. Depends. If your simulation runs every 1/30 second do you want to handle events that occurred between steps? I think most people would say "yes", but its up to you.

4. You probably won't see a difference with just two sprites and without complicated simulation. The main benefits come when you have complicated simulation logic (i.e. physics that uses integration for example), if you want to easily record and play back the simulation, or with multi-player. Think about how two people playing across the planet have to keep their simulations in sync and how that is difficult if both players are running physics at different and inconsistent rates every frame. If you assume both players are on the same rate you don't have to work as hard to keep them sync'd.

C++: A Dialog | C++0x Features: Part1 (lambdas, auto, static_assert) , Part 2 (rvalue references) , Part 3 (decltype) | Write Games | Fix Your Timestep!

This is used to cap the frameTime to at most 250ms to prevent the game loop from taking longer and longer to run as it tried to catch up since the previous frame

I see... now I get it :)

You probably won't see a difference with just two sprites and without complicated simulation. The main benefits come when you have complicated simulation logic (i.e. physics that uses integration for example), if you want to easily record and play back the simulation, or with multi-player

Ok, I may add multiplayer for my game in the future, so I will keep using the interpolation method

Thanks!

1) Is it ok to get the time as unsigned integers, or should I use something like std::chrono? SDL2 only returns time as Uint32

any sufficiently accurate timer is acceptable - say accurate to the millisecond. QueryPerformanceCounter is the underlying API typically used.

2) Why the author of the article is using this hardcoded number? if ( frameTime > 0.25 )

as mentioned, its a frame time cap to avoid the spiral of death. the desired behavior of fix-your-timestep is that it renders one or more times per update. interpolation is used to smooth out the animation. when accumulator >= dt*2, fix your timestep drops frames - IE it does not render a frame between every two consecutive updates. so render stops momentariily while updates continue in the background. this can render the game unplayable. the cap limits how severe this effect is. ideallay, you want to measure ET (frametime) from the beginning of one update to the next, not just render time. and you want to cap the accumulator, not the frametime. and the cap should be accum < dt*2 so you never drop frames.

3) Is my system events handling code in the right place? or should it be inside the while (accumulator >= dt) code?

in terms of fair play, the player should get a turn, then the AI should get a turn. the way it is now, the player gets a turn every render while the AI gets one every 30th of a second. so player speed varies with frame rate, while update does not. its unlikely you want that behavior. its more likely you want something like: while (accum>=dt) { process_input(); update_all(); accum-=dt; }. then input and update run at 30 fps max, and render runs as fast as possible.

https://www.gamedev.net/blog/1729/entry-2262574-thoughts-on-fix-your-timestep-and-the-spiral-of-death/

https://www.gamedev.net/blog/1729/entry-2262573-increasing-et-accuracy-in-fix-your-timestep/

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

its unlikely you want that behavior. its more likely you want something like: while (accum>=dt) { process_input(); update_all(); accum-=dt; }. then input and update run at 30 fps max, and render runs as fast as possible.

I should check user input in the inner loop.. ok... but how about system events? like close window, lost focus, etc?

Another questions:

1) if I add a particle system, should I use the rendering interpolation technique on every particle, every frame? Let's say I treat each particle as a sprite (I didn't designed my particle system yet)

2) Is 30 updates per sec good enough for a little 2D tilemap based game? Too much? (I'm not using any physics engine like Box2D)

3) If I let the player enable/disable Vsync, should I change anything about the game updates? or it only affects rendering?

"I should check user input in the inner loop.. ok... but how about system events? like close window, lost focus, etc?" - The inner loop lasts less than a frame, so no.

The only reason that inner loop exists is to decouple a fixed-rate physics timestep from a variable-rate rendering timestep. Nothing else matters to it or cares about it.

You can make particles interpolated if you like, although it seems like a waste of time and resources given that no gameplay is affected if particle physics is a little off.

30 physics updates per second seems reasonable but it's not clear whether you really need physics updates at all, or even a fixed timestep. It's not a problem you need answering here however, because you could change it to 10, 100, or 1000 in 2 seconds, and see for yourself.

Vsync being enabled or disabled will potentially affect the rendering rate. The whole point of the fixed physics timestep is to make your physics totally independent from that. If you already handle variable rendering rates then you're all set.

You can make particles interpolated if you like, although it seems like a waste of time and resources given that no gameplay is affected if particle physics is a little off.

My character has a jetpack, so I want to throw some fire particles when the character is flying (the particles must follow the sprite movement), that's why I asked... but yes, it sounds like a waste of time, I'll see

it's not clear whether you really need physics updates at all, or even a fixed timestep

may I need it for the particles integration?... about fixed timestep, I thought it was mandatory for every game :/ ... for some reason, I don't like the Sleep() call you see on most game programming tutorials

The whole point of the fixed physics timestep is to make your physics totally independent from that. If you already handle variable rendering rates then you're all set

Ok, thanks!

The benefit of a fixed timestep are just to avoid small errors in physics that can accumulate when frame rate changes over time or between different observers. This is a big deal in FPS games where frame rate is often very variable depending on the angle of view, and where 2 different players can have differently-powered computers. For a 2D game, your frame rate is likely to be much less variable, and any benefit from using a fixed physics timestep will be almost entirely cosmetic. The chance of you seeing any significant difference between particles being updated with a variable timestep and other objects updated with a fixed timestep is low, the chance of a player seeing it is lower, and the effect on gameplay will be zero.

Fixed physics timesteps are not mandatory and in fact the majority of games that have ever shipped did not use one. But the more important physics is to you, the more important it is that your physics is done this way.

None of this relates to whether you use sleep() or not, incidentally. You can use sleep() with any game loop. All it means is that a frame will take longer than it otherwise would, and that is fine if your frame rate is high enough and you'd rather save some CPU time or battery life on a mobile device. And if you're careful with how you implement it, you can run everything at a fixed rate - graphics and updates, physics or otherwise - and you don't have to worry about everything else. A lot of console games still do something like that. Vsync has the same effect since it basically sleeps for however long you need to synchronise with the display frame rate.

I suggest this article, as a gentler introduction to why we use fixed timesteps for physics: http://gameprogrammingpatterns.com/game-loop.html

So, a very basic game loop for a 2D game, like this one, is all I need?

http://lazyfoo.net/tutorials/SDL/44_frame_independent_movement/index.php

no sprite interpolation at all? unless I use a physics engine like Box2D? (and add a lot of rigid bodies to the world), or make an online game, right?

I guess the only "physics" I'll use is sprite movement: pos += vel * dt

Lazy Foo's loop is a pretty standard variable time-step loop, arguably used by most commercial games in the late 90s, before people started realising that the physics sometimes broke.

Dedicated physics engines will handle the fixed timesteps for you, so actually they typically work well with a variable timestep, because they subdivide it themselves. The Unreal Engine does this, for example. But when you implement physics yourself, the fixed timestep helps.

If all you do is apply velocity, then doing fixed timesteps and sprite interpolation is probably unnecessary, unless you're seeing jerky movement. If you include acceleration, jumps, or gravity, it becomes more beneficial.

Still, given how easy it is to implement - just keep 2 positions and interpolate - there's little reason not to do it if you already have that data and a game loop ready to go.

This topic is closed to new replies.

Advertisement