
KeylessUniversal

-- Wall Walk Controller MEJORADO V4 - LocalScript
-- Colocar en: StarterPlayer > StarterPlayerScripts
-- Tecla "-" activa/desactiva caminar por paredes.
-- V4 agrega:
-- 1) Auto-enganche al caminar hacia una pared.
-- 2) W sube por la pared cuando estas pegado.
-- 3) Estados internos: GROUND / APPROACHING_WALL / WALL_ATTACHED / CEILING_ATTACHED.
-- 4) Menos pelea con Humanoid: usa Humanoid:Move() + VectorForce suave.
-- 5) Limpieza completa al desactivar y soporte para respawn.
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local CollectionService = game:GetService("CollectionService")
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
--// CONFIGURACION
local WALL_WALK_ENABLED = false
local TOGGLE_KEY = Enum.KeyCode.Minus
local ONLY_TAGGED_SURFACES = false
local SURFACE_TAG = "WallWalk"
local RAY_DISTANCE = 7.5
local FRONT_ATTACH_DISTANCE = 5.25
local FLOOR_CHECK_DISTANCE = 5.5
local SURFACE_OFFSET = 2.7
local MOVE_SPEED = 18
local WALL_UP_SPEED = 18
local MOVE_ACCELERATION = 34
local NO_INPUT_DAMPING = 14
local MAX_MOVE_FORCE = 95000
local STICK_ACCELERATION = 50
local SOFT_STICK_SPEED = 2.4
local JUMP_POWER = 58
local ALIGN_RESPONSIVENESS = 48
local NORMAL_LERP_SPEED = 18
local FAST_ATTACH_LERP_SPEED = 32
local CAMERA_DISTANCE = 12
local CAMERA_HEIGHT = 4
local MOUSE_SENSITIVITY = 0.25
local CAMERA_SMOOTHNESS = 0.2
local AUTO_ATTACH_WHEN_WALKING = true
local DISABLE_WHEN_SURFACE_LOST_AFTER = 0.45
--// ESTADO INTERNO
local yaw = 0
local pitch = 0
local character
local humanoid
local root
local savedWalkSpeed
local savedJumpPower
local savedJumpHeight
local savedUseJumpPower
local attachment
local alignOrientation
local vectorForce
local currentNormal = Vector3.yAxis
local lastGoodNormal = Vector3.yAxis
local lastSurfaceResult = nil
local lostSurfaceTime = 0
local wallState = "GROUND"
local function safeUnit(v, fallback)
if typeof(v) == "Vector3" and v.Magnitude > 0.001 then
return v.Unit
end
return fallback or Vector3.yAxis
end
local function clampMagnitude(v, maxMagnitude)
if v.Magnitude > maxMagnitude then
return v.Unit * maxMagnitude
end
return v
end
local function getCharacter()
character = player.Character or player.CharacterAdded:Wait()
humanoid = character:WaitForChild("Humanoid")
root = character:WaitForChild("HumanoidRootPart")
return character, humanoid, root
end
local function getRaycastParams()
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = character and {character} or {}
params.IgnoreWater = true
return params
end
local function validSurface(result)
if not result then return false end
if not result.Instance then return false end
if not result.Instance.CanCollide then return false end
if ONLY_TAGGED_SURFACES then
return CollectionService:HasTag(result.Instance, SURFACE_TAG)
end
return true
end
local function raycast(origin, direction, distance)
local result = workspace:Raycast(origin, safeUnit(direction, -currentNormal) * distance, getRaycastParams())
if validSurface(result) then
return result
end
return nil
end
local function cleanupForces()
if alignOrientation then
alignOrientation:Destroy()
alignOrientation = nil
end
if vectorForce then
vectorForce:Destroy()
vectorForce = nil
end
if attachment then
attachment:Destroy()
attachment = nil
end
end
local function createForces()
if not root then return end
cleanupForces()
attachment = Instance.new("Attachment")
attachment.Name = "WallWalkAttachment"
attachment.Parent = root
alignOrientation = Instance.new("AlignOrientation")
alignOrientation.Name = "WallWalkAlignOrientation"
alignOrientation.Mode = Enum.OrientationAlignmentMode.OneAttachment
alignOrientation.Attachment0 = attachment
alignOrientation.Responsiveness = ALIGN_RESPONSIVENESS
alignOrientation.MaxTorque = math.huge
alignOrientation.RigidityEnabled = false
alignOrientation.Parent = root
vectorForce = Instance.new("VectorForce")
vectorForce.Name = "WallWalkMovementForce"
vectorForce.Attachment0 = attachment
vectorForce.RelativeTo = Enum.ActuatorRelativeTo.World
vectorForce.ApplyAtCenterOfMass = true
vectorForce.Force = Vector3.zero
vectorForce.Parent = root
end
local function getInputMoveVector()
local x = 0
local z = 0
if UserInputService:IsKeyDown(Enum.KeyCode.W) then z += 1 end
if UserInputService:IsKeyDown(Enum.KeyCode.S) then z -= 1 end
if UserInputService:IsKeyDown(Enum.KeyCode.D) then x += 1 end
if UserInputService:IsKeyDown(Enum.KeyCode.A) then x -= 1 end
local v = Vector3.new(x, 0, z)
if v.Magnitude > 1 then v = v.Unit end
return v
end
local function getPlanarCameraForward(upVector)
local forward = camera.CFrame.LookVector
forward = forward - upVector * forward:Dot(upVector)
if forward.Magnitude < 0.001 and root then
forward = root.CFrame.LookVector - upVector * root.CFrame.LookVector:Dot(upVector)
end
return safeUnit(forward, root and root.CFrame.LookVector or Vector3.zAxis)
end
local function getCameraRelativeMovement(upVector)
local input = getInputMoveVector()
if input.Magnitude <= 0 then return Vector3.zero end
local forward = getPlanarCameraForward(upVector)
local right = safeUnit(forward:Cross(upVector), root and root.CFrame.RightVector or Vector3.xAxis)
local moveDirection = forward * input.Z + right * input.X
moveDirection = moveDirection - upVector * moveDirection:Dot(upVector)
return safeUnit(moveDirection, Vector3.zero)
end
local function findFrontWall(moveDirection)
if not root or not camera then return nil end
if not AUTO_ATTACH_WHEN_WALKING then return nil end
local input = getInputMoveVector()
if input.Z <= 0 then return nil end -- solo engancha automaticamente cuando caminas hacia adelante
local origin = root.Position
local forward = moveDirection
if forward.Magnitude < 0.001 then
forward = getPlanarCameraForward(currentNormal)
end
local cf = root.CFrame
local origins = {
origin,
origin + currentNormal * 1.1,
origin - currentNormal * 0.5,
origin + cf.RightVector * 1.1,
origin - cf.RightVector * 1.1,
}
local best
local bestDistance = math.huge
for _, testOrigin in ipairs(origins) do
local result = raycast(testOrigin, forward, FRONT_ATTACH_DISTANCE)
if result then
local isWallLike = math.abs(result.Normal:Dot(Vector3.yAxis)) < 0.75
if isWallLike and result.Distance < bestDistance then
best = result
bestDistance = result.Distance
end
end
end
return best
end
local function findCurrentSurface(moveDirection)
if not root then return nil end
local origin = root.Position
local cf = root.CFrame
local directions = {
-currentNormal,
-lastGoodNormal,
-cf.UpVector,
Vector3.new(0, -1, 0),
Vector3.new(0, 1, 0),
cf.LookVector,
-cf.LookVector,
cf.RightVector,
-cf.RightVector,
}
if moveDirection and moveDirection.Magnitude > 0.001 then
table.insert(directions, moveDirection)
table.insert(directions, -moveDirection)
end
local origins = {
origin,
origin + cf.LookVector * 1.2,
origin - cf.LookVector * 1.2,
origin + cf.RightVector * 1.2,
origin - cf.RightVector * 1.2,
origin + currentNormal * 0.75,
origin - currentNormal * 0.75,
}
local bestResult
local bestScore = -math.huge
for _, testOrigin in ipairs(origins) do
for _, direction in ipairs(directions) do
local result = raycast(testOrigin, direction, RAY_DISTANCE)
if result then
local continuity = result.Normal:Dot(currentNormal) * 4
local distanceScore = -result.Distance * 0.65
local movementScore = 0
if moveDirection and moveDirection.Magnitude > 0.001 then
movementScore = math.abs(result.Normal:Dot(moveDirection)) * 0.5
end
local score = continuity + distanceScore + movementScore
if score > bestScore then
bestScore = score
bestResult = result
end
end
end
end
return bestResult
end
local function classifyState(normal)
local y = normal:Dot(Vector3.yAxis)
if y > 0.65 then
return "GROUND"
elseif y < -0.65 then
return "CEILING_ATTACHED"
else
return "WALL_ATTACHED"
end
end
local function getWallMovement(surfaceNormal)
local input = getInputMoveVector()
if input.Magnitude <= 0 then return Vector3.zero end
local upVector = surfaceNormal.Unit
local state = classifyState(upVector)
if state == "WALL_ATTACHED" then
-- En pared: W sube, S baja, A/D se mueve lateral.
local worldUpOnWall = Vector3.yAxis - upVector * Vector3.yAxis:Dot(upVector)
worldUpOnWall = safeUnit(worldUpOnWall, getPlanarCameraForward(upVector))
local side = safeUnit(worldUpOnWall:Cross(upVector), root and root.CFrame.RightVector or Vector3.xAxis)
local move = worldUpOnWall * input.Z + side * input.X
return safeUnit(move, Vector3.zero)
end
-- En piso/techo: movimiento relativo a camara.
return getCameraRelativeMovement(upVector)
end
local function getSurfaceBasis(upVector, moveDirection)
local forward
if moveDirection and moveDirection.Magnitude > 0.001 then
forward = moveDirection - upVector * moveDirection:Dot(upVector)
else
forward = camera.CFrame.LookVector - upVector * camera.CFrame.LookVector:Dot(upVector)
end
if forward.Magnitude < 0.001 and root then
forward = root.CFrame.LookVector - upVector * root.CFrame.LookVector:Dot(upVector)
end
forward = safeUnit(forward, root and root.CFrame.LookVector or Vector3.zAxis)
local right = safeUnit(forward:Cross(upVector), root and root.CFrame.RightVector or Vector3.xAxis)
forward = safeUnit(upVector:Cross(right), root and root.CFrame.LookVector or Vector3.zAxis)
return right, upVector, -forward
end
local function restoreHumanoid()
if not humanoid then return end
humanoid.AutoRotate = true
humanoid.PlatformStand = false
if savedWalkSpeed then humanoid.WalkSpeed = savedWalkSpeed end
if savedUseJumpPower ~= nil then
humanoid.UseJumpPower = savedUseJumpPower
end
if savedJumpPower then humanoid.JumpPower = savedJumpPower end
if savedJumpHeight then humanoid.JumpHeight = savedJumpHeight end
pcall(function()
humanoid:ChangeState(Enum.HumanoidStateType.Running)
end)
end
local function disableInternally(reason)
WALL_WALK_ENABLED = false
restoreHumanoid()
cleanupForces()
camera = workspace.CurrentCamera
if camera then camera.CameraType = Enum.CameraType.Custom end
UserInputService.MouseBehavior = Enum.MouseBehavior.Default
UserInputService.MouseIconEnabled = true
currentNormal = Vector3.yAxis
lastGoodNormal = Vector3.yAxis
lastSurfaceResult = nil
lostSurfaceTime = 0
wallState = "GROUND"
print(reason and ("[WALL WALK] Desactivado: " .. reason) or "[WALL WALK] Desactivado")
end
local function applyWallWalk(dt)
if not WALL_WALK_ENABLED then return end
if not character or not humanoid or not root or not root.Parent then
getCharacter()
createForces()
end
camera = workspace.CurrentCamera
if not camera then return end
local preliminaryMove = getCameraRelativeMovement(currentNormal)
local frontWall = findFrontWall(preliminaryMove)
local result = frontWall or findCurrentSurface(preliminaryMove)
if result then
lastSurfaceResult = result
lostSurfaceTime = 0
local targetNormal = result.Normal.Unit
local speed = frontWall and FAST_ATTACH_LERP_SPEED or NORMAL_LERP_SPEED
local maxAlpha = frontWall and 0.85 or 0.45
local alpha = math.clamp(dt * speed, 0, maxAlpha)
currentNormal = safeUnit(currentNormal:Lerp(targetNormal, alpha), targetNormal)
lastGoodNormal = currentNormal
wallState = frontWall and "APPROACHING_WALL" or classifyState(currentNormal)
else
lostSurfaceTime += dt
if lostSurfaceTime < DISABLE_WHEN_SURFACE_LOST_AFTER then
currentNormal = lastGoodNormal
result = lastSurfaceResult
else
disableInternally("no encontro superficie")
return
end
end
local upVector = currentNormal.Unit
local moveDirection = getWallMovement(upVector)
if wallState == "APPROACHING_WALL" then
wallState = classifyState(upVector)
end
humanoid.AutoRotate = false
humanoid.PlatformStand = false
humanoid.WalkSpeed = MOVE_SPEED
humanoid:Move(moveDirection, false)
local right, up, back = getSurfaceBasis(upVector, moveDirection)
if alignOrientation then
alignOrientation.CFrame = CFrame.fromMatrix(root.Position, right, up, back)
end
local mass = root.AssemblyMass
local velocity = root.AssemblyLinearVelocity
local normalVelocity = upVector * velocity:Dot(upVector)
local tangentialVelocity = velocity - normalVelocity
local targetSpeed = wallState == "WALL_ATTACHED" and WALL_UP_SPEED or MOVE_SPEED
local desiredTangentialVelocity = moveDirection * targetSpeed
local velocityError
if moveDirection.Magnitude > 0.001 then
velocityError = desiredTangentialVelocity - tangentialVelocity
else
velocityError = -tangentialVelocity
end
local accel = moveDirection.Magnitude > 0.001 and MOVE_ACCELERATION or NO_INPUT_DAMPING
local moveForce = clampMagnitude(velocityError * mass * accel, MAX_MOVE_FORCE)
local antiGravity = Vector3.new(0, workspace.Gravity * mass, 0)
local stickForce = -upVector * STICK_ACCELERATION * mass
local towardSurfaceSpeed = -velocity:Dot(upVector)
if towardSurfaceSpeed > SOFT_STICK_SPEED then
stickForce *= 0.35
end
if vectorForce then
vectorForce.Force = antiGravity + stickForce + moveForce
end
if result then
local minDistance = SURFACE_OFFSET * 0.52
local maxDistance = SURFACE_OFFSET * 1.85
if result.Distance < minDistance then
root.CFrame = root.CFrame + upVector * ((minDistance - result.Distance) * 0.14)
elseif result.Distance > maxDistance then
root.CFrame = root.CFrame - upVector * ((result.Distance - maxDistance) * 0.16)
end
end
end
local function updateCamera(dt)
if not WALL_WALK_ENABLED then return end
if not root or not camera then return end
local upVector = currentNormal.Unit
local rotacion = CFrame.Angles(0, math.rad(yaw), 0) * CFrame.Angles(math.rad(pitch), 0, 0)
local offset = rotacion:VectorToWorldSpace(Vector3.new(0, CAMERA_HEIGHT, CAMERA_DISTANCE))
local targetPosition = root.Position + offset
local targetLookAt = root.Position + upVector * 2
local targetCFrame = CFrame.new(targetPosition, targetLookAt)
if camera.CameraType ~= Enum.CameraType.Scriptable then
camera.CameraType = Enum.CameraType.Scriptable
end
camera.CFrame = camera.CFrame:Lerp(targetCFrame, math.clamp(CAMERA_SMOOTHNESS * 60 * dt, 0, 1))
end
local function activateWallWalk()
if WALL_WALK_ENABLED then return end
getCharacter()
savedWalkSpeed = humanoid.WalkSpeed
savedJumpPower = humanoid.JumpPower
savedJumpHeight = humanoid.JumpHeight
savedUseJumpPower = humanoid.UseJumpPower
createForces()
WALL_WALK_ENABLED = true
currentNormal = Vector3.yAxis
lastGoodNormal = Vector3.yAxis
lastSurfaceResult = nil
lostSurfaceTime = 0
wallState = "GROUND"
humanoid.PlatformStand = false
humanoid.AutoRotate = false
humanoid.WalkSpeed = MOVE_SPEED
camera = workspace.CurrentCamera
if camera then camera.CameraType = Enum.CameraType.Scriptable end
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
UserInputService.MouseIconEnabled = false
print("[WALL WALK] Activado")
end
local function deactivateWallWalk()
if not WALL_WALK_ENABLED then return end
disableInternally()
end
local function toggleWallWalk()
if WALL_WALK_ENABLED then
deactivateWallWalk()
else
activateWallWalk()
end
end
UserInputService.InputBegan:Connect(function(input, processed)
if processed then return end
if input.KeyCode == TOGGLE_KEY then
toggleWallWalk()
return
end
if WALL_WALK_ENABLED and input.KeyCode == Enum.KeyCode.Space and root then
local upVector = currentNormal.Unit
local velocity = root.AssemblyLinearVelocity
local tangentialVelocity = velocity - upVector * velocity:Dot(upVector)
root.AssemblyLinearVelocity = tangentialVelocity + upVector * JUMP_POWER
if vectorForce then
vectorForce.Force = Vector3.zero
end
end
end)
UserInputService.InputChanged:Connect(function(input)
if not WALL_WALK_ENABLED then return end
if input.UserInputType == Enum.UserInputType.MouseMovement then
yaw -= input.Delta.X * MOUSE_SENSITIVITY
pitch -= input.Delta.Y * MOUSE_SENSITIVITY
pitch = math.clamp(pitch, -75, 75)
end
end)
RunService.RenderStepped:Connect(function(dt)
if WALL_WALK_ENABLED then
applyWallWalk(dt)
updateCamera(dt)
end
end)
player.CharacterAdded:Connect(function()
task.wait(1)
character = nil
humanoid = nil
root = nil
cleanupForces()
if WALL_WALK_ENABLED then
getCharacter()
createForces()
end
end)
print("[WALL WALK] Script V4 cargado. Presiona '-' para activar/desactivar.")No comments yet for this script.



