Hello,
Currently, I have a system that uses raycasting and alignment constraints to allow the player to wall climb, but the player does not move dynamically with the wall.
Here’s a video demonstrating the issue:
I’ve tried a few different approaches but haven’t been able to get it to work correctly. Any advice or suggestions would be greatly appreciated!
Here’s the code:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")
local WallClimb = {}
WallClimb.__index = WallClimb
function WallClimb.new(character)
local self = setmetatable({}, WallClimb)
self.Character = character
self.ClimbCooldownTime = 0.25
self.RaycastLength = 4
self.DistanceFromWall = 1.5
self.ClimbSpeed = 15
self.AnimationSpeed = 1
self.MoveInterpolationFactor = 0.2
self.LastClimbTimestamp = 0
self.ClimbingAnimation = self.Character.Animator:LoadAnimation(ReplicatedStorage.Animations.Climbing)
-- Position and Orientation Alignment
self.PositionAlign = Instance.new("AlignPosition")
self.PositionAlign.Mode = Enum.PositionAlignmentMode.OneAttachment
self.PositionAlign.Parent = self.Character.HumanoidRootPart
self.PositionAlign.Attachment0 = self.Character.HumanoidRootPart.RootAttachment
self.PositionAlign.Enabled = false
self.OrientationAlign = Instance.new("AlignOrientation")
self.OrientationAlign.Mode = Enum.OrientationAlignmentMode.OneAttachment
self.OrientationAlign.Parent = self.Character.HumanoidRootPart
self.OrientationAlign.Attachment0 = self.Character.HumanoidRootPart.RootAttachment
self.OrientationAlign.Enabled = false
return self
end
function WallClimb:Update(deltaTime)
local raycastResult = self:CastRay()
if raycastResult and self:IsCooldownOver() and not self.Character.Humanoid.Jump then
self:BeginClimbing(raycastResult, deltaTime)
else
self:EndClimbing()
end
end
function WallClimb:CastRay()
if not self.CurrentCFrame then
self.CurrentCFrame = self.Character.HumanoidRootPart.CFrame
end
local origin = self.CurrentCFrame.Position
local direction = self.CurrentCFrame.LookVector
local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Include
rayParams.FilterDescendantsInstances = {unpack(game:GetService("CollectionService"):GetTagged("Climbable"))}
return workspace:Raycast(origin, direction * self.RaycastLength, rayParams)
end
function WallClimb:ClimbCFrame(raycastResult)
local wallNormal = raycastResult.Normal * -1
local rightVector = wallNormal:Cross(Vector3.yAxis).Unit
local upVector = rightVector:Cross(wallNormal).Unit
local holdPosition = raycastResult.Position + (raycastResult.Normal * self.DistanceFromWall)
return CFrame.fromMatrix(holdPosition, rightVector, upVector)
end
function WallClimb:MovementVector(climbCFrame)
local adjustedCFrame = climbCFrame * CFrame.Angles(math.pi / -2, 0, 0)
local moveDirection = self.Character.Humanoid.MoveDirection
local relativeMoveDirection = workspace.CurrentCamera.CFrame.Rotation:PointToObjectSpace(moveDirection)
relativeMoveDirection *= Vector3.new(1, 0, -1)
if relativeMoveDirection.Magnitude > 0 then
relativeMoveDirection = relativeMoveDirection.Unit
end
return adjustedCFrame.Rotation:PointToWorldSpace(relativeMoveDirection)
end
function WallClimb:BeginClimbing(raycastResult, deltaTime)
if not self.IsClimbing then
self.IsClimbing = true
self.ClimbingAnimation:Play()
SoundService:PlayLocalSound(SoundService.Action.Stick)
end
self.PositionAlign.Enabled = true
self.OrientationAlign.Enabled = true
self.Character.Humanoid.AutoRotate = false
local frameRate = 60
local timeFactor = deltaTime * frameRate
local climbCFrame = self:ClimbCFrame(raycastResult)
local movementVector = self:MovementVector(climbCFrame)
self.CurrentCFrame = climbCFrame + movementVector * self.ClimbSpeed * deltaTime
self.Character.HumanoidRootPart.CFrame = self.Character.HumanoidRootPart.CFrame:Lerp(self.CurrentCFrame, self.MoveInterpolationFactor * timeFactor)
self.PositionAlign.Position = self.Character.HumanoidRootPart.Position
self.OrientationAlign.CFrame = self.Character.HumanoidRootPart.CFrame
self.ClimbingAnimation:AdjustSpeed(movementVector.Magnitude * self.AnimationSpeed)
end
function WallClimb:EndClimbing()
if self:IsCooldownOver() then
self.LastClimbTimestamp = tick()
end
self.PositionAlign.Enabled = false
self.OrientationAlign.Enabled = false
self.Character.HumanoidRootPart.Anchored = false
self.Character.Humanoid.AutoRotate = true
self.CurrentCFrame = nil
if self.IsClimbing then
self.IsClimbing = false
self.Character.Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
self.ClimbingAnimation:Stop()
SoundService:PlayLocalSound(SoundService.Action.Detatch)
end
end
function WallClimb:IsCooldownOver()
local currentTime = tick()
local timeSinceLastClimb = currentTime - self.LastClimbTimestamp
return timeSinceLastClimb >= self.ClimbCooldownTime
end
return WallClimb