‹ Back to My Mechanics

No showcase footage yet

Character
0 features
Showcase pending
~4 min read

Jumping

Roblox gives you a jump for free. Space bar, character goes up, gravity brings it back down. For a lot of games that's fine and you should leave it alone. But the default jump is floaty, it has no coyote time, and it ignores input the moment your feet leave the ground, which is why platformers built on it feel bad to play.

Here's how the built-in system actually works, and how to replace the parts of it that get in your way.

JumpPower vs JumpHeight

Every Humanoid has a UseJumpPower boolean that decides which property matters.

With UseJumpPower = true (the default on older rigs), the Humanoid applies JumpPower as an upward velocity impulse. Default is 50. The actual height you reach depends on Workspace.Gravity, so if you change gravity your jump changes with it, which is usually not what you want.

With UseJumpPower = false, you set JumpHeight in studs instead and the engine solves for the velocity it needs. Default is 7.2. Gravity changes no longer break your level design. Start here unless you have a reason not to.

local humanoid = character:WaitForChild("Humanoid")
humanoid.UseJumpPower = false
humanoid.JumpHeight = 7.2

The conversion, if you're porting old values:

-- height from power
local height = (power ^ 2) / (2 * workspace.Gravity)
-- power from height
local power = math.sqrt(2 * workspace.Gravity * height)

Detecting a jump

Don't poll humanoid.Jump in a loop. Use the state change event.

humanoid.StateChanged:Connect(function(old, new)
    if new == Enum.HumanoidStateType.Jumping then
        -- fired once, on the frame the jump starts
    elseif new == Enum.HumanoidStateType.Landed then
        -- feet back on ground
    end
end)

Jumping fires once per jump. Freefall follows it about a tenth of a second later. Landed is brief and drops back to Running almost immediately, so if you're doing landing effects, do them on Landed, not on a Running check.

Variable jump height

This is the single biggest upgrade you can make. Tap the button, small hop. Hold it, full jump. Mario has done this since 1985 and players expect it without knowing they expect it.

The trick is to cut the upward velocity when the player releases the button while still rising.

local UIS = game:GetService("UserInputService")
local RunService = game:GetService("RunService")

local CUT_MULTIPLIER = 0.4 -- how much velocity survives an early release

local holding = false

UIS.JumpRequest:Connect(function()
    holding = true
end)

UIS.InputEnded:Connect(function(input)
    if input.KeyCode == Enum.KeyCode.Space then
        holding = false
    end
end)

RunService.Heartbeat:Connect(function()
    local root = character and character:FindFirstChild("HumanoidRootPart")
    if not root then return end

    if not holding and root.AssemblyLinearVelocity.Y > 0 then
        local v = root.AssemblyLinearVelocity
        root.AssemblyLinearVelocity = Vector3.new(v.X, v.Y * CUT_MULTIPLIER, v.Z)
        holding = true -- only cut once per jump
    end
end)

Tune CUT_MULTIPLIER by feel. 0.4 gives a noticeable but not jarring difference. Below about 0.2 the tap-jump feels broken.

JumpRequest fires repeatedly while the button is held on some platforms, so if you're doing anything expensive in there, debounce it.

Coyote time

Named after the cartoon coyote who hangs in the air before falling. If a player presses jump a few frames after walking off a ledge, let them jump anyway. Nobody has ever complained about this and everybody notices when it's missing.

local COYOTE_WINDOW = 0.12 -- seconds

local lastGrounded = 0

humanoid.StateChanged:Connect(function(_, new)
    if new == Enum.HumanoidStateType.Freefall then
        lastGrounded = os.clock()
    end
end)

local function canJump()
    local state = humanoid:GetState()
    if state == Enum.HumanoidStateType.Running then
        return true
    end
    if state == Enum.HumanoidStateType.Freefall then
        return (os.clock() - lastGrounded) <= COYOTE_WINDOW
    end
    return false
end

Keep the window short. 0.1 to 0.15 seconds reads as forgiveness. Push it past 0.25 and players start noticing they're jumping off thin air, which breaks the illusion in the other direction.

Jump buffering

The mirror image of coyote time. Player presses jump slightly before landing, so instead of eating the input, remember it and fire the jump the moment they touch down.

local BUFFER_WINDOW = 0.15
local bufferedAt = 0

UIS.JumpRequest:Connect(function()
    bufferedAt = os.clock()
end)

humanoid.StateChanged:Connect(function(_, new)
    if new == Enum.HumanoidStateType.Landed then
        if os.clock() - bufferedAt <= BUFFER_WINDOW then
            bufferedAt = 0
            humanoid.Jump = true
        end
    end
end)

Coyote time and buffering together do more for game feel than any amount of tweaking JumpHeight. They cost almost nothing and players will just