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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 22:46:02