问题描述
我正在 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 个连续绘图到物理监视器之间的实际时间是否不是恒定的? 因为如果是这种情况,如果没有某种反馈,您将无法实现我正在尝试的目标最后一次绘图的确切时间......
附言更新和渲染非常快 - 例如如果循环不受限制,它可以每秒进行数百万次。所以这并没有减慢周期。
#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