在进行游戏测试时(Flappigtrail,v0.1.2),我注意到跳跃高度适用于 144 FPS,但不适用于 60 FPS(60 FPS 的跳跃高度要低得多)。
当玩家跳跃时,player_dy变为-2.7(jump_height“是一个常量”==2.7);这意味着每帧播放将获得-2.7(+累积重力)。
但请注意,由于重力为 0.06,因此需要:
|跳跃高度 | * 重力 == 2.7 / 0.06 == 45 帧(144 fps),将player_dy 变为 0,然后通过附加帧为负(添加重力;下落而不是飞起)。
请注意,45 帧/144 FPS == 0.31 秒
我们可以粗略地建模(也许我错了?)通过 nb_frames / 2 * Jump_height == 22.5 * -2.7 == -60.75 => 144 FPS 的跳跃向上获得 61 像素获得的高度。
但是,在 60 FPS 下,如果单位高度获得的重力保持不变,则仍需要 45 帧才能将player_dy 变为 0,然后再增加帧为负值。
请注意,45 帧/60 FPS == 0.75 秒(因此长度是两倍多)。
另请注意,虽然跳跃高度与以前相同(即 61 像素),但到达player_dy == 0 需要两倍多的时间,给人一种“在月球上跳跃”的感觉(这不是我们想要的)。
因此,我想到使用 BASE FPS (144) / CUR_FPS(例如 60 FPS)的比率来更新玩家应从跳跃中获得的高度以及在每帧施加重力时玩家应失去的高度。
请注意我们将跳跃高度 (player_dy) 从 -2.7 更新为 -2.7 * (BASE_FPS / CUR_FPS) == -2.7 * 144/60 ~= -2.7 * 2.4 ~= -6.48 的场景
在这个更大的跳跃高度和重力不变的情况下,需要:
|跳跃高度 | * 重力 == 6.48 / 0.06 == 108 帧,然后将player_dy 调回 0
=> 即 nb_frames / 2 * Jump_height == 108 / 2 * -6.48 == 54 * -6.48 ~= 在重力不变的情况下以 60 FPS 的速度跳跃获得 350 像素
我们大约需要 10 帧才能获得 61 像素(重力保持不变;是的,我没有考虑到gravity_dy造成的player_dy损失,但这里是最小的):
height_to_gain_px / | | 高度到增益像素player_dy(跳转后)| == 61 / 6.48 ~= 10 帧
请注意,10 帧/60 FPS ~= 0.17 秒
但是,我们还将每刻获得的重力从 0.06 更新为 0.06 * (BASE_FPS / CUR_FPS) == 0.06 * 144/60
〜= 0.06 * 2.4 == 0.144
有了更大的重力,需要:
|跳跃高度 | * 重力 == 6.48 / 0.144 == 45 帧,然后将player_dy 调回 0
=> 即 nb_frames / 2 * Jump_height == 45 / 2 * -6.48 == 22.5 * -6.48 ~= 在重力不变的情况下以 60 FPS 的速度跳跃获得 146 像素
我们大约需要 10 帧才能获得 61 像素(重力保持不变;是的,我没有考虑到gravity_dy造成的player_dy损失,但这里是最小的):
height_to_gain_px / | | 高度到增益像素player_dy(跳转后)| == 61 / 6.48 ~= 10 帧
请注意,10 帧/60 FPS ~= 0.17 秒
尽管如此,实际中 60 FPS 的跳跃似乎仍然比这高得多......
有什么线索吗?
编辑:
如果我们只更新重力:
重力 * (BASE_FPS / CUR_FPS) == 0.06 * 2.4 == 0.144
|跳跃高度 | * 重力 == 2.7 / 0.144 == 18.75 帧(60 FPS)将player_dy 变为 0。
请注意,18.75 帧/60 FPS == 0.31 秒
这应该有效,哈哈
除了——情节扭曲——事实并非如此
帮助...
参见下面相关代码
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();
}
/* 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
}
}
}
我并没有真正遵循你所有的计算,但是当你以正确的方式看待它时,数学非常简单。关键是忘记 fps,只使用 deltaTime 和常规单位。
假设位置以像素为单位,速度以像素/秒为单位,deltaTime 以秒为单位,重力为
GRAVITY
,以像素/秒/秒为单位。
那么对于每一帧,计算很简单:
newY = oldY + speed * deltaTime;
newSpeed = oldSpeed + GRAVITY * deltaTime;