No showcase footage yet
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