Viewmodel bopping glitches when I stop moving

  1. What do you want to achieve?
    I wanted to create a viewmodel bopping system that I was satisfied with. I tried using a devforum post to create it, but there were a few bugs that I didn’t know how to fix and I wasn’t satisfied. I experimented with the old script and made new code based on a gun kit I found.

  2. What is the issue?
    Whenever the player stops moving, the viewmodel glitches out a little.
    robloxapp-20231108-1719354.wmv (2.0 MB)

Viewmodel I’m using:
fpa.obj (16.4 KB)

  1. What solutions have you tried so far?
    I compared the viewmodel script from the gunkit to my script, and didn’t see anything wrong.
local RunService 				= game:GetService("RunService")
local ReplicatedStorage		    = game:GetService("ReplicatedStorage")
local UserInputService			= game:GetService("UserInputService")

local PlayerEvents				= ReplicatedStorage:WaitForChild("PlayerEvents")
local LookUpDown				= PlayerEvents:WaitForChild("LookUpDown")

local Player					= game.Players.LocalPlayer
local mouse 					= Player:GetMouse()
local Camera 					= workspace.CurrentCamera

local Arms 						= ReplicatedStorage:WaitForChild("FirstPersonArms"):Clone()
Arms.Humanoid.PlatformStand 	= true
Arms.Parent 					= Player

local ClientModules				= ReplicatedStorage:WaitForChild("ClientModules")
local SpringModule				= require(ClientModules:WaitForChild("SpringModule"))

local primaryPart: Part			= nil
local humanoid: Humanoid		= nil

local deltaX = 0
local deltaY = 0

local breathScale = 0.01
local breathe = 0

local swayX = 0
local swayY = 0

local tiltZ = 0
local tiltY = 0

local currentAngularShake = CFrame.Angles(0,0,0)

local startedRunning = 0

local runDebounce				= false
local runDt						= 0
local cameraOffset 				= CFrame.new(0, -1, 1.5)

local prevCamCframe				= Camera.CFrame
local swayCframe				= CFrame.new()

local swaySpring				= SpringModule.new()
local bobSpring					= SpringModule.new()

local targets							= {}

local function lerp(num, goal, i)
	return num + (goal - num) * i
end

local function Bob(addition)
	return math.sin(tick() * addition * 1.3) * 0.25
end

local function makeNewTarget()
	local Target						= Instance.new("Part")
	Target.Name 						= "Target"
	Target.Anchored						= true
	Target.CanCollide					= false
	Target.CanQuery 					= false
	Target.Transparency					= 1
	Target.Size 						= Vector3.one

	return Target
end

local function setupIkControl(character: Model, looker: Player)
	local head: Part = character:WaitForChild("Head")
	local uppertorso: Part = character:WaitForChild("UpperTorso")

	if not head then
		return nil
	end

	local humanoid = character:WaitForChild("Humanoid")
	local IkControl: IKControl = humanoid:WaitForChild("LookUpDown")

	if not humanoid then
		return
	end

	IkControl.Target = targets[looker]
	IkControl.EndEffector = head
	IkControl.Type = Enum.IKControlType.LookAt
	IkControl.ChainRoot = uppertorso

	return humanoid
end


local function update()
	if runDebounce then
		return
	end
	
	runDebounce = true
	LookUpDown:FireServer(mouse.Hit.Position, Camera.CFrame)
	
	task.wait(.1 - runDt)
	runDebounce = false
end

local function setArmsToCam(dt: number)
	if not primaryPart then
		return
	end
	
	local mouseDelta = UserInputService:GetMouseDelta()
	runDt = dt
	
	deltaX = lerp(deltaX, mouseDelta.X*0.001, .2)
	deltaY = lerp(deltaY, mouseDelta.Y*0.001, .2)
	
	local plrVelocity = primaryPart.AssemblyLinearVelocity
	
	local velocityXZ = plrVelocity*Vector3.new(1, 0, 1)
	if velocityXZ.Magnitude > .1 then
		if not startedRunning then
			startedRunning = tick()
		end

		local swayScaleX = 0.04
		local swayScaleY = 0.08

		local swaySpeed = velocityXZ.Magnitude/1.8
		local timeRunning = tick()-startedRunning

		swayX = math.sin(timeRunning*swaySpeed)*swayScaleX
		swayY = math.abs(math.cos(timeRunning*swaySpeed))*-swayScaleY

		local plrDirection = humanoid.MoveDirection

		if plrDirection.Z < 0 then --Pressing D
			tiltZ = lerp(tiltZ, -0.05, .1)
		elseif plrDirection.Z > 0 then --Pressing A
			tiltZ = lerp(tiltZ, 0.05, .1)
		else
			tiltZ = lerp(tiltZ, 0, .1)
		end
	else
		startedRunning = nil

		swayX = lerp(swayX, 0, .1)
		swayY = lerp(swayY, 0, .1)
		tiltZ = lerp(tiltZ, 0, .1)
	end
	
	local velocityY = plrVelocity*Vector3.new(0,1,0)
	tiltY = velocityY.Magnitude > .1 and lerp(tiltY, velocityY.Y*0.003,.1) or lerp(tiltY, 0, 0.3)
	
	breathe = math.sin(tick()*2)*breathScale
	
	swaySpring:shove(Vector3.new(-mouseDelta.X/500, mouseDelta.Y/500, 0))
	bobSpring:shove(Vector3.new(Bob(3), Bob(6), Bob(3)) / 12 * (primaryPart.AssemblyLinearVelocity.Magnitude) / 12)
	
	local UpdatedSway = swaySpring:update(dt)
	local UpdatedBob = bobSpring:update(dt)
	
	local camCframe = Camera.CFrame:ToWorldSpace(cameraOffset)
	local vmCframe = camCframe * CFrame.Angles(breathe + deltaY + tiltY, swayX + deltaX, tiltZ)
	vmCframe += Vector3.new(0, breathe + swayY, 0)
	--[[
	Arms:SetPrimaryPartCFrame(camCframe*
		CFrame.new(UpdatedSway.X, UpdatedSway.Y, 0)*
		CFrame.new(UpdatedBob.X, UpdatedBob.Y, 0)
	)
	]]--
	
	Arms:SetPrimaryPartCFrame(vmCframe)
end

local function charAdded(char: Model)
	Arms.Parent = Camera
	update()

	humanoid = char:WaitForChild("Humanoid")
	primaryPart = char.PrimaryPart

	local lookConnection 		    = Camera:GetPropertyChangedSignal("CFrame"):Connect(update)

	humanoid.Died:Connect(function()
		lookConnection:Disconnect()
		Arms.Parent = Player
	end)
end

local function lookUpDown(looker: Player, mousePos: Vector3)
	if not looker.Character or looker.Character.Parent ~= workspace then
		print("not qualified")
		return
	end
	
	if not targets[looker] then
		targets[looker] = makeNewTarget()
	end
	
	setupIkControl(looker.Character, looker)
	targets[looker].Position = mousePos
end

if Player.Character then
	charAdded(Player.Character)
end

Player.CharacterAdded:Connect(charAdded)
RunService.RenderStepped:Connect(setArmsToCam)
LookUpDown.OnClientEvent:Connect(lookUpDown)

Gunkit Script:

local runSer = game:GetService("RunService")
local uis = game:GetService("UserInputService")
local ts = game:GetService("TweenService")
local rnd = Random.new()
local client = game.Players.LocalPlayer

local plrGui = client:WaitForChild("PlayerGui")
local crosshairGui = plrGui:WaitForChild("Crosshair")
local topLinePos = crosshairGui.CrosshairContainer.TopLine.Position
local botLinePos = crosshairGui.CrosshairContainer.BottomLine.Position
local leftLinePos = crosshairGui.CrosshairContainer.LeftLine.Position
local rightLinePos = crosshairGui.CrosshairContainer.RightLine.Position
local ammoGui = plrGui:WaitForChild("Ammo")
crosshairGui.Enabled, ammoGui.Enabled = false, false

local mouse = client:GetMouse()

local cam = workspace.CurrentCamera

local viewmodelRS = game:GetService("ReplicatedStorage"):WaitForChild("ViewmodelReplicatedStorage")
local remotes = viewmodelRS:WaitForChild("RemoteEvents")
local toolViewmodels = viewmodelRS:WaitForChild("ToolViewmodels")
local effects = viewmodelRS:WaitForChild("Effects")

local viewmodel:Model = cam:FindFirstChild(client.Name .. "'s Viewmodel") or workspace:FindFirstChild(client.Name .. "'s Viewmodel") or viewmodelRS:WaitForChild("ArmsViewmodel"):Clone()
viewmodel.Name = client.Name .. "'s Viewmodel"
viewmodel.HumanoidRootPart.Anchored = true
viewmodel.Parent = cam

if not viewmodel.HumanoidRootPart:FindFirstChild("VIEWMODEL M6D") then
	local viewmodelAttach = Instance.new("Motor6D")
	viewmodelAttach.Name = "VIEWMODEL M6D"
	viewmodelAttach.Part0 = viewmodel.HumanoidRootPart
	viewmodelAttach.Parent = viewmodel.HumanoidRootPart
end

local char = script.Parent
local hum = char:WaitForChild("Humanoid")
local root = char:WaitForChild("HumanoidRootPart")

local leftDown = false
local rightDown = false

local equippedTool = nil
local toolConfig = nil

local lastShot = 0
local numBulletsShot = 0
local justShot = false

local startedRunning = nil

local defaultFOV = 70

local swayX = 0
local swayY = 0
local deltaX = 0
local deltaY = 0
local breathe = 0
local tiltZ = 0
local tiltY = 0
local currentZRecoil = 0
local goalZRecoil = 0
local currentSprayRecoil = Vector2.new(0, 0)
local goalSprayRecoil = Vector2.new(0, 0)
local currentAngularShake = CFrame.Angles(0, 0, 0)
local goalAngularShake = CFrame.Angles(0, 0, 0)

local idleAnim = viewmodel.AnimationController:LoadAnimation(viewmodelRS["ArmAnimations"].IDLE)

local allToolAnimations = {}

local currentIdleAnim = idleAnim


function lerp(a:number, b:number, t:number)
	return a + (b - a) * t
end


function moveVM(deltaTime)
	if char and char.Humanoid.Health > 0 and char:FindFirstChild("Head") then
		local currentFOV = cam.FieldOfView

		local vXZ = (root.AssemblyLinearVelocity * Vector3.new(1, 0, 1))
		if vXZ.Magnitude > 0.1 then
			if not startedRunning then
				startedRunning = tick()
			end

			local swayScaleX = 0.04
			local swayScaleY = 0.08
			if equippedTool and currentIdleAnim == allToolAnimations[equippedTool.Name].FIRST_PERSON.AIM then
				swayScaleX = 0.01
				swayScaleY = 0.02
			end
			
			local swaySpeed = vXZ.Magnitude/1.8
			local timeRunning = tick() - startedRunning
			
			swayX = math.sin(timeRunning * swaySpeed) * swayScaleX
			swayY = math.abs(math.cos(timeRunning * swaySpeed)) * -swayScaleY
			
			local plrDirection = hum.MoveDirection
			if uis:IsKeyDown(Enum.KeyCode.D) and not uis:IsKeyDown(Enum.KeyCode.A) then
				tiltZ = lerp(tiltZ, -0.05, 0.1)
			elseif uis:IsKeyDown(Enum.KeyCode.A) and not uis:IsKeyDown(Enum.KeyCode.D) then
				tiltZ = lerp(tiltZ, 0.05, 0.1)
			else
				tiltZ = lerp(tiltZ, 0, 0.1)
			end
			
			local goalFOV = defaultFOV + (vXZ.Magnitude * 0.5)
			currentFOV = lerp(cam.FieldOfView, goalFOV, 0.1)

		else
			swayX = lerp(swayX, 0, 0.1)
			swayY = lerp(swayY, 0, 0.1)
			tiltZ = lerp(tiltZ, 0, 0.1)
			startedRunning = nil

			local goalFOV = defaultFOV
			currentFOV = lerp(cam.FieldOfView, goalFOV, 0.1)
		end

		if equippedTool and rightDown and toolConfig.Type == "Gun" and toolConfig.CanAim == true and currentIdleAnim ~= allToolAnimations[equippedTool.Name].FIRST_PERSON.AIM and equippedTool:WaitForChild("CurrentStats").CanShoot.Value == true then
			currentIdleAnim:Stop()
			currentIdleAnim = allToolAnimations[equippedTool.Name].FIRST_PERSON.AIM
			allToolAnimations[equippedTool.Name].THIRD_PERSON.AIM:Play()

			hum.WalkSpeed = toolConfig.AimMoveSpeed

		elseif equippedTool then 
			if not rightDown and toolConfig.Type == "Gun" and toolConfig.CanAim and currentIdleAnim == allToolAnimations[equippedTool.Name].FIRST_PERSON.AIM then
				currentIdleAnim:Stop()
				currentIdleAnim = allToolAnimations[equippedTool.Name].FIRST_PERSON.IDLE
				allToolAnimations[equippedTool.Name].THIRD_PERSON.AIM:Stop()
			end

			hum.WalkSpeed = toolConfig.MoveSpeed

		elseif not equippedTool then
			hum.WalkSpeed = 16
		end
		
		if equippedTool and toolConfig.Type == "Gun" and currentIdleAnim == allToolAnimations[equippedTool.Name].FIRST_PERSON.AIM then
			currentFOV = currentFOV * toolConfig.AimZoom
		end
		
		cam.FieldOfView = currentFOV

		if not currentIdleAnim.IsPlaying then
			currentIdleAnim:Play()
		end

		local vY = (root.AssemblyLinearVelocity * Vector3.new(0, 1, 0))
		if vY.Magnitude > 0.1 then
			tiltY = lerp(tiltY, vY.Y * 0.003, 0.1)
		else
			tiltY = lerp(tiltY, 0, 0.3)
		end

		local mouseDelta = uis:GetMouseDelta()
		deltaX = lerp(deltaX, mouseDelta.X * 0.001, 0.2)
		deltaY = lerp(deltaY, mouseDelta.Y * 0.001, 0.2)

		local breatheScale = 0.01
		if equippedTool and toolConfig.Type == "Gun" and currentIdleAnim == allToolAnimations[equippedTool.Name].FIRST_PERSON.AIM then
			breatheScale = 0.003
		end
		breathe = math.sin(tick() * 2) * breatheScale
		
		currentZRecoil = lerp(currentZRecoil, goalZRecoil, 0.2)
		
		if justShot then
			local scale = 0.2
			if equippedTool and toolConfig.CanAim and allToolAnimations[equippedTool.Name].FIRST_PERSON.AIM.IsPlaying then
				scale = 0.05
			end
			
			local noiseX = (math.noise(goalZRecoil, tick(), rnd:NextNumber())) * scale
			local noiseY = (math.noise(goalZRecoil + 1, tick(), rnd:NextNumber())) * scale
			local noiseZ = (math.noise(goalZRecoil + 2, tick(), rnd:NextNumber())) * scale
			local angularShake = CFrame.Angles(noiseX, noiseY, noiseZ)
			goalAngularShake *= angularShake
			justShot = false
		end
		currentAngularShake = currentAngularShake:Lerp(goalAngularShake, 0.1)
		goalAngularShake = goalAngularShake:Lerp(CFrame.Angles(0, 0, 0), 0.3)
		
		local goalSprayRecoilIndex = math.round(numBulletsShot)

		if toolConfig and toolConfig.Type == "Gun" and goalSprayRecoilIndex > 0 then
			if goalSprayRecoilIndex <= #toolConfig.SprayPattern.Initial then
				goalSprayRecoil = toolConfig.SprayPattern.Initial[goalSprayRecoilIndex]
			else
				goalSprayRecoilIndex -= #toolConfig.SprayPattern.Initial
				
				while goalSprayRecoilIndex > #toolConfig.SprayPattern.Looped do
					goalSprayRecoilIndex -= #toolConfig.SprayPattern.Looped
				end
				
				goalSprayRecoil = toolConfig.SprayPattern.Looped[goalSprayRecoilIndex]
			end
			goalSprayRecoil += Vector2.new(rnd:NextNumber(-0.1, 0.1), rnd:NextNumber(-0.1, 0.1))
		else
			goalSprayRecoil = goalSprayRecoil:Lerp(Vector2.new(0, 0), 0.3)
		end
		
		local sprayRecoilAlpha = deltaTime
		if toolConfig and toolConfig.Type == "Gun" then
			sprayRecoilAlpha = deltaTime / (1 / toolConfig.FireRate)
			
			if allToolAnimations[equippedTool.Name].FIRST_PERSON.AIM.IsPlaying then
				goalSprayRecoil = goalSprayRecoil / (toolConfig.BulletInaccuracy / toolConfig.ADSInaccuracy)
			end
		end
		
		currentSprayRecoil = currentSprayRecoil:Lerp(goalSprayRecoil, sprayRecoilAlpha)
		
		local recoilX = math.rad(currentSprayRecoil.X)
		local recoilY = math.rad(currentSprayRecoil.Y)
		cam.CFrame *= CFrame.Angles(recoilY, recoilX, 0)
		
		local vmCFrame = cam.CFrame
		vmCFrame = vmCFrame * CFrame.Angles(breathe + deltaY + tiltY + currentZRecoil/65, swayX + deltaX, tiltZ) * currentAngularShake + Vector3.new(0, breathe + swayY, 0) + (vmCFrame.LookVector * -currentZRecoil)
		
		if viewmodel.Parent == cam then
			viewmodel:PivotTo(vmCFrame)
		else
			viewmodel:PivotTo(CFrame.new(0, 10000, 0))
		end
	end
end