[SOLVED] Variable Jump Height

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
secretsue92
Prole
Posts: 3
Joined: Tue Apr 23, 2024 12:01 am

[SOLVED] Variable Jump Height

Post by secretsue92 »

Hello, I'm making a Mario clone, and I'm stuck implementing variable jump height. Regarding the player's vertical movement, I've added:
- falling with gradually increasing speed that is capped at a certain point
- jumping with fixed height (the jump height is really small, it's meant to be the minimum jump height if you just press jump for 1 frame)
- not letting you jump from holding the button, you need to re-press it

Here's the relevant code from the player update function:

Code: Select all

    -- this runs just after the collision function
    if self.yaccel < 20 then self.yaccel = self.yaccel + 2.5 else self.yaccel = 20 end
    if self.yspeed < self.map.gravity then self.yspeed = self.yspeed + self.yaccel else self.yspeed = self.map.gravity end
    if self.onground then
        self.yspeed = 0
        self.yaccel = 0
        if keyboard.keys.jump and self.canjump then
            self.yspeed = -160
            self.jumptimer = 0.4
            self.canjump = false
        end
    end
    if self.jumptimer > 0 and keyboard.keys.jump then 
        self.jumptimer = self.jumptimer - dt
    else 
        self.jumptimer = 0
        self.canjump = false
    end
    if not keyboard.keys.jump then self.canjump = true end
    
    self.y = self.y + self.yspeed * dt
I did succeed with adding variable jump height, but it felt very unnatural and it was more like a jetpack than a real Mario jump and it used different code (I rewrote this from scratch multiple times).

Help is greatly appreciated
-S
Last edited by secretsue92 on Tue Apr 23, 2024 7:22 am, edited 1 time in total.
User avatar
marclurr
Party member
Posts: 146
Joined: Fri Apr 22, 2022 9:25 am

Re: Variable Jump Height

Post by marclurr »

This is how I've done it in a pico8 game I'm working on.

Code: Select all

if not btn(btnjmp) and jt<10 then
	jmpheld=false
end

jt+=1
if jt<=minjmp or (jmpheld and jt<=maxjmp) then
	plyr.dy=-1
end
I run this code while the player is jumping after gravity is applied. It essentially overrides the gravity when the player is traveling upwards. It took a huge amount of tweaking the gravity and min and max hold values to make it feel like a good jump. I also had to record the start and end heights of the jump to check the player jumped a useful height (just over 1 tile for min and just over 2 tiles for max in my case). I got this method from a video I came across a few years ago which went through the source code for how the jump works in an old NES game.

Edit: you'll want to do your timings with actual time values rather than frames. I used frames because pico8 has a fixed update that's fairly reliable.
secretsue92
Prole
Posts: 3
Joined: Tue Apr 23, 2024 12:01 am

Re: Variable Jump Height

Post by secretsue92 »

marclurr wrote: Tue Apr 23, 2024 6:50 am This is how I've done it in a pico8 game I'm working on.

Code: Select all

if not btn(btnjmp) and jt<10 then
	jmpheld=false
end

jt+=1
if jt<=minjmp or (jmpheld and jt<=maxjmp) then
	plyr.dy=-1
end
I run this code while the player is jumping after gravity is applied. It essentially overrides the gravity when the player is traveling upwards. It took a huge amount of tweaking the gravity and min and max hold values to make it feel like a good jump. I also had to record the start and end heights of the jump to check the player jumped a useful height (just over 1 tile for min and just over 2 tiles for max in my case). I got this method from a video I came across a few years ago which went through the source code for how the jump works in an old NES game.

Edit: you'll want to do your timings with actual time values rather than frames. I used frames because pico8 has a fixed update that's fairly reliable.
Thanks, but could you explain what the variable dy is in this case? Is it the player's Y position, Y speed, Y acceleration? Sorry if this is a dumb question haha

-S
User avatar
marclurr
Party member
Posts: 146
Joined: Fri Apr 22, 2022 9:25 am

Re: Variable Jump Height

Post by marclurr »

No problem. dy is the y velocity of the player, analogous to your yspeed. There's a separate function that handles moving and colliding the player using dx and dy but it doesn't affect how the jump works (Aside from setting those values to 0 in the event of a collision on the relevant axis)
secretsue92
Prole
Posts: 3
Joined: Tue Apr 23, 2024 12:01 am

Re: Variable Jump Height

Post by secretsue92 »

marclurr wrote: Tue Apr 23, 2024 7:13 am No problem. dy is the y velocity of the player, analogous to your yspeed. There's a separate function that handles moving and colliding the player using dx and dy but it doesn't affect how the jump works (Aside from setting those values to 0 in the event of a collision on the relevant axis)
Ok, after making the y velocity go down on every frame of the jump timer, I've more or less achieved the desired result. Thank you
RNavega
Party member
Posts: 355
Joined: Sun Aug 16, 2020 1:28 pm

Re: [SOLVED] Variable Jump Height

Post by RNavega »

I know that this was marked as solved, but I wanted to investigate it further.

When playing Super Mario World on some SNES emulator, the jump mechanic seems to let the player do either short jumps that reach 3 tiles high, or long jumps that reach 4 tiles high, and if the player releases the jump key with a certain timing, they reach somewhere inbetween these two heights.

At first I tried doing it with these standard velocity and position calculations:

Code: Select all

v = v + a * dt
s = s + v * dt
I was told that these formulas are a variation of "Euler's method". They work fine and are easy to use, but they seem to have certain precision errors.
In my case, the player would either consistently miss or overshoot the desired height.

Changing the formulas to actual kinematic equations solved this problem, and the player can reach the short jump height or the long jump height with precision, depending on how long the player holds the jump key.

I also used a different way of making the character fall: the jump impulse is one state (from the ground, the player starts moving up and decelerating until they reach the maximum jump height), after which the falling state begins. This is needed if you want to support the player walking off a platform and gravity kicking in for example, so the falling state begins.
To organize things, I structured these like "physics effects" onto the player, so that per frame the player can have one or more "physics effects" happening at once. This is useful in that you can add a physics effect for like "sliding on ice", where you move the character and they continue sliding after you released all keys.

I've packed this into a standalone example, it's just main.lua.

preview.png
preview.png (24.36 KiB) Viewed 1340 times

Github: https://github.com/RNavega/OtherSamples ... World-Jump

Code: Select all

-- ==========================================================
-- Example of a Super Mario World style jump mechanic, where
-- the player can control how far they should jump by holding
-- the jump key, with a maximum possible jump height.
-- By Rafael Navega (2024)
--
-- License: Public Domain
-- ==========================================================

io.stdout:setvbuf('no')


local TILE_HEIGHT = 64
local PLAYER_HEIGHT = TILE_HEIGHT * 2.0
local FLOOR_Y = 500

-- How many tiles high that a SHORT jump should reach.
local SHORT_JUMP_TILES = 3
-- How many tiles high that a LONG jump should reach.
local LONG_JUMP_TILES = 5
-- Distance before which any jump key releases will cause a short jump.
local TOLERANCE_MIN_HEIGHT = TILE_HEIGHT * 1.8
-- Distance after which the player starts decelerating and will reach
-- a long jump height.
local TOLERANCE_MAX_HEIGHT = SHORT_JUMP_TILES * TILE_HEIGHT

-- Initial jump speed, in pixels per second.
-- Completely subjective, choosing a value that "feels right".
local INITIAL_SPEED = TILE_HEIGHT * 24.0
-- Gravity when falling, in pixels per second.
-- Also subjective.
local GRAVITY = TILE_HEIGHT * 220.0

local playerPosition = {100.0, FLOOR_Y - PLAYER_HEIGHT}
local drawDebug = true


local relevantKeys = {
    ['left'] = false,
    ['right'] = false
}

-- Forward declarations of the player physics effects, so they can reference
-- each other inside their handling functions.
local jumpImpulseEffect, fallEffect, steerEffect

jumpImpulseEffect = {
    active = false,
    s=0.0, u=nil, a=nil, t=0.0,
    decelerate=false, aT=0.0,
    -- Debug fields..
    maxS=0.0,
    startKey=nil,
    holdDistance=0.0,
}

function jumpImpulseEffect:start(startKey)
    local ji = jumpImpulseEffect
    ji.active = true
    ji.s = 0.0
    ji.u = INITIAL_SPEED
    ji.a = nil
    ji.decelerate = false
    ji.t = 0.0
    ji.aT = 0.0
    -- Debug fields.
    ji.startKey = startKey
    ji.maxS = 0.0
    ji.holdDistance = 0.0
end

function jumpImpulseEffect:startDecelerating()
    if self.decelerate then
        return
    else
        self.decelerate = true
    end
    -- Record the current 'time' of the jump, so the
    -- acceleration time can start from zero from now on.
    self.aT = 0.0

    -- Desired distance in pixels that the player should always reach when jumping up.
    local desiredDistance
    -- Change this 'extraPixels' value to +3 or something like that so it's added
    -- to the desired jump height. This can be used as a safety precaution so the
    -- player will reach slightly above the tile height that they aimed for
    -- and ensuring that they land there.
    local extraPixels = 0.0
    if self.s >= TOLERANCE_MIN_HEIGHT then
        -- Player released the jump key after reaching at least the minimum
        -- tolerance height, so do a linear blend between the short jump
        -- and long jump depending on how far they've already traveled.
        local minHeight = SHORT_JUMP_TILES * TILE_HEIGHT
        local maxHeight = LONG_JUMP_TILES * TILE_HEIGHT
        local heightDiff = TOLERANCE_MAX_HEIGHT - TOLERANCE_MIN_HEIGHT
        local factor = (self.s - TOLERANCE_MIN_HEIGHT) / heightDiff
        if factor > 1.0 then factor = 1.0 end
        local blendedHeight = minHeight + (maxHeight - minHeight) * factor
        desiredDistance = blendedHeight - self.s + extraPixels
    else
        -- Player released the key before reaching the tolerance height, so
        -- force the player to reach a "short jump" height.
        desiredDistance = SHORT_JUMP_TILES * TILE_HEIGHT - self.s + extraPixels
    end
    -- Solving for (a) to find the acceleration that'll make the player hit that
    -- desired jump height as the maximum, with (v²) being 0.0 as that's the speed
    -- at the highest point of the jump, after which the player starts falling.
    -- v² = u² + 2 * a * s
    -- 2 * s * a = v² - u²
    -- a = (v² - u²) / (2 * s)
    self.a = (0.0 - (self.u * self.u)) / (2.0 * desiredDistance)
end

function jumpImpulseEffect:update(dt)
    -- Classic kinematic equations (AKA "suvat" equations):
    -- A) u = s / t - 1/2 * a * t
    -- B) v = u + a * t
    -- C) s = u * t + 1/2 * a * t²
    -- D) v² = u² + 2 * a * s
    -- E) s = 1/2 * (u + v) * t
    --
    -- Using equation (C) to find (s), used as the jump offset to be
    -- added to the player position:
    -- s = (u * t) + (1/2 * a * t²)
    -- s = uniform_distance + accelerated_distance
    --
    -- The gravity only kicks in when the player has either released the jump key,
    -- or has traveled far enough that they made it clear that they want to do a
    -- long jump (uniformDistance is above a threshold).
    --
    -- Once the jump reaches its highest point the velocity of the jump becomes
    -- zero, as it's about to change sign and become negative. From this point
    -- on the player will start falling.
    self.t = self.t + dt
    local uniformDistance = self.u * self.t

    if self.decelerate then
        self.aT = self.aT + dt
        local accelDistance = 0.5 * self.a * (self.aT * self.aT)
        self.s = uniformDistance + accelDistance

        local v = self.u + self.a * self.aT
        if v < 0.0 then
            fallEffect:start(self.s, self.t)
            self.active = false
        end
    else
        self.s = uniformDistance
        self.holdDistance = uniformDistance
        -- If the player has jumped above a threshold, force a long jump.
        -- Note that you could also use "time in seconds" as the threshold,
        -- in this way: if self.t >= TIME_LIMIT then (...).
        if self.s >= TOLERANCE_MAX_HEIGHT then
            self:startDecelerating()
        end
    end
    self.maxS = self.s > self.maxS and self.s or self.maxS
end


fallEffect = {
    active=false,
    s=0.0, u=nil, a=nil, t=0.0, aT=0.0,
}

function fallEffect:start(s, t)
    self.active = true
    self.s = s
    self.t = t
    self.a = GRAVITY
    self.v = 0.0
    self.aT = 0.0
end

function fallEffect:update(dt)
    self.t = self.t + dt
    self.aT = self.aT + dt
    -- Using "Euler's method", which is less precise than using a
    -- kinematic equation, but it works perfectly for a falling effect.
    self.v = self.v + self.a * dt
    -- A top-cap on the falling speed, in pixels per second.
    -- Subjective value, using what feels right.
    -- Try setting a small value like (TILE_HEIGHT * 5) to see it more clearly.
    local SPEED_LIMIT = TILE_HEIGHT * 300.0
    if self.v > SPEED_LIMIT then
        self.v = SPEED_LIMIT
    end
    self.s = self.s - self.v * dt
    -- For the purposes of this example, consider the character on ground
    -- when the jump offset goes below zero (goes back to the ground).
    --
    -- In an actual game, in love.update() you'd check for collisions and
    -- disable this falling effect when the character lands on solid ground.
    if self.s < 0.0 then
        self.s = 0.0
        self.active = false
    end
end


-- Left and right steering.
steerEffect = {MOVE_SPEED = TILE_HEIGHT * 6.0}

function steerEffect:start(positionTable)
    self.pt = positionTable
end

function steerEffect:update(dt)
    if relevantKeys.right then
        self.pt[1] = self.pt[1] + self.MOVE_SPEED * dt
        if self.pt[1] > 800 - TILE_HEIGHT then
            self.pt[1] = 800 - TILE_HEIGHT
        end
    elseif relevantKeys.left then
        self.pt[1] = self.pt[1] - self.MOVE_SPEED * dt
        if self.pt[1] < 0 then
            self.pt[1] = 0
        end
    end
end


function love.load()
    steerEffect:start(playerPosition)
    love.window.setTitle('Super Mario World jump example')
end


function love.update(dt)
    if jumpImpulseEffect.active then
        jumpImpulseEffect:update(dt)
    end
    if fallEffect.active then
        fallEffect:update(dt)
    end
    steerEffect:update(dt)
end


function love.draw()
    love.graphics.setColor(0.5, 0.5, 0.5)
    love.graphics.rectangle('fill', 0, FLOOR_Y, 800, 600 - FLOOR_Y)

    love.graphics.setColor(1.0, 1.0, 1.0)
    for y = 1, SHORT_JUMP_TILES do
        love.graphics.rectangle('line', 300, FLOOR_Y - y * TILE_HEIGHT, TILE_HEIGHT, TILE_HEIGHT)
    end
    for y = 1, LONG_JUMP_TILES do
        love.graphics.rectangle('line', 500, FLOOR_Y - y * TILE_HEIGHT, TILE_HEIGHT, TILE_HEIGHT)
    end
    for y = 1, 10 do
        love.graphics.rectangle('line', 700, FLOOR_Y - y * TILE_HEIGHT, TILE_HEIGHT, TILE_HEIGHT)
    end

    local ji = jumpImpulseEffect
    if drawDebug then
        love.graphics.setColor(0.0, 0.8, 0.8)
        local limitY = playerPosition[2] - ji.maxS
        love.graphics.line(0, limitY, 800, limitY)
        love.graphics.line(0, limitY + PLAYER_HEIGHT, 800, limitY + PLAYER_HEIGHT)
        love.graphics.setColor(0.0, 0.3, 0.8)
        local holdY = FLOOR_Y - ji.holdDistance
        love.graphics.rectangle('fill', 10, holdY, TILE_HEIGHT, ji.holdDistance)
        if holdY < FLOOR_Y - TOLERANCE_MIN_HEIGHT then
            if holdY < FLOOR_Y - TOLERANCE_MAX_HEIGHT then
                holdY = FLOOR_Y - TOLERANCE_MAX_HEIGHT
            end
            local innerHeight = ji.holdDistance - TOLERANCE_MIN_HEIGHT
            if innerHeight > TOLERANCE_MAX_HEIGHT - TOLERANCE_MIN_HEIGHT then innerHeight = TOLERANCE_MAX_HEIGHT - TOLERANCE_MIN_HEIGHT end
            love.graphics.setColor(0.8, 0.7, 0.0)
            love.graphics.rectangle('fill', 10, holdY, TILE_HEIGHT, innerHeight)
        end
        love.graphics.line(10, holdY, playerPosition[1], holdY)
        love.graphics.setColor(0.6, 0.6, 0.0)
        love.graphics.line(10, FLOOR_Y - TOLERANCE_MIN_HEIGHT, TILE_HEIGHT + 10, FLOOR_Y - TOLERANCE_MIN_HEIGHT)
        love.graphics.line(10, FLOOR_Y - TOLERANCE_MAX_HEIGHT, TILE_HEIGHT + 10, FLOOR_Y - TOLERANCE_MAX_HEIGHT)
    end

    -- Draw the player.
    love.graphics.setColor(0.0, 0.8, 0.0)
    local activeEffect = ji.active and ji or fallEffect
    love.graphics.rectangle('fill', playerPosition[1], playerPosition[2] - activeEffect.s,
                            TILE_HEIGHT, PLAYER_HEIGHT)

    love.graphics.setColor(1.0, 1.0, 1.0)
    love.graphics.print(('Offset: %.03f'):format(activeEffect.s), 10, 10)
    love.graphics.print('On Ground: ' .. tostring(not activeEffect.active), 10, 30)
    love.graphics.print(('Stopwatch: %.03fs'):format(activeEffect.t), 10, 50)
    love.graphics.print('Press Tab to toggle the debug drawings ('
                        .. (drawDebug and 'ON' or 'OFF') .. ')',
                        10, 70)
    love.graphics.print([[- Use the Left and Right keys to steer.
- Tap any key to short jump
- Hold any key to long jump
- Press Esc to quit]], 10, 90)
end


function love.keypressed(key)
    if key == 'escape' then
        love.event.quit()
    elseif key == 'tab' then
        drawDebug = not drawDebug
    elseif key == 'right' then
        relevantKeys['right'] = true
    elseif key == 'left' then
        relevantKeys['left'] = true
    elseif not (jumpImpulseEffect.active or fallEffect.active) then
        jumpImpulseEffect:start(key)
    end
end


function love.keyreleased(key)
    if key == 'right' then
        relevantKeys['right'] = false
    elseif key == 'left' then
        relevantKeys['left'] = false
    elseif jumpImpulseEffect.active and jumpImpulseEffect.startKey == key then
        jumpImpulseEffect:startDecelerating()
    end
end
Post Reply

Who is online

Users browsing this forum: No registered users and 6 guests