Recoil system is somewhat affected by frames per second in my first person shooter

(note: This is my first forum post so I’m sorry if I am clueless or unsure of how this works.)

  1. What do you want to achieve?
    I am trying to make a simple recoil system for the guns in my game with randomly generated patterns, a little bit similar to the CS:GO or Valorant recoil pattern.

  2. What is the issue?
    The main issue (which isn’t too big, but changes the gameplay a bit) is that the recoil amount slightly changes depending on how much fps the client is getting. For example, if u have 60 fps, it works fine, but if u have 120 fps or more then the recoil decreases and is slightly less shaky, which can give a gameplay changing advantage.

  3. What solutions have you tried so far?
    I have tried looking into many (around 4?) different forum posts with a similar issue to mine, which most of them use the same or similar module script as mine, but my issue still seems to remain unfixable. On the other forums, they don’t really have the solution to my problem I’m having with the recoil.

Suggestions or maybe even the solution to the problem would be a great help

Here is my code for the client (In Starter Player):

local rs = game:GetService("ReplicatedStorage")
local uis = game:GetService("UserInputService")
local r_s = game:GetService("RunService")

local springModule = require(rs.SpringModule)

local player = game.Players.LocalPlayer
local cam = workspace.CurrentCamera
local mouse = player:GetMouse()

local recoilSpring = springModule.new()

local mouseDown = false
local canFire = true

local framework = {
	inventory = {
		"COLT M4A1"; -- Slot 1 (Primary)
		"H&K USP"; -- Slot 2 (Secondary)
		"NONE"; -- Slot 3 (Melee)
		"NONE"; -- Slot 1 (Misc)
	};
	viewmodel = nil;
	module = nil;
}

local function addWeapon(weapon, slot)
	framework.inventory[slot] = weapon
end

local function loadslot(slot)
	local weapon = framework.inventory[slot]
	if weapon == "NONE" then return end
	
	mouseDown = false
	
	framework.module = require(rs.Modules[weapon])
	
	if framework.viewmodel then
		framework.viewmodel:Destroy()
	end
	
	framework.viewmodel = rs.Viewmodels["VM_" .. weapon]:Clone()
	framework.viewmodel.Parent = cam
end

local function shoot()
	-- Raycast the bullet
	local muzPos = framework.viewmodel.GunModel.Components.Muzzle.Position
	local mouseHit = mouse.Hit.Position
	rs.Shoot:FireServer(framework.module.damage, muzPos, mouseHit)
	
	-- Recoil
	local x = math.random(20, 40)
	local y = math.random(-10, 10)
	local z = math.random(-50, 50)
	recoilSpring:shove(Vector3.new(x, y, z)) -- Up, Sideways, Shake
end

uis.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		if framework.module.mode == "Auto" then
			mouseDown = true
		else
			shoot()
		end
	end
	
	-- Inventory
	if input.KeyCode == Enum.KeyCode.One then
		loadslot(1)
	end
	
	if input.KeyCode == Enum.KeyCode.Two then
		loadslot(2)
	end
	
	if input.KeyCode == Enum.KeyCode.Three then
		loadslot(3)
	end
	
	if input.KeyCode == Enum.KeyCode.Four then
		loadslot(4)
	end
end)

uis.InputEnded:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		mouseDown = false
	end
end)

local oldPos = recoilSpring.Position

r_s.RenderStepped:Connect(function(dt)
	local newPos = recoilSpring:update(dt)
	local diff = newPos - oldPos
	oldPos = newPos
	cam.CFrame *= CFrame.Angles(math.rad(diff.X), math.rad(diff.Y), math.rad(diff.Z))
	
	if framework.viewmodel then
		framework.viewmodel:PivotTo(cam.CFrame)
	end
end)

r_s.Heartbeat:Connect(function(dt)
	if mouseDown and canFire then
		canFire = false
		
		shoot()
		
		task.wait(framework.module.fireRate)
		canFire = true
	end
end)

loadslot(1)

and the module script (In ReplicatedStorage):

-- 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

I believe the issue comes from you using renderStepped (which runs faster/slower depending on the framerate). You can probably fix this by multiplying the angle changed of the camera with delta time.

(A post to a tutorial for delta time, since im not exactly the best at using it either)

Thanks for the advice and tutorial, I’m pretty bad at using delta time too, but I’ll eventually figure it out after I look into it

1 Like