Never mind I had some time to be able to do it.
Here’s what I came up with, I’ve added comments and such + where to enable/disable moving along the entire wall’s surface or just the top edge:
local userInputService = game:GetService('UserInputService')
local humanoid: Humanoid = script.Parent:WaitForChild('Humanoid') -- setup humanoid for wall climbing
humanoid.WalkSpeed = 0
humanoid.PlatformStand = true
local root: BasePart = script.Parent:WaitForChild('HumanoidRootPart')
local rootConstraintAttachment = Instance.new('Attachment')
rootConstraintAttachment.CFrame = CFrame.Angles(0, 0.5 * math.pi, 0)
rootConstraintAttachment.Parent = root
local moveVector = Vector3.new(0, 0, 0)
local binds = {
[Enum.KeyCode.W] = Vector3.new(0, 1, 0); -- up
[Enum.KeyCode.A] = Vector3.new(1, 0, 0); -- left
[Enum.KeyCode.S] = Vector3.new(0, -1, 0); -- down
[Enum.KeyCode.D] = Vector3.new(-1, 0, 0); -- right
}
local wall = workspace:WaitForChild('Part')
local wallCFrame = wall.CFrame
local frontFace = wall.Position - (wallCFrame.LookVector * 0.5 * wall.Size.Z) -- for "locking" to the top edge + 0.5 * wall.Size.Y * wallCFrame.UpVector
root.CFrame = CFrame.lookAt(frontFace - wallCFrame.LookVector * 0.5 * root.Size.Z, frontFace)
local wallAttachment = Instance.new('Attachment') -- attachment in the wall for the PlaneConstraint
wallAttachment.CFrame = CFrame.new(0, 0, (wall.Size.Z * 0.5 + root.Size.Z * 0.501)) * CFrame.Angles(0, 0.5 * math.pi, 0)
wallAttachment.Parent = wall
-- PlaneConstraint itself, will make the root be a certain distance from the wall's front surface
local planeConstraint = Instance.new('PlaneConstraint')
planeConstraint.Attachment0 = wallAttachment
planeConstraint.Attachment1 = rootConstraintAttachment
planeConstraint.Parent = root
-- this will be what moves us around the wall
local linearVelocity = Instance.new('LinearVelocity')
linearVelocity.Attachment0 = rootConstraintAttachment
linearVelocity.ForceLimitMode = Enum.ForceLimitMode.PerAxis
linearVelocity.MaxAxesForce = Vector3.new(math.huge, math.huge, math.huge)
linearVelocity.VectorVelocity = Vector3.new(0, 0, 0)
linearVelocity.Parent = root
-- for ensuring the root keeps an appropriate orientation
local orientation = Instance.new('AlignOrientation')
orientation.RigidityEnabled = true
orientation.CFrame = rootConstraintAttachment.WorldCFrame
orientation.Mode = Enum.OrientationAlignmentMode.OneAttachment
orientation.Attachment0 = rootConstraintAttachment
orientation.Parent = root
local function update()
if moveVector.Magnitude < 0.001 then -- no movement, we have to check magnitude for 0 because we will get nan if we try to use .Unit on it
linearVelocity.VectorVelocity = Vector3.zero
return
end
linearVelocity.VectorVelocity = moveVector.Unit * 5 -- set the LinearVelocity's VectorVelocity; .Unit * 5 ensures they only move 5 studs per second
end
game:GetService('RunService').PostSimulation:Connect(function(dt)
moveVector = Vector3.zero
-- step 1: get their move vector
for key, vec in binds do
if userInputService:IsKeyDown(key) then
moveVector += vec * Vector3.new(math.sign(workspace.CurrentCamera.CFrame.LookVector:Dot(root.CFrame.LookVector)), 1, 0) -- small ux thing to swap A and D if the player's camera is facing the front of their character
end
end
-- step 2: clamp the root's position to be within the bounds of `wall`
local cfRelative = wall.CFrame:ToObjectSpace(root.CFrame)
local x = cfRelative.X
local y = cfRelative.Y
local newPosRelative = cfRelative.Position
local clampX = math.clamp(x, -0.5 * wall.Size.X, 0.5 * wall.Size.X)
local clampY = math.clamp(y, -0.5 * wall.Size.Y, 0.5 * wall.Size.Y)
-- fix X if not equal
if clampX ~= x then
moveVector = Vector3.new(0, moveVector.Y, 0)
newPosRelative = Vector3.new(clampX, newPosRelative.Y, newPosRelative.Z)
end
-- fix Y if not equal; you can remove this if you don't want to allow them to move up and down since their Y axis will remain the same
if clampY ~= y then
moveVector = Vector3.new(moveVector.X, 0, 0)
newPosRelative = Vector3.new(newPosRelative.X, clampY, newPosRelative.Z)
end
-- fix their CFrame if need be
if cfRelative.Position ~= newPosRelative then
root.CFrame = wall.CFrame:ToWorldSpace(CFrame.new(newPosRelative) * cfRelative.Rotation)
end
update()
end)