Making a "Wall Climbing" System

would you mind if you explain how this works? I don’t understand how most things work on this system.

1 Like

Well in honesty I don’t use this, I’ve done a different approach for my wall crawling where player wont be able to move from one face of a part to another.
But from how I know this works is that initially the player is mounted onto the wall using BodyVelocity and BodyGyro, this allows the player to face the part and the velocity allows the movement as you see in the script. Now how to move from one face of a part to another is actually simple… we cast a ray from inside the part by offseting the raycast origin by (0,0,-1) from rootpart, which is also shown in the script, using rightVector we can get the direction and cast a ray from within the part. What’s cool is raycasts don’t detect by part, but the surface they hit. So we can get the hit.Normal and hit.Position to turn the body using BodyGyro towards that position. The bug you are having is because of physics in general, where your script doesn’t take into consideration your velocity to turn you at the right time around the corner. I would tamper with the raycast origin, adujusting it in relation to your velocity.

Hope that gave some insight. (I’m making a rogue-like too :smiley: )

2 Likes

Uhm, I have 0 idea how raycasting works so all i have right now is this (yes, i used the open source):

--------------------------------------------------------------------------------
-------------------------------------------------------------- Service Shortcuts
--------------------------------------------------------------------------------

local cas							= game:GetService("ContextActionService")
local ppl							= game:GetService("Players")
local run							= game:GetService("RunService")
local uis							= game:GetService("UserInputService")

--------------------------------------------------------------------------------
--------------------------------------------------------------- Player Shortcuts
--------------------------------------------------------------------------------

local plr							= ppl.LocalPlayer
local char							= plr.Character or plr.CharacterAdded:Wait()
local hum							= char:WaitForChild("Humanoid")
local rootpart						= char:WaitForChild("HumanoidRootPart")

--------------------------------------------------------------------------------
---------------------------------------------------------- Global Raycast Params
--------------------------------------------------------------------------------

local params						= RaycastParams.new()
params.CollisionGroup				= ("Climbable")
params.FilterDescendantsInstances	= {char}
params.FilterType					= Enum.RaycastFilterType.Blacklist
params.IgnoreWater					= true

--------------------------------------------------------------------------------
----------------------------------------------------------- Climbing Body Movers
--------------------------------------------------------------------------------

local climbMove						= Instance.new("BodyVelocity")
climbMove.MaxForce					= Vector3.new(1,1,1)*math.huge
climbMove.P							= math.huge
climbMove.Velocity					= Vector3.new()
climbMove.Name						= ("Climb Speed")

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

local climbGyro						= Instance.new("BodyGyro")
climbGyro.CFrame					= CFrame.new()
climbGyro.D							= 100
climbGyro.MaxTorque					= Vector3.new(1,1,1)*math.huge
climbGyro.P							= 3000

--------------------------------------------------------------------------------
------------------------------------------------------------- Climbing Variables
--------------------------------------------------------------------------------

local climbing						= false
local face							= nil

--------------------------------------------------------------------------------
------------------------------------------------------------- Climbing Functions
--------------------------------------------------------------------------------

function down(KEY) return uis:IsKeyDown(Enum.KeyCode[KEY]) end

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

function offClimb()
	climbing=false
	climbGyro.Parent=nil
	climbMove.Parent=nil
	face=nil
end

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

function onClimb()
	local origin = rootpart.Position
	local direction = rootpart.CFrame.LookVector
	
	local result = workspace:Raycast(origin, direction, params)
	if result then
		climbing=true
		rootpart.CFrame = CFrame.new(rootpart.CFrame.p, Vector3.new(rootpart.Position.X - result.Normal.X, rootpart.Position.Y, rootpart.Position.Z - result.Normal.Z))
		face = CFrame.new(result.Position+result.Normal, result.Position)
		
		hum.AutoRotate=false
		hum.PlatformStand=true
		
		climbMove.Parent=rootpart
		climbGyro.Parent=rootpart
		
		repeat
			run.RenderStepped:Wait()
			climbGyro.CFrame=face or CFrame.new()
			
			--I'm bad with math so I just used this quick solution, sorry to dissapoint.
			if rootpart.Position.Y > result.Instance.Size.Y+3 then
				climbing=false
				offClimb()
			end
			
			--if statements make code look spaghetti, so I used the logical operator: OR
			local sideOrigin = rootpart.CFrame*CFrame.new(0, 0, -1).Position
			local sideDirection = rootpart.CFrame.RightVector*(down("D") and -2 or 2)

			local hit = workspace:Raycast(sideOrigin, sideDirection, params)
			if hit and (down("D") or down("A")) then
				face=CFrame.new(hit.Position+hit.Normal, hit.Position)
			end
		until (not climbing)

		hum.AutoRotate=true
		hum.PlatformStand=false
	end
end

--------------------------------------------------------------------------------
----------------------------------------------------------------- Movement Logic
--------------------------------------------------------------------------------

local actions	= {0;0;	0;0;}
local buttons	= {"A";"D";"S";"W"}
local lastTime
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

uis.InputBegan:Connect(function(obj)
	local input	= tostring(obj.KeyCode):split('.')[3]
	local key	= table.find(buttons, input)

	if key then
		actions[key]=1
	end
	
	
	--I was lazy to script double tap space
	if input == "Space" then
		local now = tick()
		local difference = (now - lastTime)

		if difference <= 0.2 then
			if (not climbing) then
				onClimb()
			else
				offClimb()
			end
			wait(2)
		end
		lastTime = tick()
	end
end)

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

uis.InputEnded:Connect(function(obj)
	local input	= tostring(obj.KeyCode):split('.')[3]
	local key	= table.find(buttons, input)

	if key then
		actions[key]=0
	end
end)

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

run.RenderStepped:Connect(function()
	local strafe = actions[2] - actions[1]
	local surge = actions[3] - actions[4]
	
	climbMove.Velocity=rootpart.CFrame.RightVector*(strafe*8)+Vector3.new(0, surge*-8, 0)
end)

--------------------------------------------------------------------------------
---------------------------------------- Hopefully that was a fun and clean read
--------------------- I know that this is glitchy but that's somewhat on purpose
--------------- This is meant to be a introduction to a climbing system, bt dubs
--------------------------------------------------------------------------------

the only thing i did so far was change L to climb to double tap spacebar

2 Likes

is there a way to check where the edge is, and where the part’s edge ends?

1 Like

Well, matter of fact I don’t think you need to find the edge what should be used in contrast with the current system is maybe AlignPosition | Roblox Creator Documentation, this should prevent the gap between the player and would be a much efficient fix as opposed to finding the sides of a part because you have to include the rotation of a part which is a bunch of math I don’t think worth diving into.

Compared to what @Blend_it has created to find the edge and have the player adjust to the next side I think this much more viable to create a revamp of the original.

All you would have to do is create a attachment for the rootpart and attachment for the raycast hit.Normal, Give these into a AlignPosition instance and I think it should work… I haven’t tested but the logic makes sense.

(Suggest you have a look into raycasting before trying to do this, every roblox dev must know how raycasting works)

2 Likes

Here I felt a little kind today, I quote on quote “assisted” the open source code so that the bug doesn’t occur…

--------------------------------------------------------------------------------
-------------------------------------------------------------- Service Shortcuts
--------------------------------------------------------------------------------

local cas							= game:GetService("ContextActionService")
local ppl							= game:GetService("Players")
local run							= game:GetService("RunService")
local uis							= game:GetService("UserInputService")

--------------------------------------------------------------------------------
--------------------------------------------------------------- Player Shortcuts
--------------------------------------------------------------------------------

local plr							= ppl.LocalPlayer
local char							= plr.Character or plr.CharacterAdded:Wait()
local hum							= char:WaitForChild("Humanoid")
local rootpart						= char:WaitForChild("HumanoidRootPart")

--------------------------------------------------------------------------------
---------------------------------------------------------- Global Raycast Params
--------------------------------------------------------------------------------

local params						= RaycastParams.new()
params.CollisionGroup				= ("Climbable")
params.FilterDescendantsInstances	= {char}
params.FilterType					= Enum.RaycastFilterType.Blacklist
params.IgnoreWater					= true

--------------------------------------------------------------------------------
----------------------------------------------------------- Climbing Body Movers
--------------------------------------------------------------------------------

local climbMove						= Instance.new("BodyVelocity")
climbMove.MaxForce					= Vector3.new(1,1,1)*math.huge
climbMove.P							= math.huge
climbMove.Velocity					= Vector3.new()
climbMove.Name						= ("Climb Speed")

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

local climbGyro						= Instance.new("BodyGyro")
climbGyro.CFrame					= CFrame.new()
climbGyro.D							= 100
climbGyro.MaxTorque					= Vector3.new(1,1,1)*math.huge
climbGyro.P							= 3000

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

local alignPos						= Instance.new("AlignPosition",rootpart)
local attachment0 					= Instance.new("Attachment",rootpart)
attachment0.CFrame					= CFrame.new(0,0,-2)
alignPos.Attachment0				= attachment0
alignPos.MaxForce					= Vector3.new(1,1,1)*math.huge
alignPos.MaxVelocity				= Vector3.new(1,1,1)*math.huge
alignPos.Responsiveness				= 20
alignPos.ApplyAtCenterOfMass		= true
alignPos.ReactionForceEnabled		= true
alignPos.RigidityEnabled			= true


--------------------------------------------------------------------------------
------------------------------------------------------------- Climbing Variables
--------------------------------------------------------------------------------

local climbing						= false
local face							= nil

--------------------------------------------------------------------------------
------------------------------------------------------------- Climbing Functions
--------------------------------------------------------------------------------

function down(KEY) return uis:IsKeyDown(Enum.KeyCode[KEY]) end

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

function offClimb()
	climbing=false
	climbGyro.Parent=nil
	climbMove.Parent=nil
	face=nil
end

function onClimb()
	local origin = rootpart.Position
	local direction = rootpart.CFrame.LookVector

	local result = workspace:Raycast(origin, direction, params)
	if result then
		climbing=true
		rootpart.CFrame = CFrame.new(rootpart.CFrame.p, Vector3.new(rootpart.Position.X - result.Normal.X, rootpart.Position.Y, rootpart.Position.Z - result.Normal.Z))
		face = CFrame.new(result.Position+result.Normal, result.Position)

		hum.AutoRotate=false
		hum.PlatformStand=true

		climbMove.Parent=rootpart
		climbGyro.Parent=rootpart

		repeat
			run.RenderStepped:Wait()
			climbGyro.CFrame=face or CFrame.new()

			if rootpart.Position.Y > result.Instance.Size.Y+3 then
				climbing=false
				offClimb()
			end

			local sideOrigin = rootpart.CFrame*CFrame.new(0, -0.4, -1).Position
			local sideDirection = rootpart.CFrame.RightVector*(down("D") and -2 or 2)

			local hit = workspace:Raycast(sideOrigin, sideDirection, params)
			if hit and (down("D") or down("A")) then
				local attachment1 = Instance.new("Attachment",hit.Instance)
				attachment1.WorldPosition = hit.Position
				alignPos.Attachment1 = attachment1
				face=CFrame.new(hit.Position+hit.Normal, hit.Position)
				game.Debris:AddItem(attachment1,0.005)
			end
		until (not climbing)

		hum.AutoRotate=true
		hum.PlatformStand=false
	end
end


local actions	= {0;0;	0;0;}
local buttons	= {"A";"D";"S";"W"}
local lastTime  = tick()


uis.InputBegan:Connect(function(obj)
	local input	= tostring(obj.KeyCode):split('.')[3]
	local key	= table.find(buttons, input)

	if key then
		actions[key]=1
	end
	
	if input == "Space" then
		local now = tick()
		local difference = (now - lastTime)

		if difference <= 0.2 then
			if (not climbing) then
				onClimb()
			else
				offClimb()
			end
			wait(2)
		end
		lastTime = tick()
	end
end)


uis.InputEnded:Connect(function(obj)
	local input	= tostring(obj.KeyCode):split('.')[3]
	local key	= table.find(buttons, input)

	if key then
		actions[key]=0
	end
end)


run.RenderStepped:Connect(function()
	local strafe = actions[2] - actions[1]
	local surge = actions[3] - actions[4]

	climbMove.Velocity=rootpart.CFrame.RightVector*(strafe*8)+Vector3.new(0, surge*-8, 0)
end)

Note I altered the one you gave so there shouldn’t be any changes to keybinds…
I didn’t include part to part, you would have to do that yourself, the approach I would take to do something like this would be to constantly cast a ray from the rootpart and cast rays depending on which key is being pressed ( A or D ) to cast a ray from either the RightArm/LeftArm in their respective rightVector directions. You would then move from there, understanding how this works and altering it to your desire.

4 Likes

would this be a valid way?

	local origin = rootpart.Position
	local direction = RightArm.CFrame.RightVector+RightArm.CFrame.LookVector

	local result = workspace:Raycast(origin, direction, params)
			local hit = workspace:Raycast(sideOrigin, sideDirection, params)
			if hit and (down("D") or down("A")) then -- right or left button is down
				local anotherDirection
				local leftOrRight = nil
				local anotherOrigin = rootpart.Position
				if down("D") then
					leftOrRight = 10
					anotherDirection = leftArm.CFrame.RightVector * -1 +leftArm.CFrame.LookVector
				elseif down("A") then
					leftOrRight = -10
					anotherDirection = rightArm.CFrame.RightVector+rightArm.CFrame.LookVector
				end
				local anotherClimbable = workspace:Raycast(anotherOrigin, anotherDirection, params)
				
				local attachment1 = Instance.new("Attachment",hit.Instance)
				attachment1.WorldPosition = hit.Position
				alignPos.Attachment1 = attachment1
				face=CFrame.new(hit.Position+hit.Normal, hit.Position)
				game.Debris:AddItem(attachment1,0.005)
				
				if anotherClimbable then
					climbing=false
					offClimb()
					local ve = Instance.new("BodyVelocity")
					ve.MaxForce = Vector3.new(1, 1, 1) * 40000
					ve.Velocity = rootpart.CFrame.RightVector * leftOrRight
					ve.Parent = rootpart
					game.Debris:AddItem(ve, 0.5)
					onClimb()
				end
			end

uhh, i have no idea what i did, and it’s not working… could you explain a bit more of what i should do to fix it?

I attempted to do a part to part

1 Like

Uh no I think you misunderstood, lemme explain…

What you would actually do is maybe casting a ray from each arm depending which direction the player is moving (left/right) this should be done using Right\Vector, reminder that you would need to offset the raycast origin to so that the ray wouldn’t detect the arm.

Example:

local rightarm = char["Right Arm"]
local hit  = workspace:Raycast((rightarm.CFrame*CFrame.new(3,0,0).Position,rightarm,CFrame.RightVector*2,param)
if hit then
    --// use face variable and the attachment code I made to adjust to the new surface

Here is how you would do a left arm

local leftarm = char["Left Arm"]
local hit  = workspace:Raycast((leftarm.CFrame*CFrame.new(-3,0,0).Position,leftarm,CFrame.RightVector*-2,param)
if hit then
    --// like rightarm

Now something you should include is casting a ray from the rootpart if the last touched part and the new part is not the same then use the alignposition code along the the climbGyro to adjust.

2 Likes
			local hit = workspace:Raycast(sideOrigin, sideDirection, params)
			if hit and (down("D") or down("A")) then -- right or left button is down
				local ifThatHit = false
				if down("D") then
					local hit  = workspace:Raycast((leftarm.CFrame*CFrame.new(-3,0,0).Position,leftarm,CFrame.RightVector*-2,param)
					if hit then
						ifThatHit = true
						local attachment1 = Instance.new("Attachment",hit.Instance)
						attachment1.WorldPosition = hit.Position
						alignPos.Attachment1 = attachment1
						face=CFrame.new(hit.Position+hit.Normal, hit.Position)
						game.Debris:AddItem(attachment1,0.005)
					end
				elseif down("A") then
					leftOrRight = -10
					local hit  = workspace:Raycast((rightarm.CFrame*CFrame.new(3,0,0).Position,rightarm,CFrame.RightVector*2,param)
					if hit then
						ifThatHit = true
						local attachment1 = Instance.new("Attachment",hit.Instance)
						attachment1.WorldPosition = hit.Position
						alignPos.Attachment1 = attachment1
						face=CFrame.new(hit.Position+hit.Normal, hit.Position)
						game.Debris:AddItem(attachment1,0.005)
					end
				end
				if ifThatHit == false then
					local attachment1 = Instance.new("Attachment",hit.Instance)
					attachment1.WorldPosition = hit.Position
					alignPos.Attachment1 = attachment1
					face=CFrame.new(hit.Position+hit.Normal, hit.Position)
					game.Debris:AddItem(attachment1,0.005)
				end
			end

so like this…?

1 Like

I mean you tell me did it work… when I gave you the example I didn’t have this in mind but what ever floats your boat man :confused:

1 Like

I fixed a few bugs, it worked, kind of… how would i use the bodygyro to face the current part?

video of my current version vvv
robloxapp-20210703-0832469.wmv (4.6 MB)

function onClimb()
	local origin = rootpart.Position
	local direction = rootpart.CFrame.LookVector

	local result = workspace:Raycast(origin, direction, params)
	if result then
		climbing=true
		script.RemoteEvent:FireServer(char.ForceShiftLock, true)
		
		rootpart.CFrame = CFrame.new(rootpart.CFrame.p, Vector3.new(rootpart.Position.X - result.Normal.X, rootpart.Position.Y, rootpart.Position.Z - result.Normal.Z))
		face = CFrame.new(result.Position+result.Normal, result.Position)

		hum.AutoRotate=false
		hum.PlatformStand=true

		climbMove.Parent=rootpart
		climbGyro.Parent=rootpart

		repeat
			run.RenderStepped:Wait()
			climbGyro.CFrame=face or CFrame.new()

			if rootpart.Position.Y > result.Instance.Size.Y then
				LedgeClimb()
				climbing=false
				offClimb()
				local ve = Instance.new("BodyVelocity")
				ve.MaxForce = Vector3.new(1, 1, 1) * 40000
				ve.Velocity = rootpart.CFrame.lookVector * 10 + Vector3.new(0, 7, 0)
				ve.Parent = rootpart
				game.Debris:AddItem(ve, 0.5)
				game.TweenService:Create(ve, TweenInfo.new(0.5, Enum.EasingStyle.Sine), {
				Velocity = rootpart.CFrame.lookVector * 10 + Vector3.new(0, 12, 0)
				}):Play()
			end
			
			local sideOrigin = rootpart.CFrame*CFrame.new(0, -0.4, -1).Position
			local sideDirection = rootpart.CFrame.RightVector*(down("D") and -2 or 2)
			local currentDown = nil
			local ifThatHit = nil
			
			if currentDown == down("D") then
				local hit  = workspace:Raycast(leftArm.CFrame*CFrame.new(-3,0,0).Position,leftArm.CFrame.RightVector*-2,params)
				if hit then
					ifThatHit = true
				end
			elseif currentDown == down("A") then
				local hit  = workspace:Raycast(rightArm.CFrame*CFrame.new(3,0,0).Position,rightArm.CFrame.RightVector*2,params)
				if hit then
					ifThatHit = true
				end
			else
				ifThatHit = false
			end
			
			local hit = workspace:Raycast(sideOrigin, sideDirection, params)
			if hit and (down("D") or down("A")) then -- right or left button is down
				if down("D") then
					currentDown = down("D")
				elseif down("A") then
					currentDown = down("A")
				end
				if ifThatHit == false then
					local attachment1 = Instance.new("Attachment",hit.Instance)
					attachment1.WorldPosition = hit.Position
					alignPos.Attachment1 = attachment1
					face=CFrame.new(hit.Position+hit.Normal, hit.Position)
					game.Debris:AddItem(attachment1,0.005)
				elseif ifThatHit == true then
					local attachment1 = Instance.new("Attachment",hit.Instance)
					attachment1.WorldPosition = hit.Position
					alignPos.Attachment1 = attachment1
					face=CFrame.new(hit.Position+hit.Normal, hit.Position)
					game.Debris:AddItem(attachment1,0.005)
				end
			elseif hit and not (down("D") and down("A")) then
				ifThatHit = false
			end
		until (not climbing)
		
		script.RemoteEvent:FireServer(char.ForceShiftLock, false)
		hum.AutoRotate=true
		hum.PlatformStand=false
	end
end
``` I just rewrote the logic, to try to fix the orientation issue, Im pretty confused how i got the logic wrong, could you maybe explain how the logic would work?
2 Likes

i finally got it to work, but it’s a bit glitchy, how would i fix this? the code for the part to part is this:

			local hit1 = workspace:Raycast(sideOrigin, sideDirection, params)
			local hit2 = nil
			local debounceThing = false
			
			if down("D") or down("A") then
				if down("D") then
					local hit2  = workspace:Raycast(rootpart.CFrame.Position,rootpart.CFrame.RightVector*2,params)
					if hit2 and debounceThing == false then
						debounceThing = true
						print("D hit")
						face=CFrame.new(hit2.Position+hit2.Normal, hit2.Position)
						wait(0.2)
						debounceThing = false
					end
				elseif down("A") then
					local hit2  = workspace:Raycast(rootpart.CFrame.Position,rootpart.CFrame.RightVector*-2,params)
					if hit2 and debounceThing == false then
						debounceThing = true
						print("A hit")
						face=CFrame.new(hit2.Position+hit2.Normal, hit2.Position)
						wait(0.4)
						debounceThing = false
					end
				end
			end
2 Likes

I apologize for the necrobump but recently I’ve been going through my old code as I didn’t work on anything or contributed for a while and well I came across this and I noticed how it uses deprecated bodymovers but what makes it even worse is the fact that it fits nowhere in the task scheduler so yeah that’s updated but please lemme clarify one more time:

PLEASE DONT USE THE SYSTEM RAW PUT IT UNDER SPECIFIED CONTROLS

Oh yeah the silly lil rapid side switch bug has been fixed too lol

as for questions or help just reply I’ll make sure to return next year not two years later!

6 Likes

Could you release a version where you can climb from side to side/part to part? I have made wall climbing but I’m not understanding how ill incorporate side to side/part to part climbing too.

2 Likes

when you find a climbable wall simply keep raycasting to the humanoidrootparts rightvector and -rightvector if if finds something then you just set it to the new found wall

1 Like

This isn’t useful if you need to check around corners, there’s a few ways to check around corners I believe but if I were to do it I’d just raycast out the rightvector then forward then left and do the inverse for the other side but there’s probably a easier way.

I was looking around the code and noticed that at one part you said that it could be converted to work to climb all surfaces of a part. Could you explain how it can be modified or maybe simply give the code along with a minor explanation?

You’d have to remove the Y limitations that I put on, then shoot rays that hit the bottom and top by shooting a ray slightly in-front and down? maybe up too, I don’t think you have to do super complex ray casting because of Vector3.FromNormalId

Can’t believe I had to edit this so many times, when brain with no food.

2 Likes