Making a "Wall Climbing" System

Personally I used bodygyro and just set it to the vector normal of the wall but their is definently another way to go about it

Do you have an open source so I can analyze?

  1. Hello, I have read this topic and used the open source that was shared. I have edited the open source to be able to switch from different walls, but I have run into a major problem. Whenever the player switches between two walls that are adjacent to each other, and create lower than a 90 degree angle, when the player tries to flip around the new wall they climbed onto, the player starts floating, and is able to climb left and right infinitely, almost as if there is a large invisible wall. The script works great at 90 degree angles, but anything lower makes this bug occur. This bug breaks many systems, like the wall jumping, and after hours of reasearch, I cannot figure out why it does this. This is the code I am using
local ppl							= game:GetService("Players")
local run							= game:GetService("RunService")
local uis							= game:GetService("UserInputService")

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

local spd							= hum:GetAttribute("ClimbSpeed")
--// I was gonna put "state = hum:GetAttribute("ClimbState")" but attributes don't work with repeated checks.


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

--// attachments are a most for constraints.
local attach						= Instance.new("Attachment")
attach.Parent						= root


--// as the name says, it's for movement while climbing
local move							= Instance.new("LinearVelocity")
move.Enabled						= false
move.MaxForce						= math.huge
move.Attachment0					= attach
move.RelativeTo						= Enum.ActuatorRelativeTo.Attachment0
move.VectorVelocity					= Vector3.new()
move.Parent							= root


--// you should try playing with gyro off it's funny
local gyro							= Instance.new("AlignOrientation")
gyro.Enabled						= false
gyro.Mode							= Enum.OrientationAlignmentMode.OneAttachment
gyro.RigidityEnabled				= true
gyro.Attachment0					= attach
gyro.Parent							= root

local boostRemote = game.ReplicatedStorage.LedgehopRE
local maxDelay = 0.4
local t1 = 0

local hotfix = false

local animationUp = script:WaitForChild("Up")

local result

local XAxis = {
	0; --Left
	0 --Right
}
local YAxis = {
	0; --Up
	0 --Down
}

--// Just here so the code in the step doesn't look spaghetti as hell lol
function down(KEY)
	return uis:IsKeyDown(Enum.KeyCode[KEY])
end

--// WHY THE HELL DOESNT CHANGED SIGNAL RETURN THE VALUE????
hum:GetAttributeChangedSignal("ClimbState"):Connect(function()
	if hum:GetAttribute("ClimbState") == true then
		local origin = root.Position
		local direction = root.CFrame.LookVector

		result = workspace:Raycast(origin, direction, params)
		
		if result then -- where climbing starts
			hum.AutoRotate=false
			hum.PlatformStand=true
			gyro.CFrame = CFrame.lookAt(result.Position+(result.Normal/2), result.Position)
			move.Enabled=true
			gyro.Enabled=true
		else
			hum:SetAttribute("ClimbState", false)
		end
	else
		hum.AutoRotate=true
		hum.PlatformStand=false
		move.Enabled=false
		gyro.Enabled=false
		result=nil
	end
end)

hum:GetAttributeChangedSignal("ClimbSpeed"):Connect(function()
	spd	= hum:GetAttribute("ClimbSpeed")
end)

--// Befored... I used a heartbeat:Wait() in a repeat until loop...
--// WELL THAT IS FIXED NOW!!
run.Heartbeat:Connect(function()
	if hum:GetAttribute("ClimbState") == true and result ~= nil then
		
		--// You may call me lazy and stupid but honestly how else would you do "Ledge Climbing"
		--// Oh yeah also added the fact u just stop climbing if u climb down a lil too much
		if root.Position.Y >= result.Instance.Position.Y + result.Instance.Size.Y/2 or root.Position.Y <= 2 then
			hum:SetAttribute("ClimbState", false) --// gotta be here before we update the cframe.
			--root.CFrame += root.CFrame.LookVector+root.CFrame.UpVector*2 -- og teleport
			boostRemote:FireServer()
			for i,v in pairs(plr.Character.Humanoid:GetPlayingAnimationTracks()) do
				v:Stop()
			end
		end
		
		--// Secondary Ray for turning, could be edited to be all sides of a basepart! figure it out yourself.
		--// No need for a if statement.
		-- ledge turning
		local sideOrigin = root.CFrame * CFrame.new(-.01, -0.3, -1).Position
		local sideDirection1 = root.CFrame.RightVector*(down("D") and -2 or 2) --this is for end corners, opposite signs of 2 is for inside corners
		local sideDirection2 = root.CFrame.RightVector*(down("A") and -2 or 2)

		local hit = workspace:Raycast(sideOrigin, sideDirection1, params)
		local corner = workspace:Raycast(sideOrigin,sideDirection2,params)
		
		if corner and (down("D") or down("A")) then
			if hotfix == true then return end
			hotfix = true
			if corner.Instance and corner.Instance ~= result.Instance then
				print("not the same")
				result = corner
				gyro.CFrame = CFrame.lookAt(corner.Position+(corner.Normal/2), corner.Position)
				root.CFrame = CFrame.lookAt(corner.Position+(corner.Normal/2), corner.Position)
				wait(0.1)
				hotfix = false
			end
		end
		
		if hit and (down("D") or down("A")) then
			if hotfix == true then return end
			hotfix = true
			if hit.Instance and hit.Instance == result.Instance then
				print("the same")
				gyro.CFrame = CFrame.lookAt(hit.Position+(hit.Normal/2), hit.Position)
				root.CFrame = CFrame.lookAt(hit.Position+(hit.Normal/2), hit.Position)
				wait(0.1)
				hotfix = false
			end
		end
		
	end
end)

--side = workspace:Raycast(char["Torso"].Position,Vector3.new(2,0,0), params)

--// I only did this because it looks cleaner.
do
	local inputTask = {
		[Enum.KeyCode.W] = function(state: boolean)
			YAxis[1] = (state and 1) or 0
		end;

		[Enum.KeyCode.S] = function(state: boolean)
			YAxis[2] = (state and 1) or 0
		end;

		[Enum.KeyCode.A] = function(state: boolean)
			XAxis[1] = (state and 1) or 0
		end;

		[Enum.KeyCode.D] = function(state: boolean)
			XAxis[2] = (state and 1) or 0
		end;

		[Enum.KeyCode.Space] = function(state: boolean)
			if tick() - t1 <= maxDelay then
			if state then
				if hum:GetAttribute("ClimbState") == false then
					hum:SetAttribute("ClimbState", true)
				else
					hum:SetAttribute("ClimbState", false)
						for i,v in pairs(plr.Character.Humanoid:GetPlayingAnimationTracks()) do
							v:Stop()
						end
				end
			end
			else
				t1 = tick()
			end
		end;
	}
	
	uis.InputBegan:Connect(function(input: InputObject)
		if inputTask[input.KeyCode] then
			inputTask[input.KeyCode](true)
		end
	end)

uis.InputEnded:Connect(function(input: InputObject)
		if inputTask[input.KeyCode] then
			inputTask[input.KeyCode](false)
		end
	end)
end

--// LOL IDK BUT I THINK ITS CHARACTER CUS WE ARE MESSING WITH THE CHARACTER
run:BindToRenderStep("constraints update", Enum.RenderPriority.Character.Value, function()
	if hum:GetAttribute("ClimbState") == true then
		local X = XAxis[2]-XAxis[1]
		local Y = YAxis[1]-YAxis[2]
		
		move.VectorVelocity = Vector3.new(X*spd, Y*spd, 0)
	end
end)

uis.InputBegan:Connect(function()
	if hum:GetAttribute("ClimbState") == true then
		local animationUp = script:WaitForChild("Up")
		local Up = hum.Animator:LoadAnimation(animationUp)

		local animationDown = script:WaitForChild("Down")
		local Down = hum.Animator:LoadAnimation(animationDown)
		
		local animationRight = script:WaitForChild("Right")
		local Right = hum.Animator:LoadAnimation(animationRight)
		
		local animationLeft = script:WaitForChild("Left")
		local Left = hum.Animator:LoadAnimation(animationLeft)
		
		local WDown =  uis:IsKeyDown(Enum.KeyCode.W)
		local SDown =  uis:IsKeyDown(Enum.KeyCode.S)
		local DDown =  uis:IsKeyDown(Enum.KeyCode.D)
		local ADown =  uis:IsKeyDown(Enum.KeyCode.A)
		
		if WDown then
			for i,v in pairs(plr.Character.Humanoid:GetPlayingAnimationTracks()) do
				v:Stop()
			end
			Up:Play()
		end
		
		if SDown then
			for i,v in pairs(plr.Character.Humanoid:GetPlayingAnimationTracks()) do
				v:Stop()
			end
			Down:Play()
		end
		
		if DDown then
			for i,v in pairs(plr.Character.Humanoid:GetPlayingAnimationTracks()) do
				v:Stop()
			end
			Right:Play()
		end
		
		if ADown then
			for i,v in pairs(plr.Character.Humanoid:GetPlayingAnimationTracks()) do
				v:Stop()
			end
			Left:Play()
		end
	end
end)

uis.InputEnded:Connect(function()
	if hum:GetAttribute("ClimbState") == true then

		local animationIdle = script:WaitForChild("Idle")
		local Idle = hum.Animator:LoadAnimation(animationIdle)
		
		for i,v in pairs(plr.Character.Humanoid:GetPlayingAnimationTracks()) do
			v:Stop()
		end
		Idle:Play()
	end
end)

I am wondering if it is a simple fix i am missing, or if it is a little more complex. If you need more info on the problem i can create a new starting place with the code for you to test yourself. Thank you. (also sorry if some stuff is formatted wrong, as this is one of my first posts)

2 Likes

i might be a bit late, but try changing your variables to this:

local sideOrigin = humanoidRootPart.Position + humanoidRootPart.CFrame.LookVector
local sideDirection1 = (isKeyDown("D") and -humanoidRootPart.CFrame.RightVector) or humanoidRootPart.CFrame.RightVector
local sideDirection2 = humanoidRootPart.CFrame.RightVector * (isKeyDown("A") and -2 or 2)

local hit = workspace:Raycast(sideOrigin, sideDirection1, raycastParams)
local corner = workspace:Raycast(sideOrigin, sideDirection2, raycastParams)
1 Like

Hello, this is my main account, as I posted my problem on my friend’s account because I wasn’t able to comment yet. The climb script i used somehow broke while it was in disuse. I have tried my best to fix my issue, and also tried your solution, but to no avail. Thank you for your help, but I have made my own, somewhat similar system that is a little simpler, and will not be needing this system anymore.

1 Like