Невозможно создать плавную анимацию в SDL 2

#c #macos #animation #sdl-2 #game-loop

#c #macos #Анимация #sdl-2 #игровой цикл

Вопрос:

У меня проблема с анимацией в SDL 2. Я попробовал два возможных решения для ограничения частоты кадров: одно с использованием постоянного режима ожидания 16 мс (только для тестирования), а другое с использованием таймера для более точного ограничения кадров (функция игрового цикла runGameLoop_computed). В целях тестирования я просто рисую и перемещаю прямоугольник, используя функцию SDL_RenderFillRect, но оба метода игрового цикла создают дрожание при движении прямоугольника.

Есть ли у вас какие-либо идеи о том, что здесь не так, и анимация не гладкая?

Полный код таков:

 #include <iostream>
#include <SDL.h>
#include <chrono>
#include <thread>
#include <math.h>

using namespace std;

//sdl window and window renderer pointers
static SDL_Window *gWindow;
static SDL_Renderer *windowRenderer;

//terminates the app when users closes the window
static bool exitAppFlag = false;

static int window_width = 1000;
static double position_x = 0; // current x position to draw the rectangle
static double delta_time = 0; // the time passed since last draw


//initilizes SDL and displays the application window
void initSDL()
{
    if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
    {
        printf( "SDL could not initialize! SDL Error: %sn", SDL_GetError() );
    }
    else
    {
        printf( "SDL ok n");

        //Create window
        gWindow = SDL_CreateWindow( "Tank Multiplayer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, 500, SDL_WINDOW_SHOWN);

        if( gWindow == nullptr )
        {
            printf( "Window could not be created! SDL Error: %sn", SDL_GetError() );
        }
        else
        {
            //Create renderer for window
            windowRenderer = SDL_CreateRenderer( gWindow, -1, SDL_RENDERER_ACCELERATED);
            //windowRenderer = SDL_CreateRenderer( gWindow, -1, SDL_RENDERER_SOFTWARE);

            if(windowRenderer == nullptr )
            {
                printf( "Renderer could not be created! SDL Error: %sn", SDL_GetError() );
            }
            else
            {
                //Initialize renderer color
                SDL_SetRenderDrawColor(windowRenderer, 0, 0, 255, 255);
                //SDL_SetRenderDrawColor(windowRenderer, 0x00, 0x00, 0x00, 0x00 );
            }
        } //SDL_CreateWindow

    }
}

//part of game loop: processes events (currently handles only window close event)
void processEvents()
{
    SDL_Event p_event;
    while (SDL_PollEvent(amp;p_event) != 0){
        if (p_event.type == SDL_EventType::SDL_QUIT){
            //if the user closed the window, then set the flag to true, so that we can exit the application
            exitAppFlag = true;
            return;
        }
    }

}

//part of game loop: updates the position_x variable based on the time passed since the last time (delta_time)
void update()
{
    static double speed = 0.0532;
    position_x  = delta_time * speed;
    if (position_x > window_width) position_x = 0;
}

//part of game loop: draws the rectange to the screen
void draw()
{
    SDL_Rect r;
    r.h = 300;
    r.w = 100;
    r.x = static_cast<int>(round(position_x));
    r.y = 0;
    SDL_SetRenderDrawColor(windowRenderer, 0x00, 0x00, 0x00, 0x00 );
    SDL_RenderClear(windowRenderer);
    SDL_SetRenderDrawColor(windowRenderer, 0, 0, 255, 255);
    SDL_RenderFillRect(windowRenderer, amp;r);
    SDL_RenderPresent(windowRenderer);
}

//game loop: frame capping based on the given fps
void runGameLoop_computed()
{
    static double fps = 60;
    static double single_frame_time_micro = (1000 / fps) * 1000;
    std::chrono::time_point<std::chrono::high_resolution_clock>begin_time_point = std::chrono::high_resolution_clock::now();//stores the time point before processing game objects and drawing
    long long delta_time_micro = 0;

    while (!exitAppFlag) {
        delta_time_micro  = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now() - begin_time_point).count();
        if (delta_time_micro < single_frame_time_micro){
            std::this_thread::sleep_for(std::chrono::microseconds(static_cast<long long>(single_frame_time_micro - delta_time_micro)));
            delta_time_micro = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now() - begin_time_point).count();
        }
        delta_time = delta_time_micro / 1000.00;
        //std::cout <<  delta_time << std::endl;

        begin_time_point = std::chrono::high_resolution_clock::now();//stores the time point before processing game objects and drawing

        processEvents();
        update();
        draw();
    }
}

//game loop: constant sleep time 16 ms, almost 60fps
void runGameLoop_static()
{
    delta_time = 16.0;
    while (!exitAppFlag) {
        SDL_Delay(16);

        processEvents();
        update();
        draw();
    }
}

int main()
{
    //initilize SDL library and create window
    initSDL();

    //enter game loop
    //runGameLoop_static(); //constant sleep time 16ms
    runGameLoop_computed(); //frame capping based on given fps

    return 0;
}
  

Внутри основной функции вы можете раскомментировать метод witch для тестирования:
runGameLoop_static
или
runGameLoop_computed

Я загрузил полный проект Qt здесь (macOS): https://www.sendspace.com/file/1p4oqq

Комментарии:

1. Включить SDL_RENDERER_PRESENTVSYNC и использовать runGameLoop_computed() без sleep_for() ?

2. Также можно поэкспериментировать с std::floor() / std::ceil() вместо std::round() преобразования целых чисел в прямую позицию.

3. Кроме того, какой вид рендеринга вы возвращаете? Серверная часть OpenGL или Metal?

4. @genpfault похоже, что SDL_RENDER_PRESENTVSYNC значительно улучшает все. Я протестирую его, чтобы узнать, поддерживается ли он везде, потому что я где-то читал, что это не так.

5. @genpfault Я получаю 4 драйвера рендеринга: opengl, opengles2, metal, software

Ответ №1:

Я не уверен, решит ли это проблему дрожания (это может быть), но вы можете увеличить точность попадания в цель на 60 кадров в секунду, перейдя в режим ожидания до определенного момента времени, в отличие от режима ожидания в течение определенного времени. Это упрощает логику вашего цикла, избавляя вас от попыток вычислить, сколько времени требуется для обработки каждого кадра.

 void
runGameLoop_computed()
{
    using FrameDuration = std::chrono::duration<int, std::ratio<1, 60>>;
    auto next_start = std::chrono::steady_clock::now()   FrameDuration{1};

    while (!exitAppFlag)
    {
        processEvents();
        update();
        draw();

        std::this_thread::sleep_until(next_start);
        next_start  = FrameDuration{1};
    }
}
  

Комментарии:

1. @howad-hinnant спасибо за ваш ответ. Я попробовал ваше решение, добавив дельта-время, но, похоже, единственный способ получить плавную анимацию с SDL — это использовать SDL_RENDER_PRESENTVSYNC и даже таким образом не на 100% плавный. Все еще изучаю, как это сделать.