Climbing system debounce issue

You can write your topic however you want, but you need to answer these questions:

  1. I Have a climbing system but ive encountered an issue

  2. When i double tap the Spacebar instead of holding it, the character seems to drift away

  3. I tried setting up debounces but it didnt seem to work, probably a skill issue from me

here’s my full code if needed

-- // Services \\ --
local UIS = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

-- // Local Player & Character Variables \\ --
local localPlr = Players.LocalPlayer
local character = localPlr.Character or localPlr.CharacterAdded:Wait()
local HRP = character:WaitForChild("HumanoidRootPart")
local Hum = character:WaitForChild("Humanoid")
local head = character:WaitForChild("Head")

local ClimbAnimation = Instance.new("Animation")
ClimbAnimation.AnimationId = "rbxassetid://115706581497213"
local Climb = Hum.Animator:LoadAnimation(ClimbAnimation)
Climb.Priority = Enum.AnimationPriority.Action2

local GrabAnimation = Instance.new("Animation")
GrabAnimation.AnimationId = "rbxassetid://122583292148993"
local Grab = Hum.Animator:LoadAnimation(GrabAnimation)
Grab.Priority = Enum.AnimationPriority.Action2


-- Climbing Constants
local MaxUpwardsVel = 30
local ClimbDuration = 1 -- Time to decrease upward velocity to 0
local MaxDistance = 10
local CD = 0.5
-- Climbing State variables
local isClimbing = false
local climbStartTime = 0
local Vel = nil
local UpwardsVel = MaxUpwardsVel
local climbingSurface = nil
local canClimbAgain = true

local isLadder = false

-----------Debug-------------------

local spaceHeld = false
local spacePressedThisFrame = false
local spaceConsumed = false
local lastClimbStart = 0
local CLIMB_INPUT_COOLDOWN = 0.15

-- Climbing logic



local function stopClimbing()
	if isClimbing then
		isClimbing = false
		canClimbAgain = false
		isLadder = false
		
		if Climb.IsPlaying then
			Climb:Stop()
		end
		if Vel then
			Vel:Destroy()
			Vel = nil
		end
		climbingSurface = nil
	end
end



-- Ledge grabbing logic
local function LedgeGrab()
	if isClimbing then
		isClimbing = false
		canClimbAgain = false

		if Climb.IsPlaying then
			Climb:Stop()
		end
		if Vel then
			Vel:Destroy()
			Vel = nil
		end
		climbingSurface = nil
	end
	Grab:Play()
	local newVel = Instance.new("BodyVelocity")
	newVel.Name = "Hello"
	newVel.Parent = HRP
	newVel.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
	newVel.Velocity = Vector3.new(0, 35, 0) + HRP.CFrame.lookVector * 20
	task.wait(0.1)
	newVel:Destroy()
print("Works")
end

----------------------------------------
local function startClimbing(hitPart, hitPosition, hitNormal)
	if isClimbing or not canClimbAgain then return end

	isClimbing = true
	climbStartTime = tick()
	UpwardsVel = MaxUpwardsVel
	climbingSurface = hitPart


	isLadder = hitPart:GetAttribute("Ladder") == true

	-- Snap character to the climbing surface
	local offset = hitNormal * (character.HumanoidRootPart.Size.Z / 2 + 1.5)
	local newPosition = hitPosition + offset

	local Properties = { CFrame = CFrame.new(newPosition, newPosition - hitNormal) }
	local Info = TweenInfo.new(0.1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
	local T = game:GetService("TweenService"):Create(HRP, Info, Properties)
	T:Play()
	T.Completed:Wait()

	HRP.CFrame = CFrame.new(newPosition, newPosition - hitNormal)

	Vel = Instance.new("BodyVelocity")
	Vel.Parent = HRP
	Vel.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
	Vel.Velocity = Vector3.new(0, UpwardsVel, 0)

	Climb:Play()
end


--Update climb velocity 
local function updateIntermittentVelocity()
	if isClimbing and Vel then

		-- LADDER: no decay, no limit
		if isLadder then
			Vel.Velocity = Vector3.new(0, MaxUpwardsVel, 0)
			Climb:AdjustSpeed(1)
			return
		end

		-- NORMAL WALL: decay over time
		local elapsedTime = tick() - climbStartTime
		local progress = math.min(elapsedTime / ClimbDuration, 1)

		UpwardsVel = MaxUpwardsVel * (1 - progress)
		Vel.Velocity = Vector3.new(0, UpwardsVel, 0)
		Climb:AdjustSpeed(UpwardsVel / MaxUpwardsVel)

		if progress >= 1 then
			stopClimbing()
		end
	end
end

Hum.StateChanged:Connect(function(_, newState)
	if newState == Enum.HumanoidStateType.Landed then
		wait(CD)
		canClimbAgain = true
	end
end)

-- Check climbing conditions
local function canClimb()
	local BodyRayOrigin = HRP.Position + Vector3.new(0, 0, 0) -- Slightly below HumanoidRootPart for body detection.
	local HeadRayOrigin = head.Position + Vector3.new(0, 2.5, 0) -- Slightly above the head for ledge detection.
	local NeckRayOrigin = head.Position + Vector3.new(0,-1,0)
	local RayDirection = HRP.CFrame.LookVector * (MaxDistance)

	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {script.Parent}
	raycastParams.IgnoreWater = true

	local BodyRaycastResult = workspace:Raycast(BodyRayOrigin, RayDirection, raycastParams)
	local HeadRaycastResult = workspace:Raycast(HeadRayOrigin, RayDirection, raycastParams)
	local NeckRaycastResult = workspace:Raycast(NeckRayOrigin, RayDirection, raycastParams)

	if BodyRaycastResult and HeadRaycastResult and NeckRaycastResult then
		return true, false, BodyRaycastResult.Instance, BodyRaycastResult.Position, BodyRaycastResult.Normal
	elseif BodyRaycastResult and not HeadRaycastResult then
		return false, true, BodyRaycastResult.Instance, BodyRaycastResult.Position, BodyRaycastResult.Normal
	end
	return false, false
end

RunService:BindToRenderStep("Climbing", Enum.RenderPriority.Input.Value, function()
	local canClimbResult, canLedgeGrab, hitPart, hitPosition, hitNormal = canClimb()
	local spaceHeld = UIS:IsKeyDown(Enum.KeyCode.Space)

	-- Space released makes stop
	if not spaceHeld and isClimbing then
		stopClimbing()
		return
	end

	-- Start climbing
	if spaceHeld
		and canClimbResult
		and canClimbAgain
		and hitPart:IsA("Part")
		and hitPart:GetAttribute("CantBeClimbed") == nil
		and not isClimbing
	then
		startClimbing(hitPart, hitPosition, hitNormal)

		-- Ledge grab
	elseif spaceHeld
		and canLedgeGrab
		and not canClimbResult
		and isClimbing
	then
		LedgeGrab()
	end

	updateIntermittentVelocity()
end)

character.AncestryChanged:Connect(function(_, parent)
	if not parent then
		stopClimbing()
	end
end)

I will aslo provide a video, at first i simply held the spacebar, then i pressed the spacebar twice quickly

Please help i cant find a proper solution!!

1 Like

So i managed to make this that works way better:

-- // Services \\ --
local UIS = game:GetService("UserInputService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")

-- // Local Player & Character Variables \\ --
local localPlr = Players.LocalPlayer
local character = localPlr.Character or localPlr.CharacterAdded:Wait()
local HRP = character:WaitForChild("HumanoidRootPart")
local Hum = character:WaitForChild("Humanoid")
local head = character:WaitForChild("Head")

local ClimbAnimation = Instance.new("Animation")
ClimbAnimation.AnimationId = "rbxassetid://115706581497213"
local Climb = Hum.Animator:LoadAnimation(ClimbAnimation)
Climb.Priority = Enum.AnimationPriority.Action2

local GrabAnimation = Instance.new("Animation")
GrabAnimation.AnimationId = "rbxassetid://122583292148993"
local Grab = Hum.Animator:LoadAnimation(GrabAnimation)
Grab.Priority = Enum.AnimationPriority.Action2

-- Climbing Constants
local MaxUpwardsVel = 30
local ClimbDuration = 1 -- Time to decrease upward velocity to 0
local MaxDistance = 10
local CLIMB_COOLDOWN = 0.5

-- Climbing State variables
local isClimbing = false
local climbStartTime = 0
local Vel = nil
local UpwardsVel = MaxUpwardsVel
local climbingSurface = nil
local canClimbAgain = true
local hasClimbedThisAirtime = false -- Prevents multiple climbs per airtime

local isLadder = false

-- Climbing logic
local function stopClimbing()
	if isClimbing then
		isClimbing = false
		canClimbAgain = false
		isLadder = false

		if Climb.IsPlaying then
			Climb:Stop()
		end
		if Vel then
			Vel:Destroy()
			Vel = nil
		end

		climbingSurface = nil

		-- Re-enable jumping
		Hum:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
	end
end

-- Ledge grabbing logic
local function LedgeGrab()
	if hasClimbedThisAirtime then
		return -- Prevent ledge grab if already climbed
	end

	if isClimbing then
		isClimbing = false
		canClimbAgain = false

		if Climb.IsPlaying then
			Climb:Stop()
		end
		if Vel then
			Vel:Destroy()
			Vel = nil
		end

		climbingSurface = nil

		Hum:SetStateEnabled(Enum.HumanoidStateType.Jumping, true)
	end

	Grab:Play()
	local newVel = Instance.new("BodyVelocity")
	newVel.Name = "LedgeGrabVel"
	newVel.Parent = HRP
	newVel.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
	newVel.Velocity = Vector3.new(0, 35, 0) + HRP.CFrame.lookVector * 20
	task.wait(0.1)
	newVel:Destroy()
end

local function startClimbing(hitPart, hitPosition, hitNormal)
	-- check if already climbed this airtime
	if hasClimbedThisAirtime then 
		print("Blocked: Already climbed this airtime")
		return 
	end

	-- Check if already climbing or can't climb
	if isClimbing or not canClimbAgain then 
		print("Blocked: isClimbing or canClimbAgain false")
		return 
	end

	-- Check humanoid state - must NOT be in freefall/flying if we already climbed
	local state = Hum:GetState()
	if (state == Enum.HumanoidStateType.Freefall or state == Enum.HumanoidStateType.Flying) and hasClimbedThisAirtime then
		print("Blocked: In air after already climbing")
		return
	end

	print("Starting climb")
	isClimbing = true
	hasClimbedThisAirtime = true -- Mark that we've climbed during this airtime
	climbStartTime = tick()
	UpwardsVel = MaxUpwardsVel
	climbingSurface = hitPart

	isLadder = hitPart:GetAttribute("Ladder") == true

	-- Disable jumping while climbing
	Hum:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)

	local offset = hitNormal * (HRP.Size.Z / 2 + 1.5)
	local newPosition = hitPosition + offset

	local Properties = { CFrame = CFrame.new(newPosition, newPosition - hitNormal) }
	local Info = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	local T = TweenService:Create(HRP, Info, Properties)
	T:Play()
	T.Completed:Wait()

	HRP.CFrame = CFrame.new(newPosition, newPosition - hitNormal)

	-- Create BodyVelocity
	Vel = Instance.new("BodyVelocity")
	Vel.Name = "ClimbVelocity"
	Vel.Parent = HRP
	Vel.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
	Vel.Velocity = Vector3.new(0, UpwardsVel, 0)

	Climb:Play()
end

-- Update climb velocity 
local function updateClimbVelocity()
	if isClimbing and Vel then
		if isLadder then
			Vel.Velocity = Vector3.new(0, MaxUpwardsVel, 0)
			Climb:AdjustSpeed(1)
			return
		end

		local elapsedTime = tick() - climbStartTime
		local progress = math.min(elapsedTime / ClimbDuration, 1)

		UpwardsVel = MaxUpwardsVel * (1 - progress)
		Vel.Velocity = Vector3.new(0, UpwardsVel, 0)
		Climb:AdjustSpeed(math.max(UpwardsVel / MaxUpwardsVel, 0.1))

		if progress >= 1 then
			stopClimbing()
		end
	end

	if hasClimbedThisAirtime and not isClimbing then
		-- After climbing finished but before landing, prevent ANY velocity objects
		for _, obj in pairs(HRP:GetChildren()) do
			if obj:IsA("BodyVelocity") then
				print("Destroying unauthorized BodyVelocity after climb:", obj.Name)
				obj:Destroy()
			end
		end

		local currentVel = HRP.AssemblyLinearVelocity
		if currentVel.Y > 5 then
			print("Capping upward velocity from", currentVel.Y, "to 5")
			HRP.AssemblyLinearVelocity = Vector3.new(currentVel.X, 5, currentVel.Z)
		end
	end
end

Hum.StateChanged:Connect(function(oldState, newState)
	print("State changed from", oldState, "to", newState)

	if newState == Enum.HumanoidStateType.Landed then
		print("Landed - resetting climb flags after cooldown")
		task.wait(CLIMB_COOLDOWN)
		canClimbAgain = true
		hasClimbedThisAirtime = false -- Reset when landed
	elseif newState == Enum.HumanoidStateType.Freefall or newState == Enum.HumanoidStateType.Flying then
		
		if hasClimbedThisAirtime then
			print("In air after climbing - blocking further climbs")
			canClimbAgain = false
		end
	end
end)

local function canClimb()
	local BodyRayOrigin = HRP.Position
	local HeadRayOrigin = head.Position + Vector3.new(0, 2.5, 0)
	local NeckRayOrigin = head.Position + Vector3.new(0, -1, 0)
	local RayDirection = HRP.CFrame.LookVector * MaxDistance

	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {character}
	raycastParams.IgnoreWater = true

	local BodyRaycastResult = workspace:Raycast(BodyRayOrigin, RayDirection, raycastParams)
	local HeadRaycastResult = workspace:Raycast(HeadRayOrigin, RayDirection, raycastParams)
	local NeckRaycastResult = workspace:Raycast(NeckRayOrigin, RayDirection, raycastParams)

	if BodyRaycastResult and HeadRaycastResult and NeckRaycastResult then
		return true, false, BodyRaycastResult.Instance, BodyRaycastResult.Position, BodyRaycastResult.Normal
	elseif BodyRaycastResult and not HeadRaycastResult then
		return false, true, BodyRaycastResult.Instance, BodyRaycastResult.Position, BodyRaycastResult.Normal
	end
	return false, false
end

UIS.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end

	if input.KeyCode == Enum.KeyCode.Space then
		print("=== SPACE PRESSED ===")
		print("hasClimbedThisAirtime:", hasClimbedThisAirtime)
		print("canClimbAgain:", canClimbAgain)
		print("isClimbing:", isClimbing)
		print("Current state:", Hum:GetState())

		local canClimbResult, canLedgeGrab, hitPart, hitPosition, hitNormal = canClimb()

		print("canClimbResult:", canClimbResult)
		print("canLedgeGrab:", canLedgeGrab)

		if canClimbResult
			and canClimbAgain
			and hitPart
			and hitPart:IsA("Part")
			and hitPart:GetAttribute("CantBeClimbed") == nil
			and not isClimbing
			and not hasClimbedThisAirtime
		then
			startClimbing(hitPart, hitPosition, hitNormal)

			
		elseif canLedgeGrab and not canClimbResult and isClimbing then
			print("Attempting ledge grab")
			LedgeGrab()
		else
			print("All climb conditions failed")
		end
	end
end)

UIS.InputEnded:Connect(function(input, gameProcessed)
	if input.KeyCode == Enum.KeyCode.Space then
		if isClimbing then
			stopClimbing()
		end
	end
end)


RunService.Heartbeat:Connect(function()
	updateClimbVelocity()
end)

character.AncestryChanged:Connect(function(_, parent)
	if not parent then
		stopClimbing()
	end

end)

1 Like

I think I may have found the problem, but I not fully sure, but it’s still worth a try.
I think what is happening is first, startClimbing() gets called like normal, and runs. Then, startClimbing() gets stopped by this line: T.Completed:Wait(). After that, you let go of the spacebar, and stopClimbing() runs, even when T.Completed:Wait() is still waiting. Next, T.Completed:Wait() finishes and a body velocity is added, but since stopClimbing() already ran, isClimbing is false. This makes it so when you press spacebar again, another body velocity is added, which overrides the old one in the Vel variable, making it so the old body velocity never gets deleted, even when you pass the ledge.

I’m not sure if I explained that to well, but even if I didn’t just try this fix:

Replace T.Completed:Wait() in startClimbing() with this code:

local tweenCompleted = false
local completionListener = T.Completed:Once(function() tweenCompleted = true end)
while not tweenCompleted do -- Waits for tween to complete
	if not isClimbing then -- If climbing is stopped, disconnect the listener and return
		completionListener:Disconnect()
		return
	end
	task.wait()
end

(I didn’t test this so there might be typos or small mistakes)

I hope this helps!

1 Like

i think when the player is climbing and press jump agian “jumping” adds more BodyVelocity… not sure but when i tried it while it destroying BodyVelocity that gets added seperately it worked Just fine.

Hello, i tried replacing my script with this one, it seems to be more stable but when the body velocity is destroyed the player is like stuck to the ground. Plus idk how but ive made it broke again :sad_but_relieved_face:

i tried this script too like you said and thats actually fixing most of the issues! when i tried it it actually seemed to fix the bug, not entirely tho because in one of my test i managed to get the bug again but only once after 20 attempts lol, thank you!

If you still get the bug sometimes, try swapping the if statement and the task.wait(), I think that’s what is causing the problem, since if the climb is canceled during the last task.wait(), this bug can still occur. Swapping the statements though should fully eliminate the window for this bug to occur.

local tweenCompleted = false
local completionListener = T.Completed:Once(function() tweenCompleted = true end)
while not tweenCompleted do -- Waits for tween to complete
	task.wait()
	if not isClimbing then -- If climbing is stopped, disconnect the listener and return
		completionListener:Disconnect()
		return
	end
end
1 Like

But it didnt got stuck for me mabey i didnt test it a enough

tysm it actually eliminated the bug, you saved me some days

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.