Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
2.0k views
in Technique[技术] by (71.8m points)

c - Shouldn't a VSYNC'ed cycle in a game take a constant amount of time?

I am writing a little snake game in C, using SDL, on Windows. On each frame I move each square a constant amount of pixels (e.g. 2 pixels).

As long as I have Vsync enabled, the movement is very smooth. However, I was trying to achieve the same smoothness without Vsync, just to learn how things work, and after a lot of struggle I have managed to time the cycles down to almost a perfect 0.016667 seconds (60Hz monitor here) by using the Performance Counter to measure repeated SDL_Delay(1) until 0.014 seconds of a cycle + an empty loop up to 0.016667s. This works great (so it's not a matter of SDL_Delay(1) taking more than it should) I guess, because 1 in 1000 frames is off by 0.1ms, and this should not be noticeable.

But the movement is still jerky sometimes - as if it doesn't move at all for one frame and then catches up double on the next. This I think boils down to the fact that the game is updating and rendering when it should, but the actual display on the monitor takes place a little sooner than it should, and then a little later than it should.

So I did some time measurements and the results are very surprising to me - see attached graphs. With Vsync enabled, the measured cycle times are all over the place - much much worse than the non-vsynced version - and I was expecting a perfect constant time. So the question is: Is the actual time between 2 consecutive drawings to the physical monitor not constant? Because if that is the case there is no way you can achieve what I am trying without some sort of feedback of when exactly the last drawing took place...

P.S. The update and rendering are very very fast - e.g. if the cycle is not limited it can go millions of times per second. So that is not slowing the cycle down.

enter image description here

#include <SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <windows.h>

typedef enum {false, true} bool;

#define NONE 0
#define QUIT 1
#define LEFT 2
#define RIGHT 3
#define UP 4
#define DOWN 5

typedef struct Color
{
    char R, G, B, A;
}Color;

typedef struct Square
{
    int x, y, side, dir;
    Color color;
}Square;


const int WINDOW_W = 600;
const int WINDOW_H = 800;
const int SQ_SIDE = 48;
const Color SQ_COLOR = {0, 0, 0x99, 0xFF};
const Color BG_COLOR = {0xFF, 0xFF, 0xFF, 0xFF};

SDL_Window* g_window = NULL;
SDL_Renderer* g_renderer = NULL;

float speed = 120; /* pixels per second */
int display_refresh_rate;

bool vsync_enabled = false; /* enable/disable vsync from here */

bool clear_screen(void);
void draw_rect(int x, int y, int width, int height, Color color);
void update_square_position(Square *square);
int process_input_events();


int main(int argc, char* args[])
{

    SDL_Init(SDL_INIT_VIDEO);
    g_window = SDL_CreateWindow("Snake", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_W, WINDOW_H, SDL_WINDOW_SHOWN);

    int renderer_flags = SDL_RENDERER_ACCELERATED;
    if(vsync_enabled) renderer_flags |= SDL_RENDERER_PRESENTVSYNC;

    g_renderer = SDL_CreateRenderer(g_window, -1, renderer_flags);
    SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);

    int display_index;
    SDL_DisplayMode display_mode;
    display_index = SDL_GetWindowDisplayIndex(g_window);
    SDL_GetDesktopDisplayMode(display_index, &display_mode);

    display_refresh_rate = display_mode.refresh_rate;
    float target_cycle_time = 1.0f / display_refresh_rate;

    clear_screen();

    unsigned long long tick_now = SDL_GetPerformanceCounter();
    unsigned long long last_render_tick = tick_now;

    float time_from_last_render = 0.0f;
    float render_times_array[3601];

    float perf_freq = (float)SDL_GetPerformanceFrequency();
    timeBeginPeriod(1);

    Square *square = (Square*) malloc(sizeof(Square));

    square->x = 50;
    square->y = 50;
    square->color = SQ_COLOR;
    square->side = SQ_SIDE;
    square->dir = RIGHT;

    int i = 0;

    while(1)
    {
        tick_now = SDL_GetPerformanceCounter();
        time_from_last_render = (tick_now - last_render_tick) / perf_freq;

        if(time_from_last_render < target_cycle_time - 0.002f)
            SDL_Delay(1);


        if(vsync_enabled || (time_from_last_render >= target_cycle_time))
        {
            if(process_input_events()== QUIT) break;

            update_square_position(square);

            clear_screen();
            draw_rect(square->x, square->y, SQ_SIDE, SQ_SIDE, square->color);
            SDL_RenderPresent(g_renderer);

            last_render_tick = tick_now;

            /* save 3600 timestamps and print them */
            render_times_array[i++] = time_from_last_render;
            if(i >= 3600)
            {
                clear_screen();
                int j;
                for(j = 0; j < i; j++)
                    printf("%f
", render_times_array[j]);

                i=0;
                break;
            }
        }
    }

    SDL_DestroyWindow(g_window);
    SDL_DestroyRenderer(g_renderer);
    SDL_Quit();

    return 0;
}

bool clear_screen(void)
{
    /* SDL functions return 0 on success! */
    if(SDL_SetRenderDrawColor(g_renderer, BG_COLOR.R, BG_COLOR.G, BG_COLOR.B, BG_COLOR.A))
        return false;
    else if(SDL_RenderClear(g_renderer))
        return false;

    return true;
}

void draw_rect(int x, int y, int width, int height, Color color)
{
    SDL_Rect rect = {x, y, width, height};
    SDL_SetRenderDrawColor(g_renderer, color.R, color.G, color.B, color.A);
    SDL_RenderFillRect(g_renderer, &rect);
}

void update_square_position(Square *square)
{
    int step = speed / display_refresh_rate;

    if(square->dir == RIGHT)
    {
        square->x += step;
        if(square->x > WINDOW_W - 100) square->dir = DOWN;
    }

    if(square->dir == LEFT)
    {
        square->x -= step;
        if(square->x < 50) square->dir = UP;
    }

    if(square->dir == DOWN)
    {
        square->y += step;
        if(square->y > WINDOW_H - 100) square->dir = LEFT;
    }

    if(square->dir == UP)
    {
        square->y -= step;
        if(square->y < 50) square->dir = RIGHT;
    }
}

int process_input_events()
{
    SDL_Event event_handler;

    while(SDL_PollEvent(&event_handler) != 0)
    {
        if(event_handler.type == SDL_QUIT)
            return QUIT;
    }
    return NONE;
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

For the question: Is the actual time between 2 consecutive drawings to the physical monitor not constant? the answer is NO because it will depend on what it needs to be render.

You are saying that you move your squares on each frame and thats an error because as you see, the frames aren't displayed in constant time. You need to code this behavior (and all your game logic) in a framerate independent manner, instead of moving N pixels each frame, you need to set a speed based in time, could it be 2pixels each 3ms and then each frame you should compute the elapsed time between frames (google that for SDL) and with that elapsed time you could move your objects. Eg (in pseudo code).

float speed = 10;
float timeFrameInMS = 2;

onNewFrame(){
    float elapsedTimeInMS = ...getting the elapsed time.. // lets say it is .5ms
    float movePercentage = elapsedTimeInMS/timeFrameInMS // .25
    float distanceToMove = speed*movePercentage.
    /*Move objects*/
}

You need to start thinking in time instead of frames, because the device running the game could at some moment have a "freeze" and the time between frames could be 1 second for example (it happens). It could also be the other way, there could be a device running at twice the framerate you are developing, if you base your logic on frames then this device will have everything at double the speed.

Here is a book on the topic and with other basic tecniques for game development: Game Programming Algorithms and Techniques: A Platform-Agnostic Approach


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...