FlappyBird类JS小游戏:帧率波动下跳跃高度与重力不一致问题排查
问题:Flappigtrail v0.1.2 跨帧率跳跃高度不一致问题
测试Flappigtrail v0.1.2时,发现跳跃高度在144 FPS下表现正常,但在60 FPS下明显偏低。
问题原理分析
玩家跳跃时,player_dy设为-2.7(jump_height为常量2.7),即每帧获得-2.7的垂直速度,叠加累计重力:
- 在144 FPS下,重力为0.06,需要45帧(约0.31秒)使
player_dy变为0,之后开始下落,跳跃高度约为61像素,符合预期。 - 在60 FPS下,若重力不变,仍需45帧使
player_dy归0,但耗时变为0.75秒,会产生类似「月球跳跃」的拖沓感,不符合预期。
已尝试的调整方案及问题
尝试用基准帧率(144)与当前帧率的比值,调整每帧跳跃速度和重力值,但均未解决:
- 将跳跃速度改为
-2.7*(144/60)≈-6.48,重力不变时,需要108帧归0,跳跃高度约350像素,远超预期; - 同时将重力改为
0.06*(144/60)=0.144,此时理论需45帧归0,但实际测试中60 FPS下跳跃高度仍过高; - 后续尝试仅调整重力为0.144,理论上需18.75帧(约0.31秒)使
player_dy归0,但实际无改善。
相关代码片段
常量定义
const BASE_FPS = 144; const BASE_GRAVITY = IS_MOBILE ? 0.035 : 0.06; // height lost per tick const BASE_JUMP_Y = IS_MOBILE ? -1.3 : -2.7; // player height gained per jump
主更新逻辑
// GAME LOGIC (per tick; e.g. 60 FPS) function update(timeStamp) { requestAnimationFrame(update); // request next frame calcDeltaTime(timeStamp); if (!gameStarted) { return; // skip game logic updates if game did not start yet } // On player loss, display gameOver and retry texts if (gameOver) { spawn_bonus = true; // reset in case player died before last_digit % 10 == 0 drawGameOverText(); // draw gameOver text in center of screen if (!wroteRetry) { // write retry only once waitAndDrawRetryText(); // draw retry text after GAMEOVER_TIME ms wroteRetry = true; } return; // do not update canvas anymore if player lost } // // Skip current frame (if IRLplayer's PC framerate > FRAMERATE) or clear board/canvas // if (skipFrameOrClearBoard(timeStamp)) { // if we should skip current frame // return; // skip current frame // } context.clearRect(0, 0, board.width, board.height); applyGravityAndBounds(); // constant heigth loss + bounds + gameOver on fall drawPipes(); // draw pipes every INTERVAL seconds (e.g. every 1s) drawScore(); // draw score text drawFPS(); }
计算DeltaTime
/* Function: calcDeltaTime() * ----------------------------------------------------------------------------------- * SUMMARY: * Calculates deltaTime (for each update) */ function calcDeltaTime(timeStamp) { deltaTime = (timeStamp - lastFrameTime) / 1000; // div 1000 for ms -> s lastFrameTime = timeStamp; // 1 FRAME time length passed, update lastFrame2curFrame }
重力与边界处理
/* Function: applyGravityAndBounds() * ----------------------------------------------------------------------------------- * SUMMARY: * Draw score at canvas top-middle */ function applyGravityAndBounds() { // player_dy += gravity * deltaTime * BASE_FPS; // player is subject to GRAVITY player_dy += gravity * (BASE_FPS / (1 / deltaTime)); // applyGrav if (gamemode === GAMEMODE.EXTREME) { player_dy += gravity * (BASE_FPS / (1 / deltaTime)); // double gravity if EXTREME } if (player.y + player_dy < 0) { // if player jumps above ceiling player.y = 0; // block player on ceiling // Else if player fell (and is offscreen) } else if ((player.y + player_dy) > BOARD_HEIGHT) { gameOver = true; } else { // player is in screen (normal case) player.y += player_dy; } player.y = Math.max(player.y + player_dy, 0); // Math.max for ceiling if (player.img) { // wait for player sprite to load context.drawImage(player.img, player.x, player.y, player.width, player.height); } }
跳跃逻辑
/* Function: playerJump() * ----------------------------------------------------------------------------------- * SUMMARY: * If IRLplayer presses Spacebar, ArrowUp or touches the screen (mobile), * makes character jump by player.jump_height */ function playerJump(event) { if (event.code == "Space" || event.code == "ArrowUp" || event.type === "touchstart" || event.type == "click") { if (!gameStarted) { gameStarted = true; // Start game logic spawnPipes(); // spawn first pipe without delay setInterval(spawnPipes, pipeSpawnInterval); // spawn pipes pair from right } // player_dy = player.jump_height * (BASE_FPS / (1 / deltaTime)); player_dy = player.jump_height; console.log("player_dy=", player_dy); console.log("player.jump_height=", player.jump_height); console.log("deltaTime=", deltaTime); console.log("FPS=", 1 / deltaTime); console.log("player.jump_height * deltaTime=", player.jump_height * deltaTime); applyFlap(); // Reset the game if gameOver == true if (gameOver && restarting) { // can only replay after restarting timer resetGlobals();// Reset game fields to defaults } } }
现寻求该问题的解决线索。
内容的提问来源于stack exchange,提问作者Jérémie Ung
相关产品推荐
相关产品推荐

