When playtesting (Flappigtrail, v0.1.2), I noticed that the jump height works for 144 FPS but not so much for 60 FPS (the jump height is way lower for 60 FPS).
When the player jumps, player_dy becomes -2.7 (jump_height “is a constant” == 2.7); meaning that the playing will gain -2.7 (+ cumulative gravity ) per frame.
However, note that since gravity is 0.06, it will take:
| jump_height | * gravity == 2.7 / 0.06 == 45 frames at 144 fps to turn player_dy into 0, then negative with additional frames (add gravity; falling instead of flying up).
Note that 45 Frames / 144 FPS == 0.31 seconds
We can roughly model (maybe I’m wrong here ?) the height gained with nb_frames / 2 * jump_height == 22.5 * -2.7 == -60.75 => 61 pixels gained upwards for the jump at 144 FPS.
However, at 60 FPS, if gravity gained per height remains the same, it will still take 45 frames to get player_dy to 0, then negative with additional frames.
Note that 45 Frames / 60 FPS == 0.75 seconds (so more than twice as long).
Also note that while the jump height is the same as before (i.e. 61 pixels), it will take more than twice as long to get to player_dy == 0, giving this “jumping on the moon” feel (it is not desired).
Therefore, I thought of using the ratio BASE FPS (144) / CUR_FPS (e.g. 60 FPS) to update the amount of height the player should get from jumping && the amount of height the player should lose when get gravity applied each frame.
Note the scenario where we updated height gained (player_dy) from jump from -2.7 to -2.7 * (BASE_FPS / CUR_FPS) == -2.7 * 144/60 ~= -2.7 * 2.4 ~= -6.48
With this greater jump height and gravity unchanged, it will take:
| jump_height | * gravity == 6.48 / 0.06 == 108 frames before turning player_dy back to 0
=> i.e. nb_frames / 2 * jump_height == 108 / 2 * -6.48 == 54 * -6.48 ~= 350 pixels gained from the jump at 60 FPS with gravity unchanged
It will take us roughly 10 frames to gain 61 pixels (with gravity remained unchanged; yes, I have not accounted to player_dy losses from gravity_dy but it’s minimal here):
height_to_gain_px / | player_dy (after jump) | == 61 / 6.48 ~= 10 frames
Note that 10 Frames / 60 FPS ~= 0.17 seconds
However, we would also update the gravity gained per tick from 0.06 to 0.06 * (BASE_FPS / CUR_FPS) == 0.06 * 144/60
~= 0.06 * 2.4 == 0.144
With this greater gravity it will take:
| jump_height | * gravity == 6.48 / 0.144 == 45 frames before turning player_dy back to 0
=> i.e. nb_frames / 2 * jump_height == 45 / 2 * -6.48 == 22.5 * -6.48 ~= 146 pixels gained from the jump at 60 FPS with gravity unchanged
It will take us roughly 10 frames to gain 61 pixels (with gravity remained unchanged; yes, I have not accounted to player_dy losses from gravity_dy but it’s minimal here):
height_to_gain_px / | player_dy (after jump) | == 61 / 6.48 ~= 10 frames
Note that 10 Frames / 60 FPS ~= 0.17 seconds
Nonetheless, it seems that the 60 FPS jump in practice is still way higher than that…
Any clue ?
Edit:
If we only update gravity:
gravity * (BASE_FPS / CUR_FPS) == 0.06 * 2.4 == 0.144
| jump_height | * gravity == 2.7 / 0.144 == 18.75 Frames at 60 FPS to turn player_dy into 0.
Note that 18.75 Frames / 60 FPS == 0.31 seconds
This should work lol
Except–plot twist–it doesn’t
help…
See relevant code below
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
}
}
}