Help, I am sliding, something is wrong with my source engine movement.
Local Script:
-- SourceMovementController LocalScript
-- Handles player input and applies SourceMovement physics, with bhop fix, surfing, and crouch sliding
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local SourceMovement = require(game.ReplicatedStorage:WaitForChild("SourceMovement"))
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local rootPart = character:WaitForChild("HumanoidRootPart")
local state = {
vel = Vector3.new(),
onGround = true,
groundNormal = Vector3.new(0,1,0),
inWater = false,
onLadder = false,
crouching = false,
jumpQueued = false,
jumpHeld = false,
canJump = true,
surf = false,
crouchSliding = false,
crouchSlideTimer = 0,
fallVel = 0,
landed = false,
lastLandTime = 0,
prevCrouch = false,
prevMoveDir = Vector3.new(), -- Added for counter-strafe
-- For counter-strafe tap logic:
axisHoldStart = {X = 0, Z = 0}, -- When did we start holding a direction on each axis
axisLastDir = {X = 0, Z = 0}, -- Last nonzero direction on each axis
axisTapTime = {X = 0, Z = 0}, -- How long the opposite was held before release
wasOnGround = true, -- Track previous onGround state for bunnyhop/counter-strafe logic
-- Bunnyhop grace period
bhopGraceTimer = 0,
bhopShouldPreserve = false,
}
local input = {
moveDir = Vector3.new(),
jump = false,
crouch = false,
}
-- Input handling
local moveVector = Vector3.new()
local jumpPressed = false
local jumpPressedLast = false -- Track previous jump key state
local jumpConsumed = false -- Prevents holding space for auto-hop
local crouchPressed = false
UserInputService.InputBegan:Connect(function(inputObj, processed)
if processed then return end
if inputObj.KeyCode == Enum.KeyCode.Space then
jumpPressed = true
elseif inputObj.KeyCode == Enum.KeyCode.LeftControl or inputObj.KeyCode == Enum.KeyCode.C then
crouchPressed = true
end
end)
UserInputService.InputEnded:Connect(function(inputObj, processed)
if inputObj.KeyCode == Enum.KeyCode.Space then
jumpPressed = false
jumpConsumed = false -- Allow jump again after key is released
elseif inputObj.KeyCode == Enum.KeyCode.LeftControl or inputObj.KeyCode == Enum.KeyCode.C then
crouchPressed = false
end
end)
local function getMoveDirection()
local dir = Vector3.new()
if UserInputService:IsKeyDown(Enum.KeyCode.W) then dir = dir + Vector3.new(0,0,-1) end
if UserInputService:IsKeyDown(Enum.KeyCode.S) then dir = dir + Vector3.new(0,0,1) end
if UserInputService:IsKeyDown(Enum.KeyCode.A) then dir = dir + Vector3.new(-1,0,0) end
if UserInputService:IsKeyDown(Enum.KeyCode.D) then dir = dir + Vector3.new(1,0,0) end
-- Camera relative
local cam = workspace.CurrentCamera
if cam then
local cf = cam.CFrame
local move = (cf.LookVector * -dir.Z + cf.RightVector * dir.X)
move = Vector3.new(move.X, 0, move.Z)
if move.Magnitude > 0 then
return move.Unit
end
end
return Vector3.new()
end
-- Main movement loop
local lastGrounded = true
local lastY = rootPart.Position.Y
-- For velocity smoothing
local prevAppliedVelocity = Vector3.new()
local velocityLerpAlpha = 1 -- Smoothing factor (1 = instant, 0.5 = fast, 0.25 = slow)
-- Counter-strafe settings
local COUNTERSTRAFE_TAP_WINDOW = 0.18 -- seconds, max time for a tap to count as counter-strafe
RunService.RenderStepped:Connect(function(dt)
-- Update input
input.moveDir = getMoveDirection()
input.crouch = crouchPressed
-- Jump logic: Only allow jump on the frame the key is pressed (no hold-to-bhop)
if jumpPressed and not jumpPressedLast and not jumpConsumed then
input.jump = true
jumpConsumed = true -- Block further jumps until key is released
else
input.jump = false
end
jumpPressedLast = jumpPressed
-- Store previous move direction for counter-strafe
state.prevMoveDir = state.moveDir or Vector3.new()
state.moveDir = input.moveDir
-- Counter-strafe tap logic: Track axis hold times and tap durations
local now = tick()
for _, axis in {"X", "Z"} do
local prev = state.axisLastDir[axis] or 0
local curr = input.moveDir[axis]
-- If we just started holding a direction (from 0 to nonzero)
if math.abs(prev) < 0.01 and math.abs(curr) > 0.5 then
state.axisHoldStart[axis] = now
end
-- If we just released a direction (from nonzero to 0)
if math.abs(prev) > 0.5 and math.abs(curr) < 0.01 then
state.axisTapTime[axis] = now - (state.axisHoldStart[axis] or now)
end
state.axisLastDir[axis] = curr
end
-- Check ground
local ray = Ray.new(rootPart.Position, Vector3.new(0, -3, 0))
local hit, pos, norm = workspace:FindPartOnRay(ray, character)
state.onGround = hit ~= nil and norm.Y > 0.7
state.groundNormal = norm or Vector3.new(0,1,0)
-- Water/Ladder detection (simple placeholder)
state.inWater = false
state.onLadder = false
-- Fall velocity
state.fallVel = rootPart.Velocity.Y
-- Landing detection
state.landed = (not lastGrounded) and state.onGround
lastGrounded = state.onGround
-- Track previous onGround state for bunnyhop/counter-strafe logic
state.wasOnGround = state.wasOnGround or true
state.wasOnGround = state.onGround
-- Crouch slide logic
state.prevCrouch = state.crouching
state.crouching = input.crouch
SourceMovement:TryStartCrouchSlide(state, input)
-- Apply movement
state.vel = SourceMovement:Move(state, input, dt)
-- --- COUNTER-STRAFE INSTANT STOP LOGIC ---
-- If the player quickly switches direction (counter-strafe) on X or Z axis, and is on ground,
-- forcibly zero velocity on that axis for one frame to prevent sliding.
if state.onGround then
for _, axis in {"X", "Z"} do
local prev = state.prevMoveDir[axis]
local curr = input.moveDir[axis]
-- If the player just switched from moving in one direction to the opposite (counter-strafe)
if math.abs(prev) > 0.5 and math.abs(curr) > 0.5 and (prev * curr) < 0 then
-- Only trigger if the tap was quick enough (within COUNTERSTRAFE_TAP_WINDOW)
if state.axisTapTime[axis] > 0 and state.axisTapTime[axis] <= COUNTERSTRAFE_TAP_WINDOW then
-- Zero velocity on this axis
state.vel = Vector3.new(
axis == "X" and 0 or state.vel.X,
state.vel.Y,
axis == "Z" and 0 or state.vel.Z
)
-- Also forcibly zero rootPart velocity on this axis for this frame
rootPart.Velocity = Vector3.new(
axis == "X" and 0 or rootPart.Velocity.X,
rootPart.Velocity.Y,
axis == "Z" and 0 or rootPart.Velocity.Z
)
-- Reset tap time so it doesn't trigger again until next tap
state.axisTapTime[axis] = 0
end
end
end
end
-- --- INSTANT STOP OVERRIDE ---
-- If there is no input and the player is on the ground, forcibly zero velocity to prevent sliding.
if input.moveDir.Magnitude < 0.01 and state.onGround then
state.vel = Vector3.new(0, 0, 0)
rootPart.Velocity = Vector3.new(0, rootPart.Velocity.Y, 0)
prevAppliedVelocity = rootPart.Velocity
else
-- Instantly set velocity for snappy control (no smoothing)
local targetVel
if state.surf then
targetVel = Vector3.new(state.vel.X, 0, state.vel.Z)
else
targetVel = Vector3.new(state.vel.X, rootPart.Velocity.Y, state.vel.Z)
if input.jump and state.onGround and state.canJump then
targetVel = Vector3.new(state.vel.X, state.vel.Y, state.vel.Z)
end
end
-- Lerp previous velocity to target velocity for smoothness (set alpha=1 for instant)
prevAppliedVelocity = prevAppliedVelocity:Lerp(targetVel, velocityLerpAlpha)
rootPart.Velocity = prevAppliedVelocity
end
-- Crouch
if input.crouch then
humanoid.CameraOffset = Vector3.new(0, -2, 0)
else
humanoid.CameraOffset = Vector3.new(0, 0, 0)
end
-- Clamp speed for sanity
local flatVel = Vector3.new(rootPart.Velocity.X, 0, rootPart.Velocity.Z)
if flatVel.Magnitude > 60 then
local y = rootPart.Velocity.Y
rootPart.Velocity = flatVel.Unit * 60 + Vector3.new(0, y, 0)
end
end)
Module:
-- SourceMovement ModuleScript
-- Implements Source-engine-like movement physics for Roblox characters, with bhop, surfing, and crouch sliding
local SourceMovement = {}
-- Tweakable parameters
local WALK_SPEED = 16
local CROUCH_SPEED = 8
local JUMP_POWER = 50
local CROUCH_JUMP_BOOST = 1.2
local AIR_ACCEL = 34 -- Increased for more bhop speed gain
local GROUND_ACCEL = 64
local FRICTION = 8
local AIR_FRICTION = 0.5
local SLOPE_SLIDE_ANGLE = 40
local SLOPE_SLOW_ANGLE = 20
local SURF_MIN_ANGLE = 10
local SURF_MAX_ANGLE = 80
local SURF_FRICTION = 0.01
local SURF_AIR_ACCEL = 60
local SURF_MAX_SPEED = 60
local LANDING_PENALTY_VEL = 60
local LANDING_PENALTY_TIME = 0.4
local WATER_SPEED = 8
local LADDER_SPEED = 10
local MAX_AIR_SPEED = 32 -- Increased for more bhop speed gain
local CROUCH_SLIDE_SPEED = 30
local CROUCH_SLIDE_TIME = 0.5
local CROUCH_SLIDE_FRICTION = 1
local COUNTERSTRAFE_TAP_THRESHOLD = 0.15 -- seconds, must tap opposite key within this time to trigger instant stop
-- Bunnyhop grace period settings
local BHOP_GRACE_PERIOD = 0.12 -- seconds to jump after landing to keep velocity
local BHOP_MAX_MULTIPLIER = 1.25 -- max velocity multiplier allowed from bunnyhop
local function getSlopeAngle(normal)
if normal.Y == 0 then return 90 end
return math.deg(math.acos(normal.Y))
end
function SourceMovement:IsSurfing(groundNormal, onGround, onLadder, inWater)
if not onGround or onLadder or inWater then return false end
local angle = getSlopeAngle(groundNormal)
return angle > SURF_MIN_ANGLE and angle < SURF_MAX_ANGLE
end
function SourceMovement:ApplyFriction(vel, dt, onGround, frictionOverride)
local friction = frictionOverride or (onGround and FRICTION or AIR_FRICTION)
local speed = vel.Magnitude
if speed ~= 0 then
local drop = speed * friction * dt
local newSpeed = math.max(speed - drop, 0)
return vel.Unit * newSpeed
end
return vel
end
function SourceMovement:Accelerate(vel, wishDir, wishSpeed, accel, dt, maxSpeed)
local currentSpeed = vel:Dot(wishDir)
local addSpeed = wishSpeed - currentSpeed
if addSpeed <= 0 then return vel end
local accelSpeed = accel * dt * wishSpeed
if accelSpeed > addSpeed then accelSpeed = addSpeed end
local newVel = vel + wishDir * accelSpeed
if maxSpeed and newVel.Magnitude > maxSpeed then
newVel = newVel.Unit * maxSpeed
end
return newVel
end
function SourceMovement:HandleSlope(vel, groundNormal)
local angle = getSlopeAngle(groundNormal)
if angle > SLOPE_SLIDE_ANGLE then
-- Slide down steep slopes
local slideDir = Vector3.new(groundNormal.X, 0, groundNormal.Z)
if slideDir.Magnitude > 0 then
slideDir = slideDir.Unit
return vel + slideDir * (angle - SLOPE_SLIDE_ANGLE)
end
elseif angle > SLOPE_SLOW_ANGLE then
-- Slow uphill
return vel * 0.7
end
return vel
end
function SourceMovement:HandleLandingPenalty(vel, fallVel, landed, lastLandTime, now)
if landed and fallVel < -LANDING_PENALTY_VEL and (now - lastLandTime) > 0.1 then
return vel * 0.5, now
end
return vel, lastLandTime
end
-- Counter-strafe helper
local function counterStrafeVelocity(vel, moveDir, prevMoveDir, axisTapTime, canCounterStrafe)
local newVel = vel
if not canCounterStrafe then
return newVel
end
if prevMoveDir and moveDir then
-- Counter-strafe left/right (A/D)
if prevMoveDir.X > 0.5 and moveDir.X < -0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
newVel = Vector3.new(0, newVel.Y, newVel.Z)
elseif prevMoveDir.X < -0.5 and moveDir.X > 0.5 and math.abs(moveDir.X) > 0.5 and math.abs(prevMoveDir.X) > 0.5 then
newVel = Vector3.new(0, newVel.Y, newVel.Z)
end
-- Counter-strafe forward/back (W/S)
if prevMoveDir.Z > 0.5 and moveDir.Z < -0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
newVel = Vector3.new(newVel.X, newVel.Y, 0)
elseif prevMoveDir.Z < -0.5 and moveDir.Z > 0.5 and math.abs(moveDir.Z) > 0.5 and math.abs(prevMoveDir.Z) > 0.5 then
newVel = Vector3.new(newVel.X, newVel.Y, 0)
end
-- NEW: If you release the opposite key after counter-strafe (i.e. go from input to zero), stop instantly
-- Only if the opposite key was tapped (held for less than threshold)
-- X axis
if math.abs(prevMoveDir.X) > 0.5 and math.abs(moveDir.X) <= 0.01 then
if axisTapTime and axisTapTime.X and axisTapTime.X > 0 and axisTapTime.X <= COUNTERSTRAFE_TAP_THRESHOLD then
newVel = Vector3.new(0, newVel.Y, newVel.Z)
end
end
-- Z axis
if math.abs(prevMoveDir.Z) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
if axisTapTime and axisTapTime.Z and axisTapTime.Z > 0 and axisTapTime.Z <= COUNTERSTRAFE_TAP_THRESHOLD then
newVel = Vector3.new(newVel.X, newVel.Y, 0)
end
end
end
return newVel
end
function SourceMovement:Move(state, input, dt)
-- state: {vel, onGround, groundNormal, inWater, onLadder, crouching, jumpQueued, jumpHeld, canJump, surf, crouchSliding, crouchSlideTimer, fallVel, landed, lastLandTime, prevMoveDir, axisTapTime, wasOnGround, bhopGraceTimer, bhopShouldPreserve}
-- input: {moveDir, jump, crouch}
local vel = state.vel
local onGround = state.onGround
local groundNormal = state.groundNormal
local inWater = state.inWater
local onLadder = state.onLadder
local crouching = input.crouch
local jump = input.jump
local moveDir = input.moveDir
local dt = dt
local now = tick()
local surf = self:IsSurfing(groundNormal, onGround, onLadder, inWater)
state.surf = surf
-- Counter-strafe logic (only on ground and not bunnyhopping)
local canCounterStrafe = false
if onGround and state.wasOnGround then
canCounterStrafe = true
end
if canCounterStrafe then
vel = counterStrafeVelocity(vel, moveDir, state.prevMoveDir, state.axisTapTime, canCounterStrafe)
-- Reset axisTapTime after using it, so it only applies once per tap
if state.axisTapTime then
if math.abs(state.prevMoveDir and state.prevMoveDir.X or 0) > 0.5 and math.abs(moveDir.X) <= 0.01 then
state.axisTapTime.X = 0
end
if math.abs(state.prevMoveDir and state.prevMoveDir.Z or 0) > 0.5 and math.abs(moveDir.Z) <= 0.01 then
state.axisTapTime.Z = 0
end
end
end
-- Water/Ladder
if inWater then
vel = moveDir * WATER_SPEED
if jump then vel = vel + Vector3.new(0, JUMP_POWER * 0.5, 0) end
return vel
elseif onLadder then
vel = moveDir * LADDER_SPEED
if jump then vel = vel + Vector3.new(0, LADDER_SPEED, 0) end
return vel
end
-- Crouch sliding
if state.crouchSliding then
vel = self:ApplyFriction(vel, dt, true, CROUCH_SLIDE_FRICTION)
vel = self:Accelerate(vel, moveDir, CROUCH_SLIDE_SPEED, GROUND_ACCEL, dt, CROUCH_SLIDE_SPEED)
state.crouchSlideTimer = state.crouchSlideTimer - dt
if state.crouchSlideTimer <= 0 or not crouching then
state.crouchSliding = false
end
return vel
end
-- Surfing
if surf then
-- Frictionless, high air acceleration, can't jump
vel = self:ApplyFriction(vel, dt, true, SURF_FRICTION)
if moveDir.Magnitude > 0 then
vel = self:Accelerate(vel, moveDir.Unit, SURF_MAX_SPEED, SURF_AIR_ACCEL, dt, SURF_MAX_SPEED)
end
-- Project velocity onto surf plane
local surfNormal = groundNormal
vel = vel - surfNormal * vel:Dot(surfNormal)
return vel
end
-- Bunnyhop grace period logic
state.bhopGraceTimer = state.bhopGraceTimer or 0
state.bhopShouldPreserve = state.bhopShouldPreserve or false
-- If just landed, start grace period
if state.landed then
state.bhopGraceTimer = now + BHOP_GRACE_PERIOD
state.bhopShouldPreserve = false
end
-- Friction
vel = self:ApplyFriction(vel, dt, onGround)
-- Slope
if onGround then
vel = self:HandleSlope(vel, groundNormal)
end
-- Acceleration
local wishSpeed = crouching and CROUCH_SPEED or WALK_SPEED
local accel = onGround and GROUND_ACCEL or AIR_ACCEL
if not onGround then wishSpeed = math.min(wishSpeed, MAX_AIR_SPEED) end
if moveDir.Magnitude > 0 then
vel = self:Accelerate(vel, moveDir.Unit, wishSpeed, accel, dt, (onGround and nil or MAX_AIR_SPEED))
end
-- Jump (no auto-hop: must release and repress jump)
if jump and onGround and state.canJump then
-- Bunnyhop grace: If jumping within grace period, preserve velocity (up to cap)
if now <= (state.bhopGraceTimer or 0) then
state.bhopShouldPreserve = true
else
state.bhopShouldPreserve = false
end
-- Apply jump
vel = Vector3.new(vel.X, JUMP_POWER, vel.Z)
if crouching then
vel = vel * CROUCH_JUMP_BOOST
end
state.canJump = false
elseif not jump and onGround then
state.canJump = true
end
-- Air control (strafe jumping)
if not onGround and moveDir.Magnitude > 0 then
local airControl = 0.4 -- Slightly increased for more air control
local proj = vel:Dot(moveDir.Unit)
if proj < wishSpeed then
vel = vel + moveDir.Unit * AIR_ACCEL * dt * airControl
end
end
-- Inertia (momentum preserved)
-- Already handled by not resetting velocity
-- Landing penalty
vel, state.lastLandTime = self:HandleLandingPenalty(vel, state.fallVel, state.landed, state.lastLandTime, now)
-- Bunnyhop velocity cap and reset
if onGround then
local flatVel = Vector3.new(vel.X, 0, vel.Z)
local maxBhopSpeed = WALK_SPEED * BHOP_MAX_MULTIPLIER
if state.bhopShouldPreserve then
-- If bunnyhopped within grace period, preserve velocity but cap it
if flatVel.Magnitude > maxBhopSpeed then
flatVel = flatVel.Unit * maxBhopSpeed
vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
end
-- Reset flag so it only applies once per jump
state.bhopShouldPreserve = false
else
-- If landed normally (not bunnyhopping), reset velocity to max walking speed
if flatVel.Magnitude > WALK_SPEED then
flatVel = flatVel.Unit * WALK_SPEED
vel = Vector3.new(flatVel.X, vel.Y, flatVel.Z)
end
end
end
return vel
end
function SourceMovement:TryStartCrouchSlide(state, input)
-- Start crouch slide if moving fast, on ground, and just pressed crouch
if state.onGround and not state.crouchSliding and input.crouch and not state.prevCrouch and state.vel.Magnitude > WALK_SPEED * 1.2 then
state.crouchSliding = true
state.crouchSlideTimer = CROUCH_SLIDE_TIME
end
end
return SourceMovement
Video: