Help with climbing system

Hello, this post is an addition to my previous post here.

I am attempting to create a ledge climbing system in my game. The player attaches to the ledge by jumping up, then can use a combination of keybinds to jump to other ledges. For example, if the player jumps onto a ledge, they can use “spacebar+a” to jump to the ledge on the left or use just “a” to move slightly over to the left. This kind of works, however I am having problems with the raycasting aspect. The raycast (or spherecast) is not picking up any nearby ledges despite there being many around.

If anyone would like to playtest this in studio, simply create a part named “Ledge” and anchor it at about character height and jump up to it. At the moment, I am sticking lots of small Ledge parts together to make bigger ledges (as moving side to side puts you onto the nearest ledge part that you are not already attached to) but if anyone has a better way to do this, let me know.

I would really appreciate it if you could take a look at my code below and suggest any relevant changes whether about my raycasting issue or anything else. Any and all help is appreciated!

Thank you

Code:

-- Wait for humanoidRootPart to exist
local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoidRootPart

repeat
	humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	wait(0.1)
until humanoidRootPart

print("HumanoidRootPart found")

local camera = game.Workspace.CurrentCamera

-- Ensure the camera is valid
if not camera then
	error("Camera not found")
	return
end

print("Camera found")

local isGrabbing = false
local ledgePart = nil
local isJumping = false
local canAttach = true  -- New variable to track if attaching is allowed

-- Create a folder for debug rays
local debugFolder = Instance.new("Folder")
debugFolder.Name = "Excluded"
debugFolder.Parent = game.Workspace

print("Debug folder created")

-- Function to create a debug ray
local function createDebugRay(origin, direction, length)
	local rayPart = Instance.new("Part")
	rayPart.Anchored = true
	rayPart.CanCollide = false
	rayPart.Size = Vector3.new(0.2, 0.2, length)
	rayPart.Position = origin + direction * (length / 2)
	rayPart.BrickColor = BrickColor.new("Bright red")
	rayPart.Transparency = 0.5
	rayPart.Parent = debugFolder
	return rayPart
end

-- Function to check for nearby ledges
local function findLedge(excludeTable)
	local characterPosition = character.Head.CFrame.Position
	local characterDirection = humanoidRootPart.CFrame.LookVector
	local rayLength = 5
	local rayHeightOffset = 5

	-- Create a debug ray for visualization
	--local debugRay = createDebugRay(characterPosition + Vector3.new(0, rayHeightOffset, 0), characterDirection, rayLength)

	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = {character, debugFolder}
	rayParams.FilterType = Enum.RaycastFilterType.Exclude

	local rRay = workspace:Raycast(characterPosition, characterDirection * rayLength, rayParams)

	-- Remove the debug ray after raycasting
	--debugRay:Destroy()
	if rRay then
		print("ray")
	end

	if rRay and rRay.Instance.Name == "Ledge" then
		print("Found Ledge: " .. rRay.Instance.Name)
		return rRay.Instance
	else
		return nil
	end
end

-- Function to attach the player to a ledge
local function attachToLedge(ledge, rayInfo)
	if not isGrabbing then
		isGrabbing = true
		ledgePart = ledge

		-- Play the grab animation (replace "AnimationId" with your animation ID)
		local humanoid = character:FindFirstChild("Humanoid")

		-- Calculate the position on the ledge where the player should grab
		local ledgeAttachPosition

		if rayInfo then
			ledgeAttachPosition = ledge.CFrame.Position + rayInfo.Normal
		else
			ledgeAttachPosition = CFrame.new(ledge.Position, character.Head.Position).Position
		end

		humanoid.AutoRotate = false

		local bodyPos = Instance.new("BodyPosition")
		bodyPos.MaxForce = Vector3.new(50000, 50000, 50000)
		bodyPos.Position = ledgeAttachPosition + Vector3.new(0, -2, 0)
		bodyPos.Name = "LedgeGrabBPos"
		bodyPos.Parent = character.HumanoidRootPart
	end
end

-- Function to release from the ledge
local function releaseFromLedge()
	if isGrabbing then
		isGrabbing = false

		-- Stop the grab animation (if playing)
		local humanoid = character:FindFirstChild("Humanoid")

		-- Remove the attachment and weld
		local ledgeAttachment = humanoidRootPart:FindFirstChild("Attachment")
		if ledgeAttachment then
			ledgeAttachment:Destroy()
		end

		if ledgePart then
			if character.HumanoidRootPart:FindFirstChild("LedgeGrabBPos") then
				character.HumanoidRootPart["LedgeGrabBPos"]:Destroy()
				humanoid.AutoRotate = true
			end
		end

		ledgePart = nil
	end
end

-- Function to attach or detach from the ledge when "E" is pressed
local function toggleLedgeGrab()
	if isGrabbing then
		releaseFromLedge()
	elseif canAttach then
		local nearbyLedge = findLedge()
		if nearbyLedge then
			attachToLedge(nearbyLedge)
		end
	end
end

-- Listen for "E" key press to toggle ledge grab
game:GetService("UserInputService").InputBegan:Connect(function(input, gameProcessed)
	if not gameProcessed and input.KeyCode == Enum.KeyCode.E then
		toggleLedgeGrab()
	end
end)

-- Function to jump to another ledge or move slightly
local function jumpOrMove(direction, excludeTable)
	print('event running')
	if isGrabbing then
		local rayParams = RaycastParams.new()
		local radius = 5
		rayParams.FilterType = Enum.RaycastFilterType.Exclude

		if excludeTable then
			rayParams.FilterDescendantsInstances = {character, debugFolder, excludeTable}
		else
			rayParams.FilterDescendantsInstances = {character, debugFolder}
		end

		local rRay = workspace:Spherecast((character.Head.CFrame.Position + Vector3.new(0, 1.5, 0)) + direction, radius, humanoidRootPart.CFrame.LookVector * 5, rayParams)

		if rRay and rRay.Instance.Name == "Ledge" then
			print("i did find a ledge!")
			releaseFromLedge()
			attachToLedge(rRay.Instance, rRay)
			print("woohoo")
		else
			local moveAmount = direction * 2

			local cFPos = humanoidRootPart.CFrame * CFrame.new(moveAmount)
			print('i didnt find a ledge :(')
			--if character.HumanoidRootPart:FindFirstChild("LedgeGrabBPos") then
			--	character.HumanoidRootPart.LedgeGrabBPos.Position = cFPos.Position
			--	character.HumanoidRootPart.LedgeGrabBPos.P = 5000
			--	releaseFromLedge()
			--end
		end
	end
end


-- Listen for input to grab, release, or perform actions while grabbing
game:GetService("UserInputService").InputBegan:Connect(function(input, gameProcessed)
	if not gameProcessed then
		if input.KeyCode == Enum.KeyCode.Space then
			--if isGrabbing then
			--	releaseFromLedge()
			if not isJumping then
				isJumping = true
				local humanoid = character:FindFirstChild("Humanoid")
				if humanoid then
					humanoid:Move(Vector3.new(0, 10, 0))
				end
			end
		elseif isGrabbing then
			print("p is grabbing")
			if input.KeyCode == Enum.KeyCode.A then
				jumpOrMove(Vector3.new(3, 0, 0))
			elseif input.KeyCode == Enum.KeyCode.D then
				jumpOrMove(Vector3.new(-3, 0, 0))
			elseif input.KeyCode == Enum.KeyCode.W then
				jumpOrMove(Vector3.new(0, 0, -3))
			elseif input.KeyCode == Enum.KeyCode.S then
				local ledgeBelow = findLedge()
				if not ledgeBelow then
					releaseFromLedge()
					jumpOrMove(Vector3.new(0, -3, 0))
				end
			end
		end
	end
end)

-- Heartbeat for ledge detection
local function onHeartbeat()
	if not isGrabbing then
		local nearbyLedge = findLedge()
		if nearbyLedge then
			attachToLedge(nearbyLedge)
		end
	end
end

game:GetService("RunService").Heartbeat:Connect(onHeartbeat)