游戏中的 VSYNC 循环不应该花费恒定的时间吗?

问题描述

我正在 Windows 上使用 SDL 用 C 编写一个小蛇游戏。 在每一帧上,我将每个方块移动恒定数量的像素(例如 2 个像素)。

只要我启用了 Vsync,移动就非常流畅。然而,我试图在没有 Vsync 的情况下实现相同的平滑度,只是为了了解事情是如何工作的,经过一番努力,我设法通过使用性能计数器将周期缩短到几乎完美的 0.016667 秒(此处为 60Hz 监视器)测量重复的 SDL_Delay(1) 直到循环的 0.014 秒 + 最多 0.016667 秒的空循环。这很好用(所以这不是 SDL_Delay(1) 比它应该占用更多的问题)我猜,因为 1000 帧中有 1 帧会延迟 0.1 毫秒,这应该不会引起注意。

但有时运动仍然很不稳定 - 好像它在一帧中根本没有移动,然后在下一帧中追上两倍。我认为这归结为游戏正在更新和渲染的事实,但显示器上的实际显示比它应该的时间早一点,然后比它应该的时间晚一点。

所以我做了一些时间测量,结果让我非常惊讶——见附图。启用 Vsync 后,测量的周期时间到处都是——比非 Vsync 版本差得多——我期待一个完美的恒定时间。 所以问题是:2 个连续绘图到物理监视器之间的实际时间是否不是恒定的? 因为如果是这种情况,如果没有某种反馈,您将无法实现我正在尝试的目标最后一次绘图的确切时间......

附言更新和渲染非常快 - 例如如果循环不受限制,它可以每秒进行数百万次。所以这并没有减慢周期。

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,0x99,0xFF};
const Color BG_COLOR = {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,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,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\n",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,Color color)
{
    SDL_Rect rect = {x,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;
}

解决方法

对于这个问题:两次连续绘图到物理显示器之间的实际时间是否不是恒定的?答案是,因为这取决于它需要什么渲染。

你是说你在每一帧上移动你的方块,这是一个错误,因为如你所见,帧不是在恒定时间内显示的。您需要以独立于帧率的方式对此行为(以及所有游戏逻辑)进行编码,而不是每帧移动 N 个像素,您需要根据时间设置速度,是否可以每 3 毫秒 2 像素,然后每帧你应该计算帧之间的经过时间(谷歌搜索 SDL),并且随着经过的时间你可以移动你的对象。例如(在伪代码中)。

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*/
}

您需要开始考虑时间而不是帧,因为运行游戏的设备可能会在某个时刻“冻结”,例如帧之间的时间可能是 1 秒(它会发生)。也可能是另一种方式,可能有一个设备以您正在开发的两倍帧率运行,如果您将逻辑基于帧,那么该设备将以两倍的速度运行所有内容。

这是一本关于该主题和其他游戏开发基本技术的书:Game Programming Algorithms and Techniques: A Platform-Agnostic Approach