Ledge grabbing script not properly working

What do you want to achieve?
I want script, that i found in open source form and tried fixing it, to work like that:
when you come close to a wall (1.5 stud maximum between center of head and wall raycast), player’s head must be near ledge, minimum, is when head 1.5 stud lower than top of wall which player faces, while maximum is head higher than top of wall by 0.75 stud. Player must be facing wall and press C to hang on wall and then press space whenever player wants to climb up, but previous conditions of distance and height must be followed to hang on wall, otherwise player will just fall down.

  1. What is the issue? Include screenshots / videos if possible!
    I got issue with detecting and firing hang script, without following conditions. It appears after correct work with first try of hanging on wall successfully, after u climbed wall after hanging on it for first time in testing session, script then refuses to depend on conditions of height and does let you hang on wall, no matter how high the part is.
    Also i cannot set the distance between wall and player needed to start hanging on wall, and instead it just works when player goes so close to wall so theres 0.1 stud gap.

  2. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I tried searching many topics with similar issues, topics about ray casting and functions of it, tried many times to find way to fix it by myself but didnt really get a fix.

local Character = plr.Character or plr.CharacterAdded:Wait()
local Root = Character:WaitForChild("HumanoidRootPart")
local Head = Character:WaitForChild("Head")
local Torso = Character:WaitForChild("Torso")
local Hum = Character:WaitForChild("Humanoid")
local CA = Hum:LoadAnimation(script:WaitForChild("ClimbAnim"))
local HA = Hum:LoadAnimation(script:WaitForChild("HoldAnim"))
local TouchGui = plr:WaitForChild("PlayerGui"):FindFirstChild("TouchGui")
local UIS = game:GetService("UserInputService")
ledgeavailable = true
holding = false
local canVault = false

while game:GetService("RunService").Heartbeat:Wait() do
	local r = Ray.new(Head.CFrame.p, Head.CFrame.LookVector)
	local part,position = workspace:FindPartOnRay(r,Character)
	local raycastResult = workspace:Raycast(Head.CFrame.p, Head.CFrame.LookVector)
	
	if ledgeavailable == true then
		print("can grab")
	else
		print("cannot grab")
	end
	if part and ledgeavailable and not holding then
		if part.Size.Y >= 7 then
			if raycastResult.Distance <= 2 then
				canVault = true
			end
			if Head.Position.Y >= part.Position.Y + ((part.Size.Y / 2) - 1) and Head.Position.Y <= part.Position.Y + (part.Size.Y / 2) and  Root.Velocity.Y <= 0 and canVault then
				if raycastResult then
					if raycastResult.Distance <= 1 then
				local key = game:GetService("UserInputService")
				key.InputEnded:Connect(function(input)	
					if input.KeyCode == Enum.KeyCode.C and ledgeavailable and not holding and canVault then
						Root.Anchored = true holding = true HA:Play() ledgeavailable = false
						end
						end)
					end
				end
			
				end
		end
		end
	
	
	
	function climb()
		local Vele = Instance.new("BodyVelocity",Root)
		Root.Anchored = false
		Vele.MaxForce = Vector3.new(1,1,1) * math.huge
		Vele.Velocity = Root.CFrame.LookVector * 10 + Vector3.new(0,30,0)
		HA:Stop() CA:Play()
		game.Debris:AddItem(Vele,.15)
		holding = false
		wait(.75)
		ledgeavailable = true
		canVault = false
	end
	
	
	
	UIS.InputBegan:Connect(function(Key,Chat)
		if not holding then return end 
		if Key.KeyCode == Enum.KeyCode.Space and not Chat then
			climb()
		end
		end)
	
	
	
		if TouchGui then
			TouchGui:WaitForChild("TouchControlFrame"):WaitForChild("JumpButton").MouseButton1Click:Connect(function()
				if not holding then return end climb()
		end)
	end
	end

Edit: Forgot to mention, script is placed in StarterCharacterScripts, HA and CA stand for animations placed into script.
изображение

1 Like

Hi, I’ve reworked the script. :smile:


Here is the grabbable asset:

Since you said there were no posts similar to your issue, I made the asset open-source so that others with the same problem - later on - can find this solution.


Here is the source code (+ Comments):

--[[
	Ledge Climb Script!
	
	Revitalized form of:
		https://devforum.roblox.com/t/ledge-grabbing-script-not-properly-working/2394257
]]

--------------------------------------

--// SERVICES //--
local CAS = game:GetService("ContextActionService")
local UIS = game:GetService("UserInputService")
local BIN = game:GetService("Debris")
--
local RS = game:GetService("RunService")

--// CLIENT //--
local plr = game.Players.LocalPlayer
local touchGUI = plr:WaitForChild("PlayerGui"):FindFirstChild("TouchGui")

--// CHARACTER //--
local char = plr.Character or plr.CharacterAdded:Wait()
--
local hum : Humanoid = char:WaitForChild("Humanoid")
local animator = hum:WaitForChild("Animator")
--
local hrp = char:WaitForChild("HumanoidRootPart")
local head = char.Head

--// ANIMATIONS //--
local climbAnim = animator:LoadAnimation(script:WaitForChild("ClimbAnim"))
local holdAnim = animator:LoadAnimation(script:WaitForChild("HoldAnim"))
--// Use Animator instead of Humanoid to load animations:
--// Animator automatically streams animations server-wide.

--// CONNECTIONS //--
local heartbeatConnection

--// VALUES //--
local maxHoldDistance = 2 --// Max distance between player and wall
local minGroundDistance = 7 --// Min distance between player and ground (-1 = disable)

--// BOOLEANS //--
local ledgeAvailable = true
local holding = false
--// canVault was redundant

--// ENUM-CONSTANTS //--
local climbKey = Enum.KeyCode.Space
local holdKey = Enum.KeyCode.C

--// RAYCAST RULES //--
local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances = {char}
rayParams.FilterType = Enum.RaycastFilterType.Exclude


--// A seperate function to apply body forces:
local function ApplyVelocity()
	hrp.Anchored = false

	local bv = Instance.new("BodyVelocity")
	bv.MaxForce = Vector3.new(1, 1, 1) * math.huge
	bv.Velocity = (hrp.CFrame.LookVector * 10) + Vector3.new(0, 30, 0)
	bv.Parent = hrp

	BIN:AddItem(bv, .15)
end

--// Attempt to climb ledge:
local function AttemptClimb()
	if (holding
		and not ledgeAvailable)
	then
		ApplyVelocity()

		holdAnim:Stop()
		climbAnim:Play()

		holding = false

		task.wait(.75)

		ledgeAvailable = true
	end
end

--// Attempt to hold ledge:
local function AttemptHold()
	if (ledgeAvailable
		and not holding)
	then
		hrp.Anchored = true
		holding = true

		holdAnim:Play()

		ledgeAvailable = false

		if heartbeatConnection then heartbeatConnection:Disconnect() end
	end
end

--// Verify if the part and character is in a climbable state:
--// All conditions MUST be 'true' for the function to RETURN 'true'.
local function IsValidClimb(wallResult, groundResult)
	return
		head.Position.Y >= wallResult.Instance.Position.Y + ((wallResult.Instance.Size.Y / 2) - 1)
		and head.Position.Y <= wallResult.Instance.Position.Y + (wallResult.Instance.Size.Y / 2)
		and ledgeAvailable
		and hrp.Velocity.Y <= 0
		and wallResult.Distance <= maxHoldDistance
		and not groundResult --// We want there to be no ground below!
		and not holding
end

--[[
	// Main Logic Explained //

	1. Each heartbeat we raycast forward
	to find a wall.
	
	2. If we find a wall, we
	raycast downward to find the ground.
	We don't want the player to hold while
	the ground is within minGroundDistance.
	
	We then compare the results with our
	conditions in IsValidClimb().
	
	3. If those checks pass we can say
	that the player is allowed to
	grab the ledge.
	
	4. So we then check if the player
	is currently holding 'holdKey'.
	
	5. If they are, AttemptHold.
]]

local function OnHeartbeat() --1
	local wallResult = workspace:Raycast(
		head.Position,
		head.CFrame.LookVector * maxHoldDistance,
		rayParams
	)

	if (wallResult) then --2
		local groundResult
		if (minGroundDistance ~= -1) then
			groundResult = workspace:Raycast(
				hrp.Position,
				-hrp.CFrame.UpVector * minGroundDistance,
				rayParams
			)
		end

		if IsValidClimb(wallResult, groundResult) then --3
			if UIS:IsKeyDown(holdKey) then --4
				AttemptHold() --5
			end
		end
	end
end

--[[
	// StateChanged Switch //

	This is a switch that enhances performance!
	
	Heartbeat is disconnected when the player is
	grounded and reconnected when they are in
	freefall.
	
	This way we're not shooting rays when we don't
	need to.
]]

hum.StateChanged:Connect(function(oldState, newState)
	if (newState == Enum.HumanoidStateType.Freefall) then
		heartbeatConnection = RS.Heartbeat:Connect(OnHeartbeat)
	else
		if heartbeatConnection then heartbeatConnection:Disconnect() end
	end
end)

--[[ 
	Combines keyboard and mobile climbs;
	Doesn't take climbKey into account.
	
	If you want climbKey on keyboard to be
	something other than Space,
	uncomment the functions below.
]]--
UIS.JumpRequest:Connect(function()
	AttemptClimb()
end)


--[[
--// Mobile climb (jump button) input:
touchGUI:WaitForChild("TouchControlFrame"):WaitForChild("JumpButton").MouseButton1Click:Connect(function()
	AttemptClimb()
end)

--// Keyboard climb (Spacebar) input:
CAS:BindAction("Climb", AttemptClimb, false, climbKey)
]]


Explanations / Notes:


I’ve tested the code and it works as intended (R6 fully works, R15 should work but wasn’t tested). I did not test it on mobile.

The following notes are ways the code was changed to be more readable / efficient. These are general fixes that you can choose whether or not to follow.

I’ve also provided links to staff posts that explain why certain services are better. They should be displayed as yellow text.

I tried to make the code as readable as possible. But if you’re confused by something feel free to ask!

Humanoid.Animator:LoadAnimation()

Use Humanoid.Animator to make animations played on a single client display for all players.

task.wait()

task.wait() is a better version of wait().

workspace:Raycast()

workspace:Raycast() is a better version of Ray.new().

UserInputService.JumpRequest

Using JumpRequest is faster and simpler than adding UIS inputs that mimic jump.

Humanoid.StateChanged & Heartbeat

We don’t want Heartbeat casting rays each frame, that would be inefficient. Instead, we can use Humanoid.StateChanged to toggle Heartbeat off when the player is grounded. We really only need to check if the player is able to hold a ledge when they’re airborne.

Nesting and ‘and’

Nesting conditional ‘if’ statements over and over decreases readability. Try to combine if statements using the ‘and’ operator. The IsValidClimb() function I provided is an example of ‘and’ stringing.


I hope this helps! :smile:

3 Likes

Thx! It works perfectly like i wanted.

1 Like

Does this work with custom rigs? I’ve tried to use it with mine but no luck. It’s an R15 rthro rig as the StarterCharacter.

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