You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SDL2游戏FPS波动异常问题排查及优化方案咨询

关于SDL2中delta_time计算与FPS波动的问题

我用C语言结合SDL2开发游戏,目前仅实现了空白屏幕上矩形移动的基础功能,采用以下代码计算delta_time:

Uint64 now = SDL_GetPerformanceCounter();
Uint64 last = 0;
double delta_time = 0;
while (running) {
    last = now;
    now = SDL_GetPerformanceCounter();
    delta_time = (double)(now - last) / SDL_GetPerformanceFrequency();
}

但出现了严重的FPS波动,部分波动数值如下:

385.733269
1021.394121
3567.873440
364.403997
276.115181
700.447376
393.435451
571.590903
494.964479
532.557362
1445.981040
593.772396
1938.300033
468.804132
1008.120410
195.745012

相关核心代码如下:

void renderContext(GraphicContext* to_render, const Entity* player) {
    if (to_render->renderer == NULL) {
        return;
    }
    SDL_SetRenderDrawColor(to_render->renderer, 255, 255, 255, 255);
    SDL_RenderClear(to_render->renderer);
    SDL_SetRenderDrawColor(to_render->renderer, 255, 255, 0, 255);
    SDL_RenderFillRect(to_render->renderer, (const SDL_Rect*)&player->dimensions);
    SDL_RenderPresent(to_render->renderer);
}

static void handleKeyboard(Entity* player, double delta_time) {
    const Uint8* keys = SDL_GetKeyboardState(NULL);
    double delta_dist = calculateDeltaDist(player, delta_time);
    printf("%lf\n", 1000.0/delta_dist);
    fflush(stdout);

    if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]) {
        player->dimensions.x -= delta_dist;
    } if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]) {
        player->dimensions.x += delta_dist;
    } if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]) {
        player->dimensions.y -= delta_dist;
    } if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]) {
        player->dimensions.y += delta_dist;
    }
}

int handleEvents(Entity* player, double delta_time) {
    SDL_Event event;

    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            return 1;
        }
    }
    handleKeyboard(player, delta_time);
    return 0;
}

int main(int argc, char** argv) {
    GraphicContext m_context = createGraphicContext(SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1200, 800,
                                                    "My Game", SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_ALWAYS_ON_TOP);
    int running = 1;
    Entity player = createEntity(100, 100, 32, 32, 100, 1000);
    Uint64 now = SDL_GetPerformanceCounter();
    Uint64 last = 0;
    double delta_time = 0;

    if (m_context.win == NULL) {
        puts("Unable to generate window. Exiting.");
        exit(0);
    }
    while (running) {
        last = now;
        now = SDL_GetPerformanceCounter();
        delta_time = (double)(now - last) / SDL_GetPerformanceFrequency();
        if (handleEvents(&player, delta_time)) {
            break;
        }
        renderContext(&m_context, &player);
    }
    quitSDL(&m_context);
    return 0;
}

目前添加SDL_RENDERER_PRESENTVSYNC渲染标志后,FPS能稳定在60,但想请教:

  1. 当前的delta_time计算方式是否存在错误?
  2. 有没有更可靠的delta_time实现方法?

编译命令:gcc -Wall -Wextra -Ofast src/main.c -lSDL2 -o build/Game


问题解答

1. 当前delta_time计算的问题

你的delta_time计算存在初始化逻辑错误:

  • 循环启动前,last被初始化为0,第一次循环时last = now(此时now是程序初始化阶段的计数器值),随后now被更新为当前计数器值,这会导致第一次循环的delta_time计算错误。不过这不是FPS剧烈波动的核心原因。
  • FPS波动的本质是游戏循环没有帧率控制:CPU会全速执行循环,每次循环的执行时间受系统负载影响极大(空闲时极快,被其他进程占用时变慢),且SDL_RenderPresent在无VSYNC时会立即返回,不等待显示器刷新,最终导致帧率剧烈波动。

2. 更可靠的delta_time实现方法

方法一:修正初始化+可选帧率限制

先修正delta_time的初始化逻辑,确保每一次循环的计算都正确,同时可添加帧率限制避免CPU空转:

int main(int argc, char** argv) {
    // ... 其他初始化代码 ...
    Uint64 last = SDL_GetPerformanceCounter(); // 初始化为当前计数器值
    double delta_time = 0.0;

    if (m_context.win == NULL) {
        puts("Unable to generate window. Exiting.");
        exit(0);
    }
    while (running) {
        Uint64 now = SDL_GetPerformanceCounter();
        delta_time = (double)(now - last) / SDL_GetPerformanceFrequency();
        last = now; // 循环开始时更新时间差,避免重复赋值错误

        if (handleEvents(&player, delta_time)) {
            break;
        }
        renderContext(&m_context, &player);

        // 可选:限制帧率为60FPS,避免CPU占用过高
        const double target_fps = 60.0;
        double frame_cost_ms = delta_time * 1000.0;
        if (frame_cost_ms < 1000.0 / target_fps) {
            Uint32 delay_ms = (Uint32)(1000.0 / target_fps - frame_cost_ms);
            SDL_Delay(delay_ms);
            // 重新计算延迟后的时间差,更新delta_time和last
            Uint64 after_delay = SDL_GetPerformanceCounter();
            delta_time += (double)(after_delay - now) / SDL_GetPerformanceFrequency();
            last = after_delay;
        }
    }
    // ... 清理代码 ...
}

方法二:固定时间步长循环(推荐用于物理模拟游戏)

如果游戏涉及物理逻辑,固定时间步长能保证物理更新频率稳定,渲染则根据累计时间差做插值,兼顾逻辑稳定和画面平滑:

int main(int argc, char** argv) {
    // ... 其他初始化代码 ...
    const double fixed_update_step = 1.0 / 60.0; // 固定60次/秒的逻辑更新
    double accumulator = 0.0;
    Uint64 last = SDL_GetPerformanceCounter();

    if (m_context.win == NULL) {
        puts("Unable to generate window. Exiting.");
        exit(0);
    }
    while (running) {
        Uint64 now = SDL_GetPerformanceCounter();
        double delta_time = (double)(now - last) / SDL_GetPerformanceFrequency();
        last = now;

        accumulator += delta_time;

        // 先处理事件,避免输入延迟
        if (handleEvents(&player, delta_time)) {
            break;
        }

        // 按固定步长更新逻辑/物理
        while (accumulator >= fixed_update_step) {
            handleKeyboard(player, fixed_update_step); // 使用固定步长更新移动逻辑
            accumulator -= fixed_update_step;
        }

        // 渲染时用插值系数优化画面平滑度(需修改renderContext支持插值)
        double interpolation = accumulator / fixed_update_step;
        renderContext(&m_context, &player, interpolation);
    }
    // ... 清理代码 ...
}

补充说明

  • SDL_RENDERER_PRESENTVSYNC是解决帧率波动最简便的方案,它会让SDL_RenderPresent等待显示器垂直同步信号,帧率被限制在显示器刷新率(通常60/144FPS),此时delta_time会稳定在对应区间。
  • 你通过delta_time计算delta_dist来控制物体移动的逻辑是正确的,只要delta_time计算准确,物体移动速度就能保持稳定,不受帧率波动影响。

内容的提问来源于stack exchange,提问作者Syntax Error12

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 00:19:50