walk on walls
Key RequiredUniversal

walk on walls

Universal ScriptLeo777Leo777 156 views 1.7K executesUpdated Jul 6, 2026
Unrated
00
Lua Script•617 lines (16711 chars)
-- 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.")

Features & Information

Key ( - ) OFF / ON

Community Comments (0)

No comments yet for this script.

More Scripts for Universal Scripts