Warning: Undefined array key "HTTP_ACCEPT_LANGUAGE" in /var/www/vhosts/bilgigunlugum.net/httpdocs/index.php on line 43
SDL Game Programming

SDL3 Oyun Programlama sayfalarımız yayında...

Ana sayfa > Oyun programlama > SDL3 programming > Motion with Vsync

Motion with Vsync

Before examining the program for moving objects, let's try to review the speed of objects moving under normal conditions, speed control methods, and the advantages and disadvantages of these methods.

Under normal conditions, when you want to move objects, they move very fast. The main reason for this is that the basic game loop works by repeating, regardless of the screen's refresh rate. In this case, the game loop runs as fast as possible, which causes objects to move too fast.

We can use 3 different methods to prevent objects from moving too fast:

  1. Vertical Sync (VSync)
  2. Fixed Frame Rate (FPS)
  3. Multiply the movement size by the loop's repeat time (delta time)

1. Vertical Sync (VSync)

Vertical Sync (VSync) synchronizes the frame rate of the graphics card to the refresh rate of the screen (usually 60Hz). This prevents screen tearing and provides a more consistent visual experience.

When a program starts running in SDL, vertical sync is not enabled by default. When vertical sync is not enabled, the game loop runs regardless of the screen refresh rate. In this case, frames are generated as fast as possible according to your computer's processing power. This causes the game loop to run very fast and objects to move a lot in each loop.

The following command enables vertical synchronization, making objects move slower.

SDL_SetRenderVSync(SDL_Renderer *renderer, 1);

2. Fixed Frame Rate (FPS)

We can use fixed frame rate (FPS) to control the speed of objects in the game. This ensures that each game loop is completed in a certain amount of time.

According to this system, the start time is taken at the beginning of each loop and the elapsed time is calculated at the end of the loop. If the elapsed time is shorter than the specified frame time (time calculated according to FPS), the loop is delayed. In this way, the game loop runs at a constant speed and the speeds of the objects are kept under control.

Calculate the movement size by multiplying the program loop's repeat time (delta time)

Calculate the movement size by multiplying the program loop's repeat time (delta time) is a very common and convenient method in game development. This approach allows game movements and animations to be more consistent and independent. Calculating movement with this method is generally considered the most flexible and accurate method.

To calculate movement with this method, the time elapsed in each game loop is calculated and multiplied by the movement size. This method ensures that game movements and animations are consistent across devices and performance conditions.

Comparison of methods

Object motion limitation methods
Method Advantages Disadvantages
Vertical Synchronization (VSync)
  • Prevents screen tearing, showing images smoother and more seamless.
  • Shows images smoother and more consistent.
  • Input lag may increase. This can be a disadvantage in fast-paced games.
  • It can limit the rendering time of the graphics card, causing performance loss.
Fixed Frame Rate (FPS)
  • Provides a consistent experience by running at a fixed speed determined by the developer.
  • Performance is independent; runs at the same speed even on slower machines.
  • Potentially underperforms high-performance machines.
  • May not run at a consistent speed on all systems, leading to inconsistent experiences across devices.
Motion calculation with time lag
  • Motion and animations are dynamically adjusted based on how long each frame is rendered, resulting in a smoother, more consistent gameplay experience.
  • Performance is independent; provides the same gaming experience on every device.
  • The calculations must be done correctly, otherwise incorrect movements or animations may occur.

Result

Calculating the movement size with time difference is considered the most flexible and accurate method. Game movements and animations run at a consistent speed regardless of system performance.

In this section, we will examine a program that moves a ball with the arrow keys in the main window with a texture created from a .png image file and controls the speed of the ball movement using the Vertical Synchronization (VSync) method.

When the program runs, in addition to the basic operations, the following operations occur:

  1. At the beginning of the program, the following global variables are first defined:
    • unsigned int time_start: Program start time
    • int frame_no: Number of times the program while loop repeats
    • int vsync: Checks whether the Vsync feature is active.
    • int move_size: Movement size
    • A variable named ball is defined from the following structure data type:
      struct objects {
        float x;
        float y;
        float width;
        float height;
        float vel_x;
        float vel_y;
      } ball;
      
  2. The init_vars() function assigns initial values ​​to global variables.
  3. In the load_img() function, an image file for the ball is loaded into the texture variable with the IMG_LoadTexture() function.
  4. In the main() function, 0 is assigned to the frame_no variable and the program start time is assigned to the time_start variable with the SDL_GetTicks() function.
  5. In the process_event() function,
    • When the arrow keys are pressed, the move size variable value named move_size in the direction indicated by the arrow is added to the ball.vel_x or ball.vel_y variable, which has a value of 0. Thus, movement is achieved.
    • When the arrow keys are released, the move size variable value named move_size in the direction indicated by the arrow is subtracted from the ball.vel_x or ball.vel_y variable and the values ​​of these variables are set to 0. Thus, the movement ends.
    • When the V key is pressed, if the vertical synchronization is active, it is made passive, if it is passive, it is made active. The frame_no variable value is 0 and the time_start value is the active time value.
  6. In update_screen() function,
    • The ball.vel_x variable value is assigned to the move_x variable
    • The ball.x value is incremented by the move_x value.
    • If ball.x is less than 0 or the sum of ball.x and ball.width is greater than the width of the window, ball.x takes its previous value.
    • The ball.vel_y variable value is assigned to the move_y variable
    • The ball.y value is incremented by the move_y value.
    • If ball.y is less than 0 or the sum of ball.y and ball.height is greater than the height of the window, ball.y takes its previous value.
  7. Inside the draw_screen() function,
    • The ball is displayed in its position in the main window with the SDL_RenderTexture() function.
    • The time elapsed since the program started (in milliseconds) in) is assigned to the time_elapsed variable. This value is reset when the V key is pressed.
    • The time_elapsed variable is used to obtain the minute, second and millisecond values ​​for the time counter.
    • The number of times the while loop repeats is divided by the seconds of the elapsed time to find the number of times the loop repeats in a second (average number of frames) and this value is assigned to the float avg_fps variable.
    • The time counter values, frame count, FPS value, the active status of the Vsync method and the x and y coordinate information of the ball are written to the main window title.
    • The frame_no value is incremented by 1.

SDL_SetRenderVSync() function

The SDL_SetRenderVSync() function sets the vertical synchronization (VSync) of the render target in SDL3. Vertical synchronization is used to prevent screen tearing by matching the refresh rate of the display to the refresh rate of the graphics card.


bool SDL_SetRenderVSync(SDL_Renderer *renderer, int vsync);

renderer: The SDL_Renderer object to set.

vsync: 1 to enable vertical synchronization, 0 to disable.

Function: This function enables or disables vertical synchronization for the SDL_Renderer you specify. VSync is used to reduce or eliminate screen tearing, especially in fast-moving graphics. Ensures that the SDL_RenderPresent() function call is synchronized with the screen refresh cycle, thus preventing screen tearing.

main.c file content will be as follows:


#include <stdio.h>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>

// Window width and height
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480

// Global variables
int is_running = false;        // Variable that controls the execution of the main loop of the program
SDL_Window *window = NULL;     // Main window variable
SDL_Texture *texture = NULL;   // Texture variable
SDL_Renderer *renderer = NULL; // Renderer variable

unsigned int time_start;       // Program start time
int frame_no;                  // while loop iteration number (frame)
int vsync;                     // Controls whether Vsync is enabled
int move_size;                 // Movement size

// Struct declaration
struct objects {
    float x;
    float y;
    float width;
    float height;
    float vel_x;
    float vel_y;
} ball;

// Function prototypes
int init_window(void);         // Create window and renderer
void init_vars(void);          // Initialize variables
void load_img(void);           // Load image to texture
void process_event(void);      // Process events
void update_screen();          // Update values
void draw_screen(void);        // Draw screen
void destroy_window(void);     // Destroy window

int main(int argc, char* argv[])
{
    // Create window and renderer
    is_running = init_window();

    // Initialize variables
    init_vars();

    // Load .png file to texture
    load_img();

    frame_no = 0;
    time_start = SDL_GetTicks(); // Get program start time

    // Main loop
    while (is_running) {
       process_event();          // Processing SDL events (Here keyboard inputs)
       update_screen();          // Updating variables
       draw_screen();            // Drawing objects on the window (Rendering)
    }

    // Destroy renderer and SDL window
    destroy_window();

    return 0;
}

// Create window and renderer
int init_window(void)
{
    // Initialize the SDL library.
    if(SDL_Init(SDL_INIT_VIDEO) == false) {
       SDL_Log("SDL init error: %s\n", SDL_GetError());
       return false;
    }

    // Create a window and a 2D rendering context for the window.
    if(!SDL_CreateWindowAndRenderer("SDL3 window", WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer)) {
       return false;
    }

    return true;
}

// Initialization function that runs only once at the beginning of the program
void init_vars(void)
{
    time_start = 0;
    frame_no = 0;
    vsync = 0;
    move_size = 10;

    ball.x = 0;
    ball.y = 0;
    ball.width = 20;
    ball.height = 20;
    ball.vel_x = 0;
    ball.vel_y = 0;
}

// Load image to texture
void load_img(void)
{
    // Loading image to texture
    texture = IMG_LoadTexture(renderer, "ball.png");

	if(texture == NULL) {
       SDL_Log("IMG_LoadTexture error: %s\n", SDL_GetError());
	}
}

// Function to control SDL events and process keyboard inputs
void process_event(void)
{
    SDL_Event event;

    // Creating a loop to process user inputs
    while (SDL_PollEvent(&event)) {
       switch (event.type) {
          case SDL_EVENT_QUIT: // Logout action by the user (x button at the top right of the window)
               is_running = false; // Terminates the execution of the program main loop.
               break;

          case SDL_EVENT_KEY_DOWN: // Key pressed.
               if(event.key.repeat == 0) {
                  switch(event.key.key) {
                     case SDLK_UP: ball.vel_y -= move_size; break;
                     case SDLK_DOWN: ball.vel_y += move_size; break;
                     case SDLK_LEFT: ball.vel_x -= move_size; break;
                     case SDLK_RIGHT: ball.vel_x += move_size; break;
                     case SDLK_V: // Enable vertical synchronization (VSync)
                          vsync = vsync==SDL_RENDERER_VSYNC_DISABLED ? 1 : SDL_RENDERER_VSYNC_DISABLED;
                          SDL_SetRenderVSync(renderer, vsync);
                          frame_no = 0;
                          time_start = SDL_GetTicks();
                          break;
                     case SDLK_ESCAPE: is_running = false; break;
                  }
               }
              break;

          case SDL_EVENT_KEY_UP: // Key released.
              if(event.key.repeat == 0) {
                 switch(event.key.key) {
                    case SDLK_UP: ball.vel_y += move_size; break;
                    case SDLK_DOWN: ball.vel_y -= move_size; break;
                    case SDLK_LEFT: ball.vel_x += move_size; break;
                    case SDLK_RIGHT: ball.vel_x -= move_size; break;
                 }
              }
              break;
       }
    }
}

// Updates objects in the main window
void update_screen(void)
{
    // Calculate moving size
    // ball.vel_x assigned to move_x variable.
    float move_x = ball.vel_x;

    // Ball x coordinate increased by move_x
    ball.x += move_x;
    // If ball.x is lower than 0 or, sum of ball.x and ball.width is greater than windows width
    // ball.x gets its previous value.
    if((ball.x<0) || (ball.x + ball.width > WINDOW_WIDTH)) {
	   ball.x -= move_x;
    }

    // ball.vel_y assigned to move_y variable.
    float move_y = ball.vel_y;

    // Ball y coordinate increased by move_y
    ball.y += move_y;
    // If ball.yx is lower than 0 or, sum of ball.y and ball.height is greater than windows height
    // ball.y gets its previous value.
    if((ball.y<0) || (ball.y + ball.height > WINDOW_HEIGHT)) {
       ball.y -= move_y;
    }
}

// Render function used to draw game objects in the main window
void draw_screen(void)
{
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); // Set the color used for drawing operations.
    SDL_RenderClear(renderer); // Clear the current rendering target with the drawing color.

    SDL_FRect rect_dst = { ball.x, ball.y, 20, 20 };
    // Load texture content to main window
	SDL_RenderTexture(renderer, texture, NULL, &rect_dst);

    // Loading all objects drawn one by one to the back buffer to the front buffer at once and loading them onto the screen
    // This process prevents each drawn object from being displayed on the screen one by one.
    SDL_RenderPresent(renderer); // Update on screen all operations performed since the previous call

    // The time elapsed since the start of the program in milliseconds
    // It is reset when V key is pressed.
    unsigned int time_elapsed = SDL_GetTicks()-time_start;

    int milliseconds = time_elapsed;   // The elapsed time in milliseconds
    int seconds = milliseconds / 1000; // The elapsed time in seconds
    milliseconds %= 1000;              // The millisecond part of the counter
    int minutes = seconds / 60;        // The elapsed time in minutes
    seconds %= 60;                     // The second part of the counter
    minutes %= 60;                     // The minute part of the counter

    // Calculating FPS
    // The number of times the while loop repeats is divided by the seconds value of the elapsed time,
    // The number of times the loop repeats in a second (average number of frames) is found.
    float avg_fps = frame_no / (time_elapsed / 1000.f);

    char cdizi[200];
    sprintf(cdizi, "%.2d:%.2d:%.4d - Frame: %d - FPS: %.2f - Vsync: %s - x: %.2f y: %.2f",
                    minutes, seconds, milliseconds, frame_no, avg_fps, vsync==1 ? "On" : "Off",
                    ball.x, ball.y);
    SDL_SetWindowTitle(window, cdizi);

    frame_no++;
}

// Destroy Renderer and SDL window, exit from SDL3
void destroy_window(void)
{
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}