How to stop velocity


I have some bugs with conflicting key input (W+S , W+D, W+A) after letting go server keeps applying velocity . I tried with remote events but , it worked of course but that’s bad practise i think, too many requests will be sent. Ideas?

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local HoverbikeConfig = require(ReplicatedStorage:WaitForChild("HoverbikeConfig"))
local HoverbikeLights = require(script.Parent:WaitForChild("HoverbikeLights"))

local remoteEvents = Instance.new("Folder")
remoteEvents.Name = "HoverbikeEvents"
remoteEvents.Parent = ReplicatedStorage

local mountBikeEvent = Instance.new("RemoteEvent")
mountBikeEvent.Name = "MountBike"
mountBikeEvent.Parent = remoteEvents

local dismountBikeEvent = Instance.new("RemoteEvent")
dismountBikeEvent.Name = "DismountBike"
dismountBikeEvent.Parent = remoteEvents

local updateControlsEvent = Instance.new("RemoteEvent")
updateControlsEvent.Name = "UpdateControls"
updateControlsEvent.Parent = remoteEvents

local hoverbike = workspace:WaitForChild("Hoverbike")
local vehicleSeat = hoverbike:WaitForChild("VehicleSeat") or hoverbike:WaitForChild("Seat")

local proximityPrompt = vehicleSeat:FindFirstChild("ProximityPrompt") or hoverbike:FindFirstChild("ProximityPrompt")

if not proximityPrompt then
	warn("ProximityPrompt not found! Place it either under Hoverbike or under VehicleSeat")
	return
end

local hoverSound = vehicleSeat:FindFirstChild("HoverSound") or hoverbike:FindFirstChild("HoverSound")
if not hoverSound then
	warn("HoverSound not found! Create a Sound object named 'HoverSound' under VehicleSeat or Hoverbike and set its SoundId in Studio")
else
	hoverSound.Looped = true
	if hoverSound.SoundId == "" then
		warn("HoverSound found but no SoundId set! Set the SoundId in Studio Properties panel")
	end
end

local bikeData = {
	rider = nil,
	isActive = false,
	boostCooldown = 0,
	targetHeight = 0,
	groundHeight = 0,
	hoverStarted = false,
	currentSpeed = 0,
	isMoving = false,
	landingMode = false,
	isAscending = false,
	isDescending = false,
	isRotating = false,
	lastRotationState = false,
	lastStabilizationUpdate = 0,
	lastGroundCheck = 0,
	bodyVelocity = nil,
	bodyPosition = nil,
	bodyAngularVelocity = nil,
	bodyGyro = nil,
	originalOrientation = nil,
	idleBobOffset = 0,
	isIdle = false
}

local lastControlUpdate = {}
local CONTROL_UPDATE_COOLDOWN = 0.05

local function findGroundHeight(position)
	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
	raycastParams.FilterDescendantsInstances = {hoverbike}
	local rayOrigin = position
	local rayDirection = Vector3.new(0, -HoverbikeConfig.raycastDistance, 0)
	local raycastResult = workspace:Raycast(rayOrigin, rayDirection, raycastParams)
	if raycastResult then
		return raycastResult.Position.Y
	else
		return position.Y - HoverbikeConfig.raycastDistance
	end
end

local function setupHoverbike()
	if not vehicleSeat then
		warn("Hoverbike missing VehicleSeat or Seat")
		return false
	end

	local initialPosition = vehicleSeat.CFrame.Position
	bikeData.groundHeight = findGroundHeight(initialPosition)
	bikeData.originalOrientation = vehicleSeat.CFrame

	local bodyVelocity = Instance.new("BodyVelocity")
	bodyVelocity.MaxForce = Vector3.new(10000, 10000, 10000)
	bodyVelocity.Velocity = Vector3.new(0, 0, 0)
	bodyVelocity.Parent = vehicleSeat

	local bodyPosition = Instance.new("BodyPosition")
	bodyPosition.MaxForce = Vector3.new(0, 0, 0)
	bodyPosition.Position = initialPosition
	bodyPosition.D = 3000
	bodyPosition.P = 50000
	bodyPosition.Parent = vehicleSeat

	local bodyAngularVelocity = Instance.new("BodyAngularVelocity")
	bodyAngularVelocity.MaxTorque = Vector3.new(50000, 50000, 50000)
	bodyAngularVelocity.AngularVelocity = Vector3.new(0, 0, 0)
	bodyAngularVelocity.Parent = vehicleSeat

	local bodyGyro = Instance.new("BodyGyro")
	bodyGyro.MaxTorque = Vector3.new(40000, 0, 40000)
	bodyGyro.P = 3000
	bodyGyro.D = 500
	bodyGyro.CFrame = bikeData.originalOrientation
	bodyGyro.Parent = vehicleSeat

	bikeData.bodyVelocity = bodyVelocity
	bikeData.bodyPosition = bodyPosition
	bikeData.bodyAngularVelocity = bodyAngularVelocity
	bikeData.bodyGyro = bodyGyro

	if vehicleSeat.ClassName == "VehicleSeat" then
		vehicleSeat.MaxSpeed = 0
		vehicleSeat.Torque = 0
		vehicleSeat.TurnSpeed = 0
		vehicleSeat.Disabled = false
	end

	vehicleSeat.Anchored = false
	vehicleSeat.CanCollide = true

	return true
end

local function canPlayerControlBike(player)
	if bikeData.rider ~= player then return false end
	if not vehicleSeat.Occupant or vehicleSeat.Occupant ~= player.Character.Humanoid then
		return false
	end
	return true
end

local function mountBike(player)
	local character = player.Character
	if not character then return end

	local humanoid = character:FindFirstChild("Humanoid")
	if not humanoid then return end

	if vehicleSeat.Occupant and vehicleSeat.Occupant.Parent ~= character then
		return
	end

	if not vehicleSeat.Occupant then
		vehicleSeat:Sit(humanoid)
		task.wait(0.2)
	end

	if vehicleSeat.Occupant == humanoid then
		bikeData.rider = player
		bikeData.isActive = false
		proximityPrompt.Enabled = false

		if hoverSound then
			hoverSound:Play()
		end

		HoverbikeLights.CancelDelayedShutdown()
		HoverbikeLights.TurnOnAllEffects()

		local currentPosition = vehicleSeat.CFrame.Position
		bikeData.groundHeight = findGroundHeight(currentPosition)
		bikeData.targetHeight = math.max(bikeData.groundHeight + HoverbikeConfig.minHoverHeight, currentPosition.Y + HoverbikeConfig.hoverHeight)

		bikeData.bodyPosition.MaxForce = Vector3.new(0, 100000, 0)
		bikeData.bodyPosition.Position = Vector3.new(currentPosition.X, bikeData.targetHeight, currentPosition.Z)

		local _, currentYaw, _ = vehicleSeat.CFrame:ToEulerAnglesYXZ()
		bikeData.bodyGyro.CFrame = CFrame.new(vehicleSeat.Position) * CFrame.Angles(0, currentYaw, 0)

		bikeData.bodyVelocity.Velocity = Vector3.new(0, 20, 0)
		task.delay(1, function()
			if bikeData.bodyVelocity then
				bikeData.bodyVelocity.Velocity = Vector3.new(0, 0, 0)
			end
		end)

		bikeData.isActive = true
		bikeData.hoverStarted = true
		bikeData.isIdle = true
	end
end

local function dismountBike(player)
	if vehicleSeat.Occupant then
		vehicleSeat.Occupant.Jump = true
	end

	proximityPrompt.Enabled = true

	if hoverSound then
		hoverSound:Stop()
	end

	HoverbikeLights.TurnOffAllEffectsWithSmartDelay(vehicleSeat, bikeData.groundHeight)

	bikeData.rider = nil
	bikeData.isActive = false
	bikeData.isMoving = false
	bikeData.isIdle = false
	bikeData.isAscending = false
	bikeData.isDescending = false
	bikeData.isRotating = false
	bikeData.currentSpeed = 0
	bikeData.landingMode = true

	local currentPosition = vehicleSeat.CFrame.Position
	bikeData.groundHeight = findGroundHeight(currentPosition)

	if bikeData.bodyVelocity then
		bikeData.bodyVelocity.Velocity = Vector3.new(0, 0, 0)
	end
	if bikeData.bodyAngularVelocity then
		bikeData.bodyAngularVelocity.AngularVelocity = Vector3.new(0, 0, 0)
	end
end

local function updateBikeControls(player, controls)
	if not canPlayerControlBike(player) then return end

	local now = tick()
	if lastControlUpdate[player] and now - lastControlUpdate[player] < CONTROL_UPDATE_COOLDOWN then
		return
	end
	lastControlUpdate[player] = now

	if not bikeData.isActive then return end

	local moveVector = Vector3.new(controls.X or 0, 0, controls.Z or 0)
	local boost = controls.boost or false
	bikeData.isAscending = controls.ascending or false
	bikeData.isDescending = controls.descending or false

	local wasRotating = bikeData.isRotating
	if controls.X and controls.X > 0 then
		bikeData.bodyAngularVelocity.AngularVelocity = Vector3.new(0, HoverbikeConfig.rotationSpeed, 0)
		bikeData.bodyAngularVelocity.MaxTorque = Vector3.new(0, 100000, 0)
		bikeData.isRotating = true
	elseif controls.X and controls.X < 0 then
		bikeData.bodyAngularVelocity.AngularVelocity = Vector3.new(0, -HoverbikeConfig.rotationSpeed, 0)
		bikeData.bodyAngularVelocity.MaxTorque = Vector3.new(0, 100000, 0)
		bikeData.isRotating = true
	else
		bikeData.bodyAngularVelocity.AngularVelocity = Vector3.new(0, 0, 0)
		bikeData.bodyAngularVelocity.MaxTorque = Vector3.new(50000, 50000, 50000)
		bikeData.isRotating = false
	end
	bikeData.lastRotationState = wasRotating

	local hasInput = moveVector.Magnitude > 0 or bikeData.isAscending or bikeData.isDescending or bikeData.isRotating
	bikeData.isIdle = not hasInput

	local currentBoostMultiplier = 1
	if boost and bikeData.boostCooldown <= 0 then
		currentBoostMultiplier = HoverbikeConfig.boostMultiplier
		bikeData.boostCooldown = HoverbikeConfig.boostCooldown
	end

	bikeData.currentSpeed = moveVector.Z * HoverbikeConfig.maxSpeed * currentBoostMultiplier
	bikeData.isMoving = moveVector.Z ~= 0
end

local function updatePhysics(deltaTime)
	if bikeData.boostCooldown > 0 then
		bikeData.boostCooldown = math.max(0, bikeData.boostCooldown - deltaTime)
	end

	if bikeData.landingMode then
		local currentY = vehicleSeat.Position.Y
		local groundY = bikeData.groundHeight
		if currentY > groundY + 1 then
			bikeData.targetHeight = bikeData.targetHeight - (10 * deltaTime)
			bikeData.bodyPosition.Position = Vector3.new(vehicleSeat.Position.X, bikeData.targetHeight, vehicleSeat.Position.Z)
		else
			bikeData.bodyPosition.MaxForce = Vector3.new(0, 0, 0)
			bikeData.bodyGyro.MaxTorque = Vector3.new(0, 0, 0)
			bikeData.landingMode = false
			bikeData.hoverStarted = false
		end
	end

	if bikeData.isActive and bikeData.hoverStarted then
		local currentTime = tick()
		if (currentTime - bikeData.lastGroundCheck) >= HoverbikeConfig.groundCheckRate then
			bikeData.lastGroundCheck = currentTime
			bikeData.groundHeight = findGroundHeight(vehicleSeat.Position)
		end

		if (currentTime - bikeData.lastStabilizationUpdate) >= HoverbikeConfig.stabilizationUpdateRate or bikeData.isRotating ~= bikeData.lastRotationState then
			bikeData.lastStabilizationUpdate = currentTime
			if not bikeData.isRotating then
				local currentCFrame = vehicleSeat.CFrame
				local _, currentYaw, _ = currentCFrame:ToEulerAnglesYXZ()
				bikeData.bodyGyro.CFrame = CFrame.new(vehicleSeat.Position) * CFrame.Angles(0, currentYaw, 0)
				bikeData.bodyGyro.MaxTorque = Vector3.new(40000, 0, 40000)
				bikeData.bodyGyro.P = 3000
				bikeData.bodyGyro.D = 500
			else
				bikeData.bodyGyro.MaxTorque = Vector3.new(0, 0, 0)
			end
		end

		if bikeData.isAscending then
			bikeData.targetHeight = bikeData.targetHeight + (HoverbikeConfig.ascendSpeed * deltaTime)
		elseif bikeData.isDescending then
			local minHeight = bikeData.groundHeight + HoverbikeConfig.minHoverHeight
			bikeData.targetHeight = math.max(minHeight, bikeData.targetHeight - (HoverbikeConfig.descendSpeed * deltaTime))
		end
	end

	if bikeData.hoverStarted then
		local bobOffset = 0
		if bikeData.isIdle then
			bobOffset = math.sin(tick() * HoverbikeConfig.idleBobSpeed) * HoverbikeConfig.idleBobAmplitude
		else
			bobOffset = math.sin(tick() * 3) * 0.3
		end
		bikeData.bodyPosition.Position = Vector3.new(vehicleSeat.Position.X, bikeData.targetHeight + bobOffset, vehicleSeat.Position.Z)
		bikeData.bodyPosition.MaxForce = Vector3.new(0, 100000, 0)
	end

	if bikeData.isActive then
		local heightDiff = bikeData.targetHeight - vehicleSeat.Position.Y
		local verticalVelocity = math.clamp(heightDiff * 5, -30, 30)
		if bikeData.isMoving then
			local bikeCFrame = vehicleSeat.CFrame
			if math.abs(bikeData.currentSpeed) > 100 then
				local moveDistance = (bikeData.currentSpeed * deltaTime)
				local newPosition = vehicleSeat.Position + (bikeCFrame.LookVector * moveDistance)
				bikeData.bodyPosition.MaxForce = Vector3.new(100000, 100000, 100000)
				bikeData.bodyPosition.Position = Vector3.new(newPosition.X, bikeData.targetHeight, newPosition.Z)
				bikeData.bodyPosition.D = 10000
				bikeData.bodyPosition.P = 100000
				bikeData.bodyVelocity.MaxForce = Vector3.new(0, 0, 0)
			else
				local desiredVelocity = bikeCFrame.LookVector * (bikeData.currentSpeed or 0)
				bikeData.bodyVelocity.Velocity = Vector3.new(desiredVelocity.X, verticalVelocity, desiredVelocity.Z)
				bikeData.bodyVelocity.MaxForce = Vector3.new(50000, 10000, 50000)
				bikeData.bodyPosition.MaxForce = Vector3.new(0, 100000, 0)
			end
		else
			bikeData.bodyVelocity.Velocity = Vector3.new(0, verticalVelocity, 0)
			bikeData.bodyVelocity.MaxForce = Vector3.new(10000, 10000, 10000)
			bikeData.bodyPosition.MaxForce = Vector3.new(0, 100000, 0)
		end
	end
end

vehicleSeat:GetPropertyChangedSignal("Occupant"):Connect(function()
	local occupant = vehicleSeat.Occupant
	if occupant then
		local character = occupant.Parent
		local player = Players:GetPlayerFromCharacter(character)
		if player and bikeData.rider ~= player then
			mountBike(player)
		end
	else
		if bikeData.rider then
			dismountBike(bikeData.rider)
		end
	end
end)

mountBikeEvent.OnServerEvent:Connect(function(player)
	local character = player.Character
	if character and character.PrimaryPart then
		local bikePosition = hoverbike.PrimaryPart and hoverbike.PrimaryPart.Position or vehicleSeat.Position
		local distance = (character.PrimaryPart.Position - bikePosition).Magnitude
		if distance > 10 then return end
	end
	mountBike(player)
end)

dismountBikeEvent.OnServerEvent:Connect(function(player)
	dismountBike(player)
end)

updateControlsEvent.OnServerEvent:Connect(function(player, controls)
	if typeof(controls) ~= "table" then return end
	updateBikeControls(player, controls)
end)

RunService.Heartbeat:Connect(updatePhysics)

task.spawn(function()
	while true do
		task.wait(5)
		if bikeData.rider then
			local rider = bikeData.rider
			if not rider.Parent or not rider.Character or 
				not vehicleSeat.Occupant or 
				vehicleSeat.Occupant.Parent ~= rider.Character then
				dismountBike(rider)
			end
		end
	end
end)

Players.PlayerRemoving:Connect(function(player)
	if bikeData.rider == player then
		dismountBike(player)
	end
	lastControlUpdate[player] = nil
end)

setupHoverbike()
local lightsInitialized = HoverbikeLights.Initialize(hoverbike)
if not lightsInitialized then
	warn("Hoverbike lights system failed to initialize - effects may not work")
end

You should try making your hover bike moves client sided, That way the movement will still be smooth even if you are experiencing a small ping spikes.

The reason your hoverbike keeps applying velocity after you release keys is because the server only reacts to the last input it received from the client.

When a key is released, the server has no way of knowing that the input stopped, so it keeps using the old value. Thats why conflicting keys like W+S or W+A cause the bike to “stick” or move weirdly…


A better way to handle it is to use a state table for each players input. The client can track which keys are currently pressed in a table of booleans and it only fires UpdateControls when the state changes instead of every frame.

local controls = {
	forward = false,
	backward = false,
	left = false,
	right = false,
	boost = false,
	ascend = false,
	descend = false
}

-- example of updating when keys change
UserInputService.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.W then
		controls.forward = true
		updateControlsEvent:FireServer(controls)
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.W then
		controls.forward = false
		updateControlsEvent:FireServer(controls)
	end
end)

On the server you store that table and apply physics each Heartbeat tick based on the current state. Because its a table of booleans, conflicting keys cancel naturally… for example; pressing W+S will result in a zero forward/backward vector, so the bike wont move in that direction.

local moveVector = Vector3.new(
	(controls.right and 1 or 0) - (controls.left and 1 or 0),
	0,
	(controls.forward and 1 or 0) - (controls.backward and 1 or 0)
)

moveVector = moveVector.Unit * HoverbikeConfig.maxSpeed * (controls.boost and HoverbikeConfig.boostMultiplier or 1)
bikeData.bodyVelocity.Velocity = vehicleSeat.CFrame:VectorToWorldSpace(moveVector)

Then in your physics update you just calculate the move vector from the booleans and convert it to world space. This approach keeps the movement smooth, prevents stuck velocity and drastically reduces RemoteEvent spam because the client only sends input when something changes.

It also keeps boosts, ascend/descend, and rotation working without conflicting with the input system!


This system is basically how most Roblox vehicle controllers handle input: the client tells the server only when input changes, and the server uses the current state to calculate everything per frame.

Hope this helps!!