1
\$\begingroup\$

I am experiencing stutter when I am moving faster than 0.1 units in my program.

jittery motion on 0.3 units

When doing exactly 0.1 units I get:

smooth motion on 0.1 For test purposes I've made the bot always heading southeast.

int main()
{

// Intialize SDL2

SDL_Init(SDL_INIT_EVERYTHING);

  // Defining world & window dimensions and camera position
  const int SCREEN_WIDTH{ 800 };
  const int SCREEN_HEIGHT{ 480 };
  const int WORLD_WIDTH{ 1000 };
  const int WORLD_HEIGHT{ 1000 };
  int view_x{ 0 };
  int view_y{ 0 };

  // Create window and default rendering context
  SdlCreateWindowAndRendererWrapped wr{ SCREEN_WIDTH, SCREEN_HEIGHT };
  SDL_Renderer * const ren{ &wr.get_resource_renderer() };

  Object ground(ren, "assets/ground.png", 0, 0, 95);
  ground.set_size(600);
  ground.set_pos(200, 200);

  Object bot(ren, "assets/bot.png", 40, 46, 32);
  bot.set_size(200);

  // Game loop
  bool is_running{ true };
  SDL_Event event{};
  SDL_SetRenderDrawColor(ren, 0, 0, 0, 0);
  while (is_running) {
    Uint64 start = SDL_GetPerformanceCounter();
    /*--------------Event loop--------------*/
    while (SDL_PollEvent(&event))
    {
      if (event.type == SDL_QUIT)
      {
        is_running = false;
      }
    } // end of the event loop

    /*--------------Physics loop--------------*/
    static Clock clock;
    clock.tick();
    Vec2f v{ 0.3f, 0.3f };
    bot.move(v, clock.delta);

    // Screen coordinate translations
    bot.set_pos(bot.get_pos().x - view_x, bot.get_pos().y - view_y);
    ground.set_pos(ground.get_pos().x - view_x, ground.get_pos().y - view_y);
    ground.update();
    bot.update();

    // Check camera bounds
    if (view_x < 0)
    {
      view_x = 0;
    }
    if (view_y < 0)
    {
      view_y = 0;
    }
    if (view_x > WORLD_WIDTH - SCREEN_WIDTH)
    {
      view_x = WORLD_WIDTH - SCREEN_WIDTH;
    }
    if (view_y > WORLD_HEIGHT - SCREEN_HEIGHT)
    {
      view_y = WORLD_HEIGHT - SCREEN_HEIGHT;
    }

    // Make the camera follow the bot
    view_x = bot.get_pos().x - SCREEN_WIDTH / 2;
    view_y = bot.get_pos().y - SCREEN_HEIGHT / 2;

    /*--------------Rendering loop--------------*/
    SDL_RenderClear(ren);
    ground.draw(ren);
    bot.draw(ren);
    SDL_RenderPresent(ren);

    /*--------------Todo: Animation loop--------------*/

    // Cap to 60 FPS (approx. 16.666 ms per frame -- the cycle time)
    Uint64 end = SDL_GetPerformanceCounter();
    float elapsed_ms{ (end - start) / static_cast<float>(SDL_GetPerformanceFrequency()) * 1000.0f };
    if (std::isless(elapsed_ms, 16.666f))
    {
      SDL_Delay(static_cast<int>(floorf(16.666f - elapsed_ms)));
    }
  }

  // Clean up used resources
  SDL_Quit();
  return 0;
}

I took inspiration for designing my game loop from

https://thenumbat.github.io/cpp-course/sdl2/08/08.html

The Clock class/struct was implemented exactly as Salajouni's one:

How to calculate delta time with SDL?

The camera was implemented via this method:

https://wiki.allegro.cc/index.php?title=How_to_implement_a_camera

This is how my Object struct/class looks like:

class Object
{
public:
    explicit Object(SDL_Renderer * t_renderer, const std::string & t_s, const int t_x, const int t_y, const int t_sz)
    {
        sprite = new Sprite;
        sprite->set_texture(t_renderer, t_s);
        sprite->set_src_rect(t_x, t_y, t_sz, t_sz);
        sprite->set_dest_rect(0, 0, t_sz, t_sz);
        size = t_sz;
    }
    ~Object()
    {
        delete sprite;
        sprite = nullptr;
    }
    int get_size() const
    {
        return size;
    }
    void set_size(const int t_sz)
    {
        size = t_sz;
    }
    const Vec2i & get_pos() const
    {
        return pos;
    }
    void set_pos(const int t_x, const int t_y)
    {
        pos.x = t_x;
        pos.y = t_y;
    }
    void move(const Vec2f & t_v, const Uint32 t_delta)
    {
        pos.x += static_cast<int>(t_v.x * t_delta);
        pos.y += static_cast<int>(t_v.y * t_delta);
    }
    // Todo:
    void animate()
    {
    }
    void update()
    {
        sprite->set_dest_rect(pos.x, pos.y, size, size);
    }
    void draw(SDL_Renderer * ren)
    {
        SDL_RenderCopy(ren, &sprite->get_texture(), &sprite->get_src_rect(), &sprite->get_dest_rect());
    }
private:
    Sprite * sprite{};
    Vec2i pos{};
    int size{};
};

The part that supposedly needs the most attention is the physics loop. This is the part where all the motion and motion updates happen. In there I define a velocity vector and set both of its components too 0.3. After that the stutter/jitter happens. However, when I do 0.1, then it runs smoothly as shown in the pictures above. I created the window via SDL_CreateWindowAndRenderer(). So accelerated rendering should be active. I am not sure whether or not VSYNC gets activated as well when doing SDL_CreateWindowAndRenderer().

So what could possibly be the cause? Is it due to cascading rounding errors? Is it due to the active VSYNC and the manual framerate cap at the end of the loop? What is it exactly that is causing the stutter?

PS: And for the possibility that my Vector2 template class needs attention as well, there you go:

/* 2D math classes */
namespace oki2d::math2d
{
    // Vector2 class definition
    template <typename T>
    struct Vector2
    {
    public:
        T x{};
        T y{};
        explicit Vector2() : x{}, y{}
        { }
        explicit Vector2(T t_value) : x{ t_value }, y{ t_value }
        { }
        explicit Vector2(const T t_x, const T t_y) : x{ t_x }, y{ t_y }
        { }
        explicit Vector2(const Vector2 & t_v) : x{ t_v.x }, y{ t_v.y }
        { }
        Vector2 & operator=(const Vector2 & t_rhs)
        {
            if (&t_rhs == this)
            {
                return *this;
            }
            x{ t_rhs.x };
            y{ t_rhs.y };
            return *this;
        }
        Vector2 operator-() const
        {
            return Vector2{ -x, -y };
        }
        bool operator==(const Vector2 & t_rhs) const
        {
            // Perform single-precision floating-point comparison (float)
            if (std::is_floating_point<T>::value)
            {
                return (std::fabsf(static_cast<float>((*this).x - t_rhs.x)) < std::numeric_limits<float>::epsilon())
                    && (std::fabsf(static_cast<float>((*this).y - t_rhs.y)) < std::numeric_limits<float>::epsilon());
            }

            assert(std::is_floating_point<T>::value == false);

            // Perform integer comparison otherwise
            return x == t_rhs.x && y == t_rhs.y;
        }
        bool operator!=(const Vector2 & t_rhs) const
        {
            return !((*this) == t_rhs);
        }
        const Vector2 & operator+=(const Vector2 & t_rhs)
        {
            if (&t_rhs == this)
            {
                return *this;
            }
            x += t_rhs.x;
            y += t_rhs.y;
            return *this;
        }
        Vector2 & operator-=(const Vector2 & t_rhs) const
        {
            if (&t_rhs == this)
            {
                return *this;
            }
            x -= t_rhs.x;
            y -= t_rhs.y;
            return *this;
        }
        Vector2 operator+(const Vector2 & t_rhs) const
        {
            return Vector2{ x + t_rhs.x, y + t_rhs.y };
        }
        Vector2 operator-(const Vector2 & t_rhs) const
        {
            return Vector2{ x - t_rhs.x, y - t_rhs.y };
        }
        Vector2 operator*(const T t_rhs) const
        {
            return Vector2{ x * t_rhs, y * t_rhs };
        }
        Vector2 operator/(const T t_rhs) const
        {
            return Vector2{ x / t_rhs, y / t_rhs };
        }
        static T double_length(const Vector2 & t_v)
        {
            return t_v.x * t_v.x + t_v.y * t_v.y;
        }
        static T length(const Vector2 & t_v)
        {
            return std::sqrt(t_v.x * t_v.x + t_v.y * t_v.y);
        }
        static Vector2 normalize(const Vector2 & t_v)
        {
            const T len{ length(t_v) };
            return Vector2{ t_v.x / len, t_v.y / len };
        }
        static T dot_product(const Vector2 & t_lhs, const Vector2 & t_rhs)
        {
            return t_lhs.x * t_rhs.x + t_lhs.y * t_rhs.y;
        }
        friend std::ostream & operator<<(std::ostream & t_os, const Vector2 & t_v)
        {
            t_os << "(" << t_v.x << ", " << t_v.y << ")";
            return t_os;
        }
    }; // Vector2

    // Using declarations
    using Vec2i = Vector2<int>;
    using Vec2f = Vector2<float>;
} // oki2d::math2d

It is just a simple templated 2D vector math class. Nothing scary.

\$\endgroup\$
  • 2
    \$\begingroup\$ It can be caused by the fact that you mix ints for your positions with floats for the movement (i.e. it can be caused by weird rounding issues). \$\endgroup\$ – Vaillancourt Sep 6 '18 at 19:20
  • \$\begingroup\$ Alexandre Vaillancourt. Well, you are probably right. It works even without LERP doing the framerate capping via accumulation. \$\endgroup\$ – user9266 Sep 7 '18 at 13:06
  • \$\begingroup\$ Thanks for adding an update, and I'm glad you fix the issue! On stack exchange sites, we add answers when we found the solution, and its perfectly fine to answer your own question :) \$\endgroup\$ – Vaillancourt Sep 7 '18 at 13:16
  • \$\begingroup\$ Alexandre Vaillancourt: I answered my question. :) Should I remove the edit in my first posting? \$\endgroup\$ – user9266 Sep 7 '18 at 15:42
  • \$\begingroup\$ Yes, please, there is no point in duplicating content :) You'll be able to mark the answer as "accepted" in one or two days, that's help future user know that this has the fix. \$\endgroup\$ – Vaillancourt Sep 7 '18 at 15:44
1
\$\begingroup\$

As Alexandre Vaillancourt in the comments suggested:

  • it is caused by the conversion from float to int (rounding errors).
  • It might also be caused by the fact that I used SDL_Delay() earlier to cap the framerate.
  • It is apparently unreliable since it doesn't wait the specified amount of time in an accurate fashion.

So the two issues that needed attention were:

  1. The conversions from float to int
  2. And possibly: Using SDL_Delay() to achieve the framerate cap**

Many tutorials on the web seem to achieve the framerate cap via a combination of SDL_Delay() and SDL_GetTicks().

I will do it differently, since this is what helped me to get a steady and fluid motion. So here it goes:

  • What to do you do instead? Well, you want 1 frame to be 16 ms long (that is, if you are aiming to realize the frequency of 60 Hz -- 60 FPS).
  • Thus, you need to wait until the 16 ms is up and only then execute your routines be it rendering, physics or animation.
  • IOW: execute the routines iff the 16 ms have passed.

I used an if block to make the execution depending on the minimum cycle time of 16 ms.

For the time measurement I made a small utility class/struct inspired by Salajouni:

struct Timer
{
    Uint64 previous_ticks{};
    float elapsed_seconds{};

    void tick()
    {
        const Uint64 current_ticks{ SDL_GetPerformanceCounter() };
        const Uint64 delta{ current_ticks - previous_ticks };
        previous_ticks = current_ticks;
        static const Uint64 TICKS_PER_SECOND{ SDL_GetPerformanceFrequency() };
        elapsed_seconds = delta / static_cast<float>(TICKS_PER_SECOND);
    }
};

You need to collect/accumulate the seconds in each iteration. The steps that you need to take then are: accumulating the seconds of each iteration (hence the variable name accumulator), then make a float comparison either with an epsilon comparison (float_val1 - float_val2 < epsilon, epsilon = 0.00001f) or just use std::isgreater() or std::isless() for that.

Then, accumulating the seconds until you reach the cycle time you need e.g. 16 ms. (And again: do not forget to reset the accumulator inside the if block. It needs to recount to the 16 ms every time. I reset it with -CYCLE_TIME)

int main()
{
    /*--------------Game loop--------------*/
    // Timing constants
    const int UPDATE_FREQUENCY{ 60 };
    const float CYCLE_TIME{ 1.0f / UPDATE_FREQUENCY };
    // System timing
    static Timer system_timer;
    float accumulated_seconds{ 0.0f };
    while (is_running)
    {
        // Update clock
        system_timer.tick();
        accumulated_seconds += system_timer.elapsed_seconds;

        /*--------------Event loop--------------*/
        /* ... */

        // Cap the framerate
        if (std::isgreater(accumulated_seconds, CYCLE_TIME))
        {
            // Reset the accumulator
            accumulated_seconds = -CYCLE_TIME;

            /*--------------Physics loop--------------*/
            static Timer physics_timer;
            physics_timer.tick();

            bot.position.x += bot.direction.x * bot.speed * physics_timer.elapsed_seconds;
            bot.position.y += bot.direction.y * bot.speed * physics_timer.elapsed_seconds;

            // Screen coordinate translations
            /* ... */

            ground.update();
            bot.update();

            // Camera
            /* ... */

            /*--------------Rendering loop--------------*/
            SDL_RenderClear(ren);
            ground.draw(ren);
            bot.draw(ren);
            SDL_RenderPresent(ren);

            /*--------------Todo: Animation loop--------------*/
            static Timer animation_timer;
            animation_timer.tick();

            /* ... */
        }
    }

    // Clean up used resources
    /* ... */
    SDL_Quit();
    return 0;
}

Note that I left out some code parts to make it easier to follow.

Hope it helps! :)

enter image description here

Tl;tr:

  • Accumulate floats instead of using SDL_Delay() and do not attempt to convert floats to integers carelessly.
  • Avoid conversions by constructing a timer class that is based on float.
  • Then do a float comparison via an epsilon comparison or via std::isgreater()/std::isless().

\$\endgroup\$
  • 1
    \$\begingroup\$ Shouldn't if (std::isgreater(accumulated_seconds, CYCLE_TIME)) be while (std::isgreater(accumulated_seconds, CYCLE_TIME)) ? \$\endgroup\$ – talex Sep 29 '19 at 7:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.