Need help with frame delta to make viewmodel bobbing the same through all frame rates

I need help using the delta to make the bobbing the same across all frame rates, as the title says. I have been looking at the code now for a while but I really struggle to understand springs and the spring module. Can someone help?

(Also I know my code probably isnt that optimized, I made most of it myself so yeah)

Video of issue

File

Run&Gun Viewmodel Testing.rbxl (115.4 KB)

Raw Code
-- Services
local player = game:GetService("Players").LocalPlayer
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Spring = require(script:WaitForChild("Spring"))
local UserInputService = game:GetService("UserInputService")
local MouseService = game:GetService("MouseService")
local TweenService = game:GetService("TweenService")
local DebrisService = game:GetService("Debris")
local SoundService = game:GetService("SoundService")

-- Essentials
local Camera = game.Workspace.CurrentCamera
local GunFolder = ReplicatedStorage.Guns
local ItemFolder = ReplicatedStorage.Items
local DefaultFOV = 90
local DownsightsFOV = 60
local CanShoot = true
local CurrentGun = nil
local Downsights = false
local Shooting = false
local FireCooldown = 1
local SightsAnim = nil
local FireAnim = nil
local FireSightsAnim = nil
local ReloadAnim = nil
local WalkSpeed = game:GetService("StarterPlayer").CharacterWalkSpeed
local fovIn = TweenService:Create(Camera, TweenInfo.new(0.25), {FieldOfView = DownsightsFOV})
local fovOut = TweenService:Create(Camera, TweenInfo.new(0.1), {FieldOfView = DefaultFOV})
local Ammo = nil
pcall(function()
	player.PlayerGui.MobileButtons:Destroy()
end)

local fireButton = script:WaitForChild("MobileButtons").shootButton
local reloadButton = fireButton.reloadButton
if UserInputService.TouchEnabled then
	script.MobileButtons.Parent = player.PlayerGui
end

local gui = player.PlayerGui.GameUI.Location.Gun.gunFrame
local ammoDisplay = gui.ammo.amount
local gunIcon = gui
local gunName = gui.gun.id
local character = player.Character or player.CharacterAdded:Wait()

local serverLoadGun = ReplicatedStorage.Events.LoadSlot
local serverPlayAnimations = ReplicatedStorage.Events.playAnimation

-- Viewmodel Setup
pcall(function()
	game.Workspace.CurrentCamera.Viewmodel:Destroy()
end)
UserInputService.MouseIconEnabled = false
local Viewmodel = ReplicatedStorage.Viewmodel:Clone()
Viewmodel.Parent = Camera
character:WaitForChild("Body Colors"):Clone().Parent = Viewmodel
for _, descendant in Viewmodel:GetDescendants() do
	if descendant:IsA("BasePart") then
		descendant.CollisionGroup = "Player"
	end
end
local SwaySpring = Spring.new()
local BobSpring = Spring.new()
local RecoilSpring = Spring.new()
local Sound = nil
local Animator = Viewmodel.Humanoid.Animator
local debuffTimer = 1
local bobTime = 0

-- Functions

local function Bob(addition, tickSpeed)
	local result = math.sin(tickSpeed * tonumber(addition) * 1.85) * 2
	return result
end

local function load(name, consumable)
	serverLoadGun:FireServer(name, consumable)
	pcall(function()
		CurrentGun:Destroy()
	end)
	if consumable == true then
		CanShoot = false
		CurrentGun = ItemFolder:FindFirstChild(name):Clone()
		FireCooldown = CurrentGun:GetAttribute("Rate")
	else
		CurrentGun = GunFolder:FindFirstChild(name):Clone()	
		CanShoot = true
	end
	CurrentGun.Parent = Viewmodel["Right Arm"]
	local RootToArmAttachment = Instance.new("Motor6D")
	RootToArmAttachment.Parent = Viewmodel["Right Arm"]
	RootToArmAttachment.Part0 = Viewmodel["Right Arm"]
	RootToArmAttachment.Part1 = CurrentGun.Handle
	RootToArmAttachment.Name = "Handle"
	if consumable == false then
		if Ammo == nil then
			Ammo = CurrentGun:GetAttribute("Ammo")
		end
	end

	-- Animations
	local Animations = CurrentGun.Animations
	pcall(function()
		Viewmodel.Humanoid:WaitForChild("Animator"):LoadAnimation(Animations.Out):Play() -- takeout animation
	end)
	Viewmodel.Humanoid.Animator:LoadAnimation(Animations.Idle):Play() -- Play idle animation
	pcall(function()
		SightsAnim = Viewmodel.Humanoid.Animator:LoadAnimation(Animations.Sights)
		FireAnim = Viewmodel.Humanoid.Animator:LoadAnimation(Animations.Fire)
		FireSightsAnim = Viewmodel.Humanoid.Animator:LoadAnimation(Animations["Fire Downsights"])
		ReloadAnim = Viewmodel.Humanoid.Animator:LoadAnimation(Animations.Reload)
	end)

	-- Sound
	Sound = CurrentGun.Handle.FireSound

	-- User Interface (GUI)
	gunName.Text = name
	pcall(function()
		ammoDisplay.Text = Ammo.."/"..CurrentGun:GetAttribute("Ammo")
		ammoDisplay.TextColor3 = Color3.new(1, 1, 1)
	end)
end

local function Effect(boolean, duration)
	if CurrentGun == nil or player.CameraMode == Enum.CameraMode.Classic then return end
	for i,v in CurrentGun.Handle.Effect:GetChildren() do
		v.Enabled = boolean
	end
	task.wait(duration)
	local opposite = not boolean
	if boolean == true then opposite = false else opposite = true end
	for i,v in CurrentGun.Handle.Effect:GetChildren() do
		v.Enabled = opposite
	end
end

local function firegun(recoil)
	if CanShoot == false then return end
	if FireCooldown <= 0 and Ammo > 0 then
		game.ReplicatedStorage.Events.fireGun:FireServer(Camera.CFrame.Position, Camera.CFrame.LookVector, CurrentGun:GetAttribute("Damage"))
		local Recoil = CurrentGun:GetAttribute("Recoil")
		if UserInputService.TouchEnabled == true then
			Recoil = false
		end
		if Recoil == false then
			Recoil = 0
		end
		if Downsights == true then
			Recoil = Recoil/1.4
		end
		Shooting = true
		FireCooldown = CurrentGun:GetAttribute("Shooting_Cooldown")
		Ammo -= 1
		ammoDisplay.Text = Ammo.."/"..CurrentGun:GetAttribute("Ammo")
		if Ammo <= 5 then
			ammoDisplay.TextColor3 = Color3.new(1, 0, 0)
		else
			ammoDisplay.TextColor3 = Color3.new(1, 1, 1)
		end
		if Downsights == true then
			FireSightsAnim:Play(.1)
			serverPlayAnimations:FireServer(FireSightsAnim.Animation.AnimationId, "FireSights", .1, "play")
		else
			FireAnim:Play(.1)
			serverPlayAnimations:FireServer(FireAnim.Animation.AnimationId, "Fire", .1, "play")
		end
		Sound:Stop()
		Sound.TimePosition = 0
		Sound:Play()
		Effect(true, .05)
		RecoilSpring:shove(Vector3.new(Recoil, math.random(Recoil * -2,Recoil * 2),Recoil * 5))
		coroutine.wrap(function()
			task.wait(.2)
			RecoilSpring:shove(Vector3.new(-2.8, math.random(Recoil * -1,Recoil), Recoil * -5))
		end)
		if CurrentGun:GetAttribute("Auto") == false then Shooting = false end
		if Shooting == true then
			repeat
				if FireCooldown <= 0 and Ammo > 0 then
					Shooting = true
					game.ReplicatedStorage.Events.fireGun:FireServer(Camera.CFrame.Position, Camera.CFrame.LookVector, CurrentGun:GetAttribute("Damage"))
					FireCooldown = CurrentGun:GetAttribute("Shooting_Cooldown")
					Ammo -= 1
					ammoDisplay.Text = Ammo.."/"..CurrentGun:GetAttribute("Ammo")
					if Ammo <= CurrentGun:GetAttribute("Ammo")*0.25 then
						ammoDisplay.TextColor3 = Color3.new(1, 0, 0)
					else
						ammoDisplay.TextColor3 = Color3.new(1, 1, 1)
					end
					if Downsights == true then
						FireSightsAnim:Play(.1)
						serverPlayAnimations:FireServer(FireSightsAnim.Animation.AnimationId, "FireSights", .1, "play")
					else
						FireAnim:Play(.1)
						serverPlayAnimations:FireServer(FireAnim.Animation.AnimationId, "Fire", .1, "play")
					end
					Sound:Stop()
					Sound.TimePosition = 0
					Sound:Play()
					Effect(true, .05)
					RecoilSpring:shove(Vector3.new(Recoil, math.random(Recoil * -2,Recoil * 2),Recoil * 5))
					coroutine.wrap(function()
						task.wait(.2)
						RecoilSpring:shove(Vector3.new(-2.8, math.random(Recoil * -1,Recoil), Recoil * -5))
					end)
				end
				task.wait(FireCooldown)
			until Shooting == false
			--game.Workspace.CurrentCamera.CFrame *= CFrame.Angles(math.rad(UpdatedRecoilSpring.X), math.rad(UpdatedRecoilSpring.Y), math.rad(UpdatedRecoilSpring.Z))
		end
	end
end

local function reloadgun()
	if CanShoot == false then return end
	if Shooting == false and FireCooldown <= 0 then
		fovOut:Play()
		ReloadAnim:Play(.05)
		serverPlayAnimations:FireServer(ReloadAnim.Animation.AnimationId, "Reload", .5, "play")
		FireCooldown = CurrentGun:GetAttribute("Reload_Time")
		task.wait(FireCooldown)
		Ammo = CurrentGun:GetAttribute("Ammo")
		ammoDisplay.Text = Ammo.."/"..CurrentGun:GetAttribute("Ammo")
		ammoDisplay.TextColor3 = Color3.new(1, 1, 1)
		if Downsights == true then
			fovIn:Play()
		end
	end
end

function sights(state)
	if CanShoot == false then return end
	if state == true then
		UserInputService.MouseDeltaSensitivity = .4
		Downsights = true
		character.Humanoid.WalkSpeed = WalkSpeed/1.75
		fovIn:Play()
		SightsAnim:Play(.25)
		serverPlayAnimations:FireServer(SightsAnim.Animation.AnimationId, "Sights", .25, "play")
	else
		UserInputService.MouseDeltaSensitivity = 1
		Downsights = false
		character.Humanoid.WalkSpeed = WalkSpeed
		SightsAnim:Stop(.1)
		serverPlayAnimations:FireServer(SightsAnim.Animation.AnimationId, "Sights", .25, "stop")
		fovOut:Play()
	end
end

-- Main
Camera.FieldOfView = DefaultFOV
Viewmodel.Shirt.ShirtTemplate = character:WaitForChild("Shirt").ShirtTemplate
load(script:GetAttribute("gunToLoad"), false)
-- Main Loops
RunService.RenderStepped:Connect(function(delta_time)
	if Viewmodel ~= nil then
		if CurrentGun == nil then return end

		if player.CameraMode == Enum.CameraMode.Classic then
			for i,v in Viewmodel:GetDescendants() do
				if v:IsA("BasePart") then
					v.Transparency = 1
				end
			end
		else
			for i,v in Viewmodel:GetDescendants() do
				if v:IsA("BasePart") and v.Name ~= "HumanoidRootPart" and v.Name ~= "Handle" and v.Name ~= "Torso" and v.Name ~= "Head" then
					v.Transparency = 0
				end
			end
		end

		local Delta = game.UserInputService:GetMouseDelta()
		bobTime = tick()--(bobTime + 1.5)* delta_time

		-- Shoving
		SwaySpring:shove(Vector3.new((-Delta.X/500)/0.25, (Delta.Y/500)/0.25, 0))
		if character.Humanoid:GetState() == Enum.HumanoidStateType.Running then
			BobSpring:shove(Vector3.new(Bob(5,bobTime), Bob(10,bobTime), Bob(5,bobTime)) / 10 * (character.HumanoidRootPart.Velocity.Magnitude) / game.StarterPlayer.CharacterWalkSpeed/1.2)
		else
			BobSpring:shove(Vector3.new(Bob(5,bobTime), Bob(10,bobTime), Bob(5,bobTime)) / 10 * (5) / game.StarterPlayer.CharacterWalkSpeed/1.2)
		end

		-- Updating springs
		local UpdatedBob = nil
		local UpdatedSway = SwaySpring:update(0.0015)
		if Downsights == true then
			UpdatedBob = BobSpring:update(0.1)
		else
			UpdatedBob = BobSpring:update(0.0015)
		end

		-- Applying springs
		local success, errorMsg = pcall(function()
			Viewmodel.Head:PivotTo(
				workspace.CurrentCamera.CFrame *
					CFrame.new(UpdatedSway.X, UpdatedSway.Y, UpdatedSway.Z) *
					CFrame.new(UpdatedBob.X, UpdatedBob.Y, UpdatedSway.Z)
			)
		end)

		if errorMsg then
			Viewmodel:Destroy()
			Viewmodel = nil
			return
		end

		if FireCooldown <= 0 then
			FireCooldown = 0
			if CanShoot == false then
				load(player.currentGun.Value, false)
			end
		else
			FireCooldown -= delta_time
		end

		-- Recoil
		local UpdatedRecoilSpring = RecoilSpring:update(0.015)
		Viewmodel.HumanoidRootPart.CFrame *= CFrame.Angles(math.rad(UpdatedRecoilSpring.X) * 2, 0, 0)
		game.Workspace.Camera.CFrame *= CFrame.Angles(math.rad(UpdatedRecoilSpring.X), math.rad(UpdatedRecoilSpring.Y), math.rad(UpdatedRecoilSpring.Z))
	end
end)

UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	if CurrentGun == nil or Viewmodel == nil then return end
	if input.KeyCode == Enum.KeyCode.R or input.KeyCode == Enum.KeyCode.ButtonX then
		reloadgun()
	end
	-- Mouse buttons
	if input.UserInputType == Enum.UserInputType.MouseButton2 or input.KeyCode == Enum.KeyCode.ButtonL2 then
		sights(true)
	end
	if UserInputService.TouchEnabled == false then
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.KeyCode == Enum.KeyCode.ButtonR2 then
			firegun(true)
		end
	end
end)

fireButton.InputBegan:Connect(function()
	sights(true)
	firegun(true)
end)

fireButton.MouseButton1Up:Connect(function()
	Shooting = false
	sights(false)
end)

fireButton.MouseLeave:Connect(function()
	Shooting = false
	sights(false)
end)

reloadButton.MouseButton1Click:Connect(function()
	reloadgun()
end)

UserInputService.InputEnded:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton2 or input.KeyCode == Enum.KeyCode.ButtonL2 then
		sights(false)
	end
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.KeyCode == Enum.KeyCode.ButtonR2 then
		Shooting = false
	end
end)

game.ReplicatedStorage.Events.RoundEnd.OnClientEvent:Connect(function()
	Viewmodel:Destroy()
end)

character.Humanoid.Died:Connect(function()
	Viewmodel:Destroy()
	script:Destroy()
end)

And heres the module (named spring)

-- Constants

local ITERATIONS	= 8

-- Module

local SPRING	= {}

-- Functions 

function SPRING.new(self, mass, force, damping, speed)

	local spring	= {
		Target		= Vector3.new();
		Position	= Vector3.new();
		Velocity	= Vector3.new();

		Mass		= mass or 5;
		Force		= force or 50;
		Damping		= damping or 4;
		Speed		= speed  or 4;
	}

	function spring.getstats(self)
		return self.Mass, self.Force, self.Damping, self.Speed
	end

	function spring.changestats(self, mass, force, damping, speed)
		self.Mass = mass or self.Mass
		self.Force = force or self.Force
		self.Damping = damping or self.Damping
		self.Speed = speed or self.Speed
	end

	function spring.shove(self, force)
		local x, y, z	= force.X, force.Y, force.Z
		if x ~= x or x == math.huge or x == -math.huge then
			x	= 0
		end
		if y ~= y or y == math.huge or y == -math.huge then
			y	= 0
		end
		if z ~= z or z == math.huge or z == -math.huge then
			z	= 0
		end
		self.Velocity	= self.Velocity + Vector3.new(x, y, z)
	end

	function spring.update(self, dt)
		local scaledDeltaTime = dt * self.Speed / ITERATIONS
		for i = 1, ITERATIONS do
			local iterationForce= self.Target - self.Position
			local acceleration	= (iterationForce * self.Force) / self.Mass

			acceleration		= acceleration - self.Velocity * self.Damping

			self.Velocity	= self.Velocity + acceleration * scaledDeltaTime
			self.Position	= self.Position + self.Velocity * scaledDeltaTime
		end

		return self.Position
	end

	return spring
end

-- Return

return SPRING
1 Like

The spring module which you are using has a dt parameter in its update function which is for delta time

You’re updating the springs with constant values which aren’t multiplied by the delta_time variable. If you like how it feels like at 240 FPS, then you can add delta_time * 240 * before all your constants.

Gun Framework, Line 289: Change 0.0015 to delta_time * 240 * 0.0015
Gun Framework, Line 291: Change 0.1 to delta_time * 240 * 0.1
Gun Framework, Line 293: Change 0.0015 to delta_time * 240 * 0.0015
Gun Framework, Line 321: Change 0.015 to delta_time * 240 * 0.015

I recommend simplifying the expression to be only DeltaTime * Constant even though Luau’s compiler already folds constants

1 Like

Thanks lol, I honestly had a brain fart while doing this