New Bunny Hoppy Movement Code

Hello, this is an update to my pre-existing topic about bunny hopping in Roblox. You can find that code here: Quake Engine Movement [Open Source]

Now I am choosing to update my old code as it could be improved quite the amount.

One of the major improvements with my new code is that it uses the humanoid:Move() function instead of a body velocity. This allows the player to be able to climb and be less buggy.

I also am using a part for checking if the player is grounded instead of a raycast so there is no glitch where the player is standing on an edge of a part but the code still thinks the player is in the air.

Enough talk, here is the code!

local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")

local player = game:GetService("Players").LocalPlayer
local camera = workspace.CurrentCamera

local character
local humanoidRootPart
local humanoid
local groundCheck

player.CharacterAdded:Connect(function(newCharacter)

	character = newCharacter

	humanoidRootPart = character:WaitForChild("HumanoidRootPart")
	humanoid = character:WaitForChild("Humanoid")

	groundCheck = script.GroundCheck:Clone()
	groundCheck.Weld.Part0 = humanoidRootPart
	groundCheck.Parent = character

end)


local Cmd = {

	upMove = false;
	downMove = false;

	leftMove = false;
	rightMove = false;

	lastUp = false;
	lastLeft = false;

	jump = false;
	justPressed = false;

	crouch = false;
	cjustPressed = false;

}

local function onUp(actionName, inputState)

	if inputState == Enum.UserInputState.Begin then

		Cmd.upMove = true
		Cmd.lastUp = true

	elseif inputState == Enum.UserInputState.End then

		Cmd.upMove = false

	end

end

local function onLeft(actionName, inputState)

	if inputState == Enum.UserInputState.Begin then

		Cmd.leftMove = true
		Cmd.lastLeft = true

	elseif inputState == Enum.UserInputState.End then

		Cmd.leftMove = false

	end

end

local function onDown(actionName, inputState)

	if inputState == Enum.UserInputState.Begin then

		Cmd.downMove = true
		Cmd.lastUp = false

	elseif inputState == Enum.UserInputState.End then

		Cmd.downMove = false

	end

end

local function onRight(actionName, inputState)

	if inputState == Enum.UserInputState.Begin then

		Cmd.rightMove = true
		Cmd.lastLeft = false

	elseif inputState == Enum.UserInputState.End then

		Cmd.rightMove = false

	end

end

local function onJump(actionName, inputState)

	if inputState == Enum.UserInputState.Begin then

		Cmd.jump = true
		Cmd.justPressed = true

	elseif inputState == Enum.UserInputState.End then

		Cmd.jump = false

	end

end

local function onCrouch(actionName, inputState)

	if inputState == Enum.UserInputState.Begin then

		Cmd.crouch = true
		Cmd.cjustPressed = true

	elseif inputState == Enum.UserInputState.End then

		Cmd.crouch = false

	end

end

ContextActionService:BindAction("Up", onUp, false, "w")
ContextActionService:BindAction("Left", onLeft, false, "a")
ContextActionService:BindAction("Down", onDown, false, "s")
ContextActionService:BindAction("Right", onRight, false, "d")
ContextActionService:BindAction("Jump", onJump, false, Enum.KeyCode.Space)
ContextActionService:BindAction("Crouch", onCrouch, false, "c")


local moveSpeed = 30
local runAccel = 5
local runDeaccel = 5
local airAccel = 2.5
local airDeaccel = 2.5
local sideStrafeAccel = 100
local sideStrafeSpeed = 1
local friction = 8
local airFriction = 3
local maxSpeed = 30

local playerVel = Vector3.new(0, 0, 0)
local forwardMove = 0
local sideMove = 0

local grounded = false

local wishJump = false
local canJump = true
local jumpBuffer = 0.1
local holdJumpToBhop = true


local function Update(deltaTime)

    if not humanoidRootPart then return

	if Cmd.leftMove and Cmd.rightMove then

		if Cmd.lastLeft then sideMove = -1 else sideMove = 1 end

	else

		if Cmd.leftMove then sideMove = -1 elseif Cmd.rightMove then sideMove = 1 else sideMove = 0 end

	end

	if Cmd.upMove and Cmd.downMove then

		if Cmd.lastUp then forwardMove = -1 else forwardMove = 1 end

	else

		if Cmd.upMove then forwardMove = -1 elseif Cmd.downMove then forwardMove = 1 else forwardMove = 0 end

	end

	QueueJump()
	grounded = CheckGround()

	if grounded then

		GroundMove()

	else

		AirMove()

	end

	local newDir = Vector3.new()
	if playerVel ~= Vector3.new(0, 0, 0) then newDir = playerVel.Unit end

	local newSpeed = playerVel.Magnitude
	newSpeed = math.clamp(newSpeed, 0, maxSpeed)

	humanoid.WalkSpeed = newSpeed
	humanoid:Move(newDir, false)

end

RunService.RenderStepped:Connect(Update)


function QueueJump()

	if holdJumpToBhop then

		wishJump = Cmd.jump
		return

	end

	if Cmd.jump and not wishJump and Cmd.justPressed then

		wishJump = true
		Cmd.justPressed = false

	end

	if not Cmd.jump then

		wishJump = false

	end

end


function CheckGround()

	local connection = groundCheck.Touched:Connect(function()end)

	for index, value in ipairs(groundCheck:GetTouchingParts()) do

		if value:IsA("BasePart") and value.Parent ~= character and value.CanCollide then

			connection:Disconnect()
			return true

		end

	end

	if humanoid:GetState() == Enum.HumanoidStateType.Climbing then

		return true

	end

	connection:Disconnect()
	return false

end


function AirMove()

	if forwardMove ~= 0 then

		ApplyFriction(1, true)

	end

	local accel
	local cameraCFrame = camera.CFrame
	local inputVel = Vector3.new(sideMove, 0, forwardMove)

	local wishDir = cameraCFrame:VectorToWorldSpace(inputVel)
	wishDir = Vector3.new(wishDir.X, 0, wishDir.Z)
	local wishSpeed = wishDir.Magnitude

	wishSpeed *= moveSpeed

	if wishDir ~= Vector3.new(0, 0, 0) then

		wishDir = wishDir.Unit

	end

	if playerVel:Dot(wishDir) < 0 then

		accel = airDeaccel

	else

		accel = airAccel

	end

	if forwardMove == 0 and sideMove ~= 0 then

		if wishSpeed > sideStrafeSpeed then

			wishSpeed = sideStrafeSpeed

		end

		accel = sideStrafeAccel

	end

	Accelerate(wishDir, wishSpeed, accel)

end


function GroundMove()

	if not wishJump and canJump then

		ApplyFriction(1, false)

	else

		ApplyFriction(0, false)

	end

	local cameraCFrame = camera.CFrame
	local inputVel = Vector3.new(sideMove, 0, forwardMove)

	local wishDir = cameraCFrame:VectorToWorldSpace(inputVel)
	wishDir = Vector3.new(wishDir.X, 0, wishDir.Z)

	if wishDir ~= Vector3.new(0, 0, 0) then

		wishDir = wishDir.Unit

	end

	local wishSpeed = wishDir.Magnitude

	wishSpeed *= moveSpeed

	Accelerate(wishDir, wishSpeed, runAccel)

	playerVel = Vector3.new(playerVel.X, 0, playerVel.Z)

	if wishJump and canJump then

		humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
		wishJump = false
		canJump = false

		local co = coroutine.create(function()

			local begin = tick()

			while tick() - begin < jumpBuffer do

				RunService.Heartbeat:Wait()

			end

			canJump = true

		end)

		coroutine.resume(co)

	end

end


function ApplyFriction(t, inAir)

	local vec = Vector3.new(playerVel.X, 0, playerVel.Z)
	local speed = vec.Magnitude
	local drop = 0
	local control = 0
	local newFriction = inAir and airFriction or friction

	control = speed < runDeaccel and runDeaccel or speed
	drop = control * newFriction * RunService.Heartbeat:Wait() * t


	local newspeed = speed - drop

	if newspeed < 0 then

		newspeed = 0

	end

	if speed > 0 then

		newspeed /= speed

	end

	local x = playerVel.X * newspeed
	local z = playerVel.Z * newspeed
	playerVel = Vector3.new(x, 0, z)

end


function Accelerate(wishDir, wishSpeed, accel)

	local currentspeed = playerVel:Dot(wishDir)
	local addSpeed = wishSpeed - currentspeed

	if addSpeed <= 0 then return end

	local accelSpeed = accel * RunService.Heartbeat:Wait() * wishSpeed

	if accelSpeed > addSpeed then

		accelSpeed = addSpeed

	end

	local x = playerVel.X + accelSpeed * wishDir.X
	local z = playerVel.Z + accelSpeed * wishDir.Z
	playerVel = Vector3.new(x, 0, z)

end

Thats just the code, but it uses a pre-existing part, so here is a model that you can use, just put it in the StarterPlayerScripts.

https://www.roblox.com/library/6803581351/Bunny-Hopping-Movement

If you would like to see an this in action, check out my game World War Bean!: World War Bean - Roblox

19 Likes

I don’t understand, since you’re using the default humanoid why don’t you detect landing using the, yk state named landed, you can read about it in the developer hub, or you can you the floor material property, as it returns the “air” if the humanoid is mid air

2 Likes

Real good, reminds me good old CS:GO days will use it for fun, but I would like to see an explanition of code or just explanition on math. Thanks as always!

2 Likes

I have read articles saying it is unreliable, I have also tried using it and it did not give me what I wanted. One of the main problems is that it only detects the player is grounded once the player is actually touching the floor, this leads to an effect where it feels like you are dragging you feet on the ground between each jump. With having a hitbox that has a bit of height, you actually end of jumping before you hit the ground and it feels a bit better.

I am not very good at explaining, but this resource helped me understand the math: Bunnyhopping from the Programmer’s Perspective (adrianb.io)

1 Like

If you want reliability you should use Region3 instead. The code will be shorter, more performant, and more reliable. You could use workspace:IsRegion3Empty to check if the character is grounded.
Overall, this is great though.

1 Like

help

1 Like

Did u use the model or just copy and paste the script?

i am using the model and i have tried in starter character and sarterplayer

Hey I just updated the model. I found the bug, it was one line that I accidentaly removed earlier that would make the code run when the character doesn’t existed, it should be fixed now :slight_smile:

Hey, I love this code and ive been trying to make a similar system for my FPS game. However i noticed that it doesnt work with mobile or controller and i was wondering how to make it work with mobile since i think i know how to make it work on controller.

Sorry I have not and do not plan to add mobile/console support because the game I am working on will be PC only. If you do end up making it I would love to see it!

Ive said this before but i really like this controller. theres this infinte yeild warning just wanna make sure like its not a gamebreaking thing

image

That is something that happens to me as well. I think it it because the code replaces the default ControlScript which does not have a MasterControl. The CameraScript tries to access this in the VRCamera script, but can’t because its not there. So this only effects VR, which is not supported anyways. So you should be all good!

If you want to get rid of this warning (even though it won’t effect anything), you would have to make your own camera script.