Help with Player Move with the Wall Dynamically

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
2 Likes

I think you want to use train platforming, here is a tutorial: Jailbreak train platform system? - #35 by Kord_K

It’s simply CFrame math that you can use

1 Like

I think the issue is in the BeginClimbing method when you set self.PositionAlign.Position to itself

self.PositionAlign.Position = self.Character.HumanoidRootPart.Position
1 Like

Does AssemblyLinearVelocity help?

1 Like

if this section makes u climb and stays then your align instances are being turned to false when enabled.

1 Like

Maybe use a weld :man_shrugging:

Haven’t tried, it’s just a suggestion, not guaranteed

Tho could work :ok_hand:

1 Like

Thank you! I managed to get it working after some time, here’s the result and modified code:

function WallClimb:BeginClimbing(raycastResult, deltaTime)
	local wall = raycastResult.Instance

	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
	
	if self.LastCFrame == nil then
		self.LastCFrame = wall.CFrame 
	end
	
	local WallCFrame = wall.CFrame
	local Rel = WallCFrame * self.LastCFrame:inverse()
	self.LastCFrame = wall.CFrame
	
	local frameRate = 60
	local timeFactor = deltaTime * frameRate

	local climbCFrame = self:ClimbCFrame(raycastResult)
	local movementVector = self:MovementVector(climbCFrame)
	local adjustedClimbCFrame = climbCFrame + movementVector * self.ClimbSpeed * deltaTime

	self.CurrentCFrame = Rel * adjustedClimbCFrame
	self.Character.HumanoidRootPart.CFrame = self.Character.HumanoidRootPart.CFrame:Lerp(self.CurrentCFrame, self.MoveInterpolationFactor * timeFactor)
	self.Character.HumanoidRootPart.CFrame = Rel * self.Character.HumanoidRootPart.CFrame

	self.PositionAlign.Position = self.Character.HumanoidRootPart.Position
	self.OrientationAlign.CFrame = self.Character.HumanoidRootPart.CFrame

	self.ClimbingAnimation:AdjustSpeed(movementVector.Magnitude * self.AnimationSpeed)
end
2 Likes

Pretty much entire quest is to detect if player touches platform, rest is adding relative CFrame soo player position will update