Help with ledge grabbing

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

  1. What do you want to achieve? Keep it simple and clear!
    In my game one of the main mechanics are chases and im trying to add ledge grabbing to add diversity to it.
  2. What is the issue? Include screenshots / videos if possible!
    I’ve made a simple raycast check for the rootpart to check for a part, and one above the torso to check if theres nothing there, and it * mostly * works until i have to actually put the player into place, where it kind of works but only fits well with parts relativelly the size of the character and face the ledge.
  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I tried looking at some scripts in the forum but can’t figure out most things, and cant find any tutorials regarding this, only uncopylocked models ( which is not really what im looking for )
    After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

function detectledge()
	if canVault == true then
		local lCheck = workspace:Raycast(rootPart.CFrame.Position, rootPart.CFrame.LookVector * 5, raycastParams)
		if lCheck then
			if lCheck.Instance and not lCheck.Instance.Parent:FindFirstChild("Humanoid") then
				local ledgeposition = lCheck.Instance.Position
				local headray = workspace:Raycast(rootPart.CFrame.Position + Vector3.new(0, 6, 0), rootPart.CFrame.LookVector * 5, raycastParams)
		
				if headray == nil or not headray then
					local magnitude = (ledgeposition - rootPart.Position).Magnitude
					if magnitude < 10 then
						canVault = false
						ledgePart = Instance.new("Part")
						ledgePart.Position = lCheck.Position + Vector3.new(0,-2,0)
						ledgeconnect = runservice.RenderStepped:Connect(function()
							-- grabbing
							rootPart.Anchored = true
							rootPart.CFrame = ledgePart.CFrame
						end)
					end
				end
			end			
		end
	elseif canVault == false then
		-- jumping off ledge
		humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
	    rootPart.Anchored = false
		if ledgeconnect then
			ledgeconnect:Disconnect()
		end

		if ledgePart then
			ledgePart:Destroy()
		end
		canVault = true
	end
end
1 Like

whenever I made my system, I didn’t want any over complicated math systems, so instead I used raycasts. I sent one out to find a wall, iterated them upwards until it found the top of the wall then raycasted a little upwards pushed it forward a bit and then pointed it downward that way I got the position on the x, z and y. sorry if that was confusing I’m not very good at explaining things, So instead here’s an example.

 -- // CLIENT \\ --

task.wait(.5)

local run = game:GetService("RunService")
local uis = game:GetService("UserInputService")

local plr = game.Players.LocalPlayer
local char = plr.Character or plr.CharacterAdded:Wait()

local hum = char:WaitForChild("Humanoid")
local hrp = char:WaitForChild("HumanoidRootPart")

local ledging = false
local ledgingSpeed = 2

local ledgeBodyVelocity = nil
local maxYIterations = 12
local yIterateAmount = 0.04

local lerpSpeed = 0.2

local maxPositionDifference = 6

local forwardRaycastLength = 2
local downwardRaycastLength = 0.7

local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = {char, workspace.t} -- workspace.t can be removed I ONLY HAVE IT FOR THE VISUALIZATION CODE

local function changeCollisionGroup(newGroup) -- isnt needed I just have it
	script.changeCollisionGroup:FireServer(newGroup)
end

local function ledge(doLedge)
	if doLedge then
		local forwardWallCheck = workspace:Raycast(hrp.Position, hrp.CFrame.LookVector * forwardRaycastLength, rayParams)

		if forwardWallCheck then
			local yHeight = forwardWallCheck.Position.Y

			local normCFrame = CFrame.new(forwardWallCheck.Position) * CFrame.new(Vector3.new(), Vector3.new() -forwardWallCheck.Normal)

			for i = 0, maxYIterations, yIterateAmount do
				local newCFrame = normCFrame * CFrame.new(0,0,0.2)
				
				local yHeightCheck = workspace:Raycast(newCFrame.Position + Vector3.new(0, i, 0), newCFrame.LookVector * forwardRaycastLength, rayParams)
				
				--[[
				local part = Instance.new("Part")
				part.Parent = workspace.t
				part.Anchored = true
				part.Size = Vector3.new(0.5,0.5,0.5)
				part.CanCollide = false
				part.Transparency = 0.6
				
				part.CFrame = newCFrame
				
				game.Debris:AddItem(part, 2)
				]]--
				
				-- IS A VISUALIZER
				
				if yHeightCheck then
					if yHeight ~= yHeightCheck.Position.Y then
						yHeight = yHeightCheck.Position.Y	
					end
				elseif not yHeightCheck then
					break
				end
			end
			
			local newCFrame = normCFrame * CFrame.new(0, 0.3,-0.3)
			local findYHeightRaycast = workspace:Raycast(newCFrame.Position, -hrp.CFrame.UpVector * downwardRaycastLength, rayParams)

			if findYHeightRaycast then
				yHeight = findYHeightRaycast.Position.Y
			end
			
			local positionDifference = (yHeight - forwardWallCheck.Position.Y)
			
			if positionDifference <= maxPositionDifference then
				if hum.AutoRotate then
					hum.AutoRotate = false
				end

				if not ledgeBodyVelocity then
					ledgeBodyVelocity = Instance.new("BodyVelocity")
					ledgeBodyVelocity.Parent = hrp
					ledgeBodyVelocity.MaxForce = Vector3.new(1e9, 1e9, 1e9)
					ledgeBodyVelocity.Velocity = Vector3.new(0, 0, 0)
				end

				local lockCFrame = CFrame.new(forwardWallCheck.Position.X, yHeight, forwardWallCheck.Position.Z) * CFrame.new(Vector3.new(), Vector3.new() -forwardWallCheck.Normal) * CFrame.new(0, -hrp.Size.Y/2, hrp.Size.Z/2)

				if hrp ~= lockCFrame then
					hrp.CFrame = hrp.CFrame:Lerp(lockCFrame, lerpSpeed)
				end
			else
				ledging = false	
			end
		else
			ledging = false	
		end
	elseif not doLedge then
		if not hum.AutoRotate then
			hum.AutoRotate = true
		end
		
		if ledgeBodyVelocity then
			ledgeBodyVelocity:Destroy()
			
			ledgeBodyVelocity = nil
		end
	end
end


uis.InputBegan:Connect(function(key, proc)
	if not proc then
		if key.KeyCode == Enum.KeyCode.Space then
			ledging = true
		end
		
		if key.KeyCode == Enum.KeyCode.S and ledging then
			ledging = false
		end
		
		changeCollisionGroup((ledging and "NoCollision") or "Default")
	end
end)


run.RenderStepped:Connect(function(dt)
	ledge(ledging)
end)

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

-- SERVER IS NOT NECESSARY I JUST HAVE IT FOR COLLISION GROUPS

-- // SERVER \\ --

script.Parent.changeCollisionGroup.OnServerEvent:Connect(function(plr:Player, newGroup)
	if not newGroup then return end
	
	for i,v in pairs(plr.Character:GetDescendants()) do
		if v:IsA("BasePart") then
			v.CollisionGroup = newGroup
		end
	end
end)

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

This is a fully working system I thought it would probably be better to just restart because your code looks a lil weird ngl. this is just something I came up with quickly and should really just be used as a base/reference. also server part isn’t necessary I just added it for collision groups, It can be ignored.

2 Likes

Thank you! The Yheight check method is very clever and i would have never tought about that, i remade my script and added a few other things ( such as a ceiling check ) and made it so it only happens when the player jumps, and managed to put it on a few enemy NPCS to enhance chases.

1 Like

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