Creating a Wall Climbing System (With Wall Switching!)

Creating a Wall Climbing System
(a guide by Doomcolp)
NOTE

I DID NOT USE AlignPosition or any body movers (body movers are deprecated anyways).

It’s super jittery but it does work. There’s also no animations because I can’t seem to figure out to make them look good.

This is also probably not the most performant way to create a wall climbing system, I haven’t done any performance tests (I don’t know how to lol)



Background

A couple of months ago, I was helping this guy in Scripting Support create a “wall switching” system for his wall climbing system. I decided to attempt it on my own, and I did it. So now, I’d like to share how I did it.

Here’s that post.

And yes, this does include mobile support.


To start…

just add a LocalScript (it can be in either StarterPlayerScripts or StarterCharacterScripts). Player movement is automatically replicated to the server. I will be referring to this LocalScript as “Client”.

Defining Variables

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local Player = game.Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")
local Humanoid = Character:WaitForChild("Humanoid")
local Head = Character:WaitForChild("Head")

local IsOnPC = UserInputService.KeyboardEnabled

We’re gonna need ReplicatedStorage so we can access the Modules folder which will contain all of the modules that we’ll create.

RunService is also required because we’ll need to move the player’s character along the wall as soon as they trigger input.

UserInputService will be used to get the input the player will provide.

Other Stuff to Define

local Keys = {
    [Enum.KeyCode.W] = true,
    [Enum.KeyCode.A] = true,
    [Enum.KeyCode.S] = true,
    [Enum.KeyCode.D] = true,
    [Enum.KeyCode.Space] = true
}

This will make it so that when we’re detecting input, we can simply check if the provided input is a valid key for climbing a wall.

local Params = RaycastParams.new()
Params.FilterDescendantsInstances = {Character}
Params.FilterType = Enum.RaycastFilterType.Exclude
Params.RespectCanCollide = true

These raycast params will just make it so that we can detect walls and ignore the player’s character.


Modules

I have a Cast module in my setup, but it only has one function (.new). I only created it because I thought it looked nicer. In all the code I’m going to exclude it and replace it with workspace:Raycast since that’s all that module does.

A module you do need is a Climb (or name it whatever you want to name it) module. This is going to hold our actual climbing logic.

I’m keeping my Climb module in a folder in ReplicatedStorage (Modules). Keep it wherever you’d like as long as it is accessible to the client.

Reference The Module

local Climb = require(path.to.module)

“Climb” Module Code

local Climb = {}
Climb.IsClimbing = false

local Player = game.Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Humanoid = Character:WaitForChild("Humanoid")
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")

local CLIMB_MULTIPLIER = 0.25

CLIMB_MULTIPLIER isn’t really a multiplier, but I didn’t know what else to call it. Anyways, it will just be applied to make the climbing speed faster / slower.

local function getKeyMovement(Key: Enum.KeyCode): Vector3
    if Key == Enum.KeyCode.W or Key == "W" then
        return HumanoidRootPart.CFrame.UpVector
    elseif Key == Enum.KeyCode.A or Key == "A" then
        return -HumanoidRootPart.CFrame.RightVector
    elseif Key == Enum.KeyCode.S or Key == "S" then
        return -HumanoidRootPart.CFrame.UpVector
    elseif Key == Enum.KeyCode.D or Key == "D" then
        return HumanoidRootPart.CFrame.RightVector
    end
end

This function will just return a vector based on what key the player pressed (excluding the Space key). If you’re confused about this, I can probably link a guide here later.

function Climb:Init()
    --// I've already defined "Params", scroll up the post and you'll find it
    local Cast: RaycastResult = workspace:Raycast(HumanoidRootPart.Position, HumanoidRootPart.CFrame.LookVector, Params)
    if not Cast then return end
    self.IsClimbing = true
    HumanoidRootPart.Anchored = true
    Humanoid:ChangeState(Enum.HumanoidStateType.Physics)
end

Init is short for initialize for those who don’t know. This is casting a ray straight forward from the player’s character to see if there’s a wall directly in front of the wall.

If there is a wall directly in front, we’ll just make it so that the player can’t walk normally. We’ll manually move their character later.

The “Start” Function

function Climb:Start(Key: Enum.KeyCode | string)
    local Cast = workspace:Raycast(HumanoidRootPart.Position, HumanoidRootPart.CFrame.LookVector, Params)
    local KeyMovement = getKeyMovement(Key)

Once again, we’re casting a ray forward from the player’s character to see if there’s a wall. This time however, there will be scenerios where not having a wall in front is acceptable which is why we don’t exit the function.

if not Cast then
  if Key == Enum.KeyCode.W or Key == "W" then
    self:Stop()
    HumanoidRootPart:ApplyImpulse(HumanoidRootPart.CFrame.UpVector * 500)
  else
    self:Stop()
  end
  return
end

This first code snippet is for detecting when the player isn’t on the wall anymore. This code stops the player from climbing a wall.

The if statement (which structure will be present for the other keys too):

if Key == Enum.KeyCode.W or Key == “W” then

is just checking whether the Key is the actual W key (Enum.KeyCode.W) or if it is a string, “W” (for mobile support which I’ll explain later).

HumanoidRootPart:ApplyImpulse(HumanoidRootPart.CFrame.UpVector * 500)

simply pushes the player’s character up a bit if they reach the top of the wall. If you remove this line, the rays used to detect when the player is infront of a wall will keep hitting. Therefore, the player will keep climbing, but they’ll be sort of stuck.

The else statement just accounts for when the player has switched walls and is sort of noclipped into that wall.

if Key == Enum.KeyCode.S or Key == "S" then
        local VerticalCast = workspace:Raycast(HumanoidRootPart.Position, -HumanoidRootPart.CFrame.UpVector * (Humanoid.HipHeight * 1.25), Params)
        if VerticalCast then self:Stop() return end
    end
HumanoidRootPart.CFrame = CFrame.lookAt(HumanoidRootPart.Position, Cast.Position - Cast.Normal) + KeyMovement * CLIMB_MULTIPLIER

This will check if the player is into the ground too much (Humanoid.HipHeight * 1.25). If they are, it’ll stop the player from climbing.

This next part might be hard to visualize / interpret, so I’ll try my best to break it down…

HumanoidRootPart.CFrame = CFrame.lookAt(HumanoidRootPart.Position, Cast.Position - Cast.Normal) + KeyMovement * CLIMB_MULTIPLIER

(“Cast” refers to this raycast from the top of the function)

We’re just making the player’s character face the current wall and move downwards (according to KeyMovement which when S is pressed, will return the DownVector relative to the HumanoidRootPart’s current CFrame). We then just multiply it by our previously defined “multiplier” (CLIMB_MULTIPLIER).


Wall Switching

if Key == Enum.KeyCode.A or Key == "A" then
        local AdjacentCast = workspace:Raycast(HumanoidRootPart.Position + (HumanoidRootPart.CFrame.LookVector * 1.5) + -HumanoidRootPart.CFrame.RightVector, HumanoidRootPart.CFrame.RightVector, Params)
        local PerpendicularCast = workspace:Raycast(HumanoidRootPart.Position, -HumanoidRootPart.CFrame.RightVector, Params)
        if AdjacentCast and not PerpendicularCast then
            self:Switch(AdjacentCast.Position, AdjacentCast.Normal)
        elseif PerpendicularCast then
            self:Switch(PerpendicularCast.Position, PerpendicularCast.Normal)
        end

about the D key

The D if statement is literally the same thing as the A if statement. It just reverses the directions (instead using LeftVector of the player’s character, it uses RightVector).

(For the A if statement) LeftVector isn’t a default property of the CFrame property in a part. Why? Because it’s just the negated version of RightVector which is how I reference it.

    elseif Key == Enum.KeyCode.D or Key == "D" then
        local AdjacentCast: RaycastResult = workspace:Raycast(HumanoidRootPart.Position + (HumanoidRootPart.CFrame.LookVector * 1.5) + HumanoidRootPart.CFrame.RightVector, -HumanoidRootPart.CFrame.RightVector, Params)
        local PerpendicularCast: RaycastResult = workspace:Raycast(HumanoidRootPart.Position, HumanoidRootPart.CFrame.RightVector, Params)
        if AdjacentCast and not PerpendicularCast then
            self:Switch(AdjacentCast.Position, AdjacentCast.Normal)
        elseif PerpendicularCast then
            self:Switch(PerpendicularCast.Position, PerpendicularCast.Normal)
        end
    end

The Adjacent Ray

Now it’s time for wall switching! I personally think this is one of those features that makes the UX better. This one is also kinda hard to visualize, so I’ll try my best to explain it.

local AdjacentCast = workspace:Raycast(HumanoidRootPart.Position + (HumanoidRootPart.CFrame.LookVector * 1.5) + -HumanoidRootPart.CFrame.RightVector, HumanoidRootPart.CFrame.RightVector, Params)

We’re just casting a ray starting from in front of the player’s character a bit and a bit to the left. Why? Well, if there’s no wall directly to the left, we’re gonna switch sides.

To detect that other side, we have to hit it with a ray (of course). To do this, we move the ray’s starting point forwards into the wall (so it’s now inside the wall)

HumanoidRootPart.Position + (HumanoidRootPart.CFrame.LookVector * 1.5)

and then to the left so that it exits the wall. It’s able to exit the wall because this part will only work when the player is at the very end of the left side of the wall they’re currently on.

+ -HumanoidRootPart.CFrame.RightVector

The arrow is moving the ray’s starting point forward, then we move to the left to “exit” the wall. The dot represents the new starting point.

The player is at the end of the wall just enough so that when we move the ray forward, it still goes into the wall. But, remember, we’re at the end / edge of the wall to the left side; so, moving the ray’s starting point to the left a bit will “push” it out of the wall entirely.

Now, we cast it towards the right since the ray’s starting point is to the left of the face we want to switch to. This will allow our ray to hit the face.

The arrow represents the ray being casted to the right. The star is where the ray hit the face. Notice how it’s at the right edge of the new face.

Since we moved forward only a little bit earlier, the position that the ray will hit will be really close the the right side of the face we’re switching to which will make it look like the player smoothly climbed over.

In more detail, since the player’s character before switching is on the right side of the new face (and the character is facing the left side of the new face), moving the ray more forward would make the ray get closer to the left side (further away from the desired right edge).

HumanoidRootPart.CFrame.RightVector


What if you move the ray's starting point to right instead of the left?

On the contrary, moving the ray to the right will make it go deeper into the wall towards the right which is NOT what we want. We need to hit the wall adjacent to the left side of wall the player is current on.


“Why are you moving out to the left first? Can’t you just move into the wall and cast to the left and hit the corresponding face?”

Well no, you can’t hit a face of a part via raycasting from inside the part (I figured that out thru five hours of trial and error)


The Perpendicular Ray

Next, since the starting point is outside (and directly in front of the face we want to switch to), we will simply cast to the left so that we hit that face:

local PerpendicularCast = workspace:Raycast(HumanoidRootPart.Position, -HumanoidRootPart.CFrame.RightVector, Params)

This ray is here to detect if there is a wall directly to the player’s left (unlike what we’re detecting with our AdjacentCast). Then, we will be able to switch to that wall (notice how I say “wall” instead of “side”).

We cast it directly to the left.

The arrow represents the ray being casted to the left. The start represents the point where the ray hit the wall directly to the left of the player’s character’s current CFrame

Deciding Where To Switch To

if AdjacentCast and not PerpendicularCast then
self:Switch(AdjacentCast.Position, AdjacentCast.Normal)

If there’s no wall directly to our left, then we’ll switch the player’s character to the other side of the wall that they’re currently on.

elseif PerpendicularCast then
self:Switch(PerpendicularCast.Position, PerpendicularCast.Normal)

If there’s a wall directly left to the player’s character, we’ll switch to that wall.

And Here’s The Last 2 Functions in This Module…

Here’s the Switch function:

function Climb:Switch(Position, Surface)
    HumanoidRootPart.CFrame = CFrame.lookAt(Position + (Surface / 1.25), Position)
end

All it does it makes the player’s character move to where the ray hit whether it be another side or another wall, and it makes the character face the direction of the face / wall that was hit.

function Climb:Stop(): nil
    if not self.IsClimbing then return end
    self.IsClimbing = false
    HumanoidRootPart.Anchored = false
    Humanoid:ChangeState(Enum.HumanoidStateType.Running)
end

This function is the exact opposite of the Start function.


The Final Part

Go back to Client (the LocalScript where you defined all of those variables). And take a look at this function:

local function ConstructDirections()
    return {
        ["W"] = HumanoidRootPart.CFrame.LookVector,
        ["A"] = -HumanoidRootPart.CFrame.RightVector,
        ["S"] = -HumanoidRootPart.CFrame.LookVector,
        ["D"] = HumanoidRootPart.CFrame.RightVector
    }
end

All this does is it gets all of the directions we need relative to the player’s character’s current CFrame and returns it.

local function GetKeyFromMoveDirection()
    local Direction= Humanoid.MoveDirection
    for Key, DirectionVector in pairs(ConstructDirections()) do
        if math.round(Direction:Dot(DirectionVector)) == 1 then
            return Key
        end
    end
end

This function adds mobile support. It basically gets the direction that the player is moving (with Humanoid.MoveDirection)

and uses Vector3:Dot on every valid direction from ConstructDirections to see which direction they’re going. Then, it returns the name of the key which corresponds with the direction.

Dot compares how much two vectors are going in the same direction. A value of one (1) means that they’re going in the same direction.

For more info on dot, check out this link.

I Don’t Know What to Name This Section

local function ClimbFunction(Key)
    if Key == Enum.KeyCode.W or Key == "W" then
        if Climb.IsClimbing then
            Climb:Start(Key)
        elseif not Climb.IsClimbing then
            Climb:Init()
        end
    elseif Key == Enum.KeyCode.A or Key == "A" then
        if Climb.IsClimbing then
            Climb:Start(Key)
        end
    elseif Key == Enum.KeyCode.S or Key == "S" then
        if Climb.IsClimbing then
            Climb:Start(Key)
        end
    elseif Key == Enum.KeyCode.D or Key == "D" then
        if Climb.IsClimbing then
            Climb:Start(Key)
        end
    elseif Key == Enum.KeyCode.Space then
        if Climb.IsClimbing then
            Climb:Stop()

Don’t want the post to be too large but hopefully this is self explanatory. Key == (key) is for mobile support since GetKeyFromMoveDirection returns a string (although you could check to see if there is a Enum.KeyCode for the string version of Key).

Now The ACTUAL Final Thing

RunService:BindToRenderStep("ClimbDetector", Enum.RenderPriority.Camera.Value, function()
    if IsOnPC then
        for i , Key in pairs(UserInputService:GetKeysPressed()) do
            if not Keys[Key.KeyCode] then continue end
            ClimbFunction(Key.KeyCode)
        end
    else
        local Key = GetKeyFromMoveDirection()
        ClimbFunction(Key)
    end
end)

Finally, we will constantly (after the camera is rendered each frame) get the keys the player is holding with UserInputService:GetKeysPressed and move them accordingly for both PC (UserInputService.KeyboardEnabled) and for mobile (else).


Closing Statements

Congratulations! You now know how to make a climbing system! Feel free to make changes you think are necessary! If you have any feedback or suggestions, please reply and let me know!

Again, I don’t know how performant this code actually is. So I don’t know how it would perform in a big game.

It has also been bug free from all my testing so it should work just fine.

Where Do I Test This?

You can join this game. But the animations aren’t very pretty (there are none).


How Easy Was It to Understand This Tutorial?
(If you chose Hard or Very Hard, then please reply and tell me how I can make the post easier to understand)
  • Very Easy
  • Easy
  • Not too easy, not too hard
  • Hard
  • Very Hard
0 voters

Thank you for reading my tutorial! This took about 3 hours to make.

7 Likes

I am the guy you were helping, nice tutorial man

1 Like

Thanks! Yeah I just wanted to make a full tutorial for those who dont know how to make one :slight_smile:

1 Like