自制引擎2D单机游戏适配不同FPS,保证游戏速度一致的方法
Hey there! I’ve dealt with this exact headache while building my own 2D engine—nothing’s more annoying than your game speeding up on a high-end PC or dragging to a crawl on older hardware, right? Let’s walk through the standard, battle-tested solutions to keep your game’s pace consistent no matter the frame rate.
核心思路:分离逻辑更新与渲染
The key fix here is decoupling your game’s logic updates (movement, physics, AI, etc.) from its rendering. Rendering can happen as fast as the hardware allows, but logic should run at a fixed, consistent interval. Here’s how to implement it:
1. 使用固定时间步长(Fixed Timestep)
Instead of updating logic once per frame (which depends on FPS), you run logic in fixed time increments—say, 60 times per second (1/60 ≈ 0.0167 seconds per step). This way, no matter how many frames are rendered, your game’s core mechanics progress at the same rate.
Here’s a simplified pseudocode example that you can adapt to your engine:
float accumulator = 0.0f; const float fixedDeltaTime = 1.0f / 60.0f; // 60 logic updates per second float lastTimestamp = getCurrentTime(); // Get system time in seconds while (gameIsRunning) { float currentTimestamp = getCurrentTime(); float frameDelta = currentTimestamp - lastTimestamp; lastTimestamp = currentTimestamp; // Clamp delta to prevent spiral of death if game freezes briefly frameDelta = std::min(frameDelta, 0.25f); // Accumulate the time since last frame accumulator += frameDelta; // Run logic in fixed steps until we catch up to real time while (accumulator >= fixedDeltaTime) { // Pass fixedDeltaTime to ALL logic functions updatePlayerMovement(fixedDeltaTime); updatePhysics(fixedDeltaTime); updateGameState(fixedDeltaTime); accumulator -= fixedDeltaTime; } // Render with interpolation for smoothness (more on this below) float alpha = accumulator / fixedDeltaTime; renderGame(alpha); }
Why this works:
When calculating movement (e.g., a player moving 100 pixels per second), you multiply the speed by fixedDeltaTime instead of relying on FPS:
// Player moves 100px/s consistently, regardless of FPS player.position.x += 100.0f * fixedDeltaTime;
This ensures every logic step moves the player the same tiny distance, so over a full second, they’ve moved exactly 100 pixels—whether the game rendered 20 frames or 1000.
2. 用插值解决低FPS下的跳帧问题
If your game is running at 20 FPS but logic updates 60 times per second, rendering only the latest logic state will make movement look jerky. The alpha value we calculated (a number between 0 and 1) lets you interpolate between the previous logic state and current logic state to smooth out rendering.
For example, track both the previous and current position of your player:
// Inside your logic update: player.previousPosition = player.position; player.position.x += 100.0f * fixedDeltaTime; // Inside rendering: float interpolatedX = player.previousPosition.x + (player.position.x - player.previousPosition.x) * alpha; drawPlayer(interpolatedX, player.position.y);
This makes the rendered position smoothly transition between logic steps, even when frames are sparse.
3. 放弃基于FPS的参数调整
Your earlier attempt to adjust movement based on FPS (e.g., speed / FPS) is error-prone because FPS fluctuates frame-to-frame. A sudden drop from 1000 FPS to 20 would make the speed jump drastically, causing noticeable stutters. Fixed timestep eliminates this by tying logic to real time, not frame count.
额外小贴士
- Test edge cases: Simulate low FPS (e.g., cap it at 20) to make sure your logic doesn’t break and rendering stays smooth.
- Adjust fixed step rate: 60 updates per second is standard, but you can use 30 if your logic is heavy—just make sure it’s a rate that feels smooth for your game.
内容的提问来源于stack exchange,提问作者Agney

