Static recoil doesn't work like i wanted it to

before i elaborate, i do want to point out that the entire function is ripped from GPT

this is because the math seen in this function is very confusing to me and i wouldn’t ever write it myself

feel free to mock me for it


anyway,

the camera cframe is supposed to go back to its original position always, so for example:

in automatic guns:

  • the first shot sets the original camera position
  • any shot afterwards will still kick the camera up, but will NOT change the original camera position
  • any shot afterwards will also most likely extend the time it takes to recover, but i’ll handle that part don’t worry!!

but the issues that are present right now are:

  • mouse movement is entirely restricted while the recoil recovery is in-progress, this is very bad and i do NOT want this to happen
  • the camera does not kick up from any shot after shot #1, but i think this is because the recoil recovery time is faster than the recoil appliance (if that makes sense)

here’s a visual presentation of what’s going on:


and here we have the function:

local function ApplyRecoil(tool)
	local camera = workspace.CurrentCamera
	local recoilAngle = recoilModule[tool.Name]["VISUAL_RECOIL"]
	local currentCFrame = camera.CFrame -- Capture the camera CFrame at the start of shooting
	local lookVector = currentCFrame.LookVector
	local upVector = Vector3.new(0, 1, 0)
	local rightVector = lookVector:Cross(upVector).Unit

	-- Compute the recoil rotation around the right vector (pitch adjustment)
	local recoilOffset = CFrame.fromAxisAngle(rightVector, math.rad(-recoilAngle))

	-- Apply the recoil offset instantly (kick the camera up)
	camera.CFrame = camera.CFrame * recoilOffset
	-- Smoothly return the camera back to the captured `currentCFrame`
	coroutine.wrap(function()
		local recoveryTime = 0.5 -- Time it takes to recover from recoil (in seconds)
		local elapsedTime = 0

		while elapsedTime < recoveryTime do
			-- Interpolate the camera back to the original `currentCFrame`
			local alpha = elapsedTime / recoveryTime
			camera.CFrame = camera.CFrame:Lerp(currentCFrame, alpha)

			-- Increment elapsed time and wait for the next frame
			elapsedTime = elapsedTime + task.wait(0.01)
		end

		-- Ensure the final position is exactly the original CFrame after recovery
		camera.CFrame = currentCFrame
	end)()
end

its because you’re interpolating to a previous position of the camera instead of the current position of the camera.

--problem is here, you're lerping to the old camera position.
			camera.CFrame = camera.CFrame:Lerp(currentCFrame, alpha)

Instead, apply an inverse of the recoil offset to the current camera cf

camera.CFrame *= recoilOffset:Inverse()

A more practical solution would be to use springs for recoil. Springs are much smoother and easier to deal with.

Heres an example from my old game using springs

function recoil(dt)
	local random = Random.new()
	local modifier = 1
	local rotmod = 1
	local del = (dt*60)
	if _isAiming then
		modifier = 0.5
		rotmod = 0.5
	end
	local modelRecoil = Vector3.new(random:NextNumber(gunData.minModelRecoil.X, gunData.maxModelRecoil.X)*modifier, random:NextNumber(gunData.minModelRecoil.Y, gunData.maxModelRecoil.Y)*modifier, random:NextNumber(gunData.minModelRecoil.Z, gunData.maxModelRecoil.Z)*modifier)
	local rotationalRecoil = Vector3.new(random:NextNumber(gunData.minRotationalRecoil.X, gunData.maxRotationalRecoil.X)*rotmod, random:NextNumber(gunData.minRotationalRecoil.Y, gunData.maxRotationalRecoil.Y)*rotmod, random:NextNumber(gunData.minRotationalRecoil.Z, gunData.maxRotationalRecoil.Z)*rotmod)
	local cameraRecoil = Vector3.new(random:NextNumber(gunData.minCameraRecoil.X, gunData.maxCameraRecoil.X)*del*modifier,random:NextNumber(gunData.minCameraRecoil.Y, gunData.maxCameraRecoil.Y)*del*modifier, 0)

	recoilspring.Speed = gunData.springSpeed
	recoilspring.Damping = gunData.springDampening
	recoilspring.Mass = gunData.springMass

	camrecoilspring.Damping = gunData.camSpringDampening
	camrecoilspring.Speed = gunData.camSpringSpeed
	camrecoilspring.Mass = gunData.camSpringMass


	recoilspring:shove(modelRecoil)
	rotationalspring:shove(rotationalRecoil)
	camrecoilspring:shove(cameraRecoil)

	task.delay(gunData.camRecoilRecoveryTime, function()
		if _isCamRecoiling then
			return
		end
		_isCamRecoiling = true
		camrecoilspring:shove(-cameraRecoil)
		_isCamRecoiling = false
	end)


	print(modelRecoil)

end
2 Likes

Hello again @notsad2,

Yeah you will need to use the CFrame Inverse to cancel out the old offset applied in the previous frame as @556natorounds mentioned trick there is no running away from it.

Here is a post I linked in another topic you made some time ago.

1 Like

uhm, sorry but
the code has changed a bit since this post

-- Misc values;
local originalCameraCFrame = nil
local shooting = false
local recoveryTime = 0.2

-- Setting values;
local hasFirstPersonTracersEnabled = true
local hasVisibleViewmodel = true

-- Functions;
local function ResetRecoil()
	originalCameraCFrame = nil
end

local function ApplyRecoil(tool)
	local camera = workspace.CurrentCamera
	local recoilToApply = recoilModule[tool.Name]["VISUAL_RECOIL"]

	if not originalCameraCFrame then
		originalCameraCFrame = camera.CFrame
	end

	-- Calculating recoil;
	local currentLookVector = camera.CFrame.LookVector
	local upVector = Vector3.new(0, 1, 0)

	local rightVector = currentLookVector:Cross(upVector).Unit
	local recoilOffset = CFrame.fromAxisAngle(rightVector, math.rad(-recoilToApply)) -- recoilToApply is a number, 3, for example

	camera.CFrame = camera.CFrame * recoilOffset
end

local function InterpolateToOriginalCameraCFrame(originalCameraCFrame: CFrame)
	local timeElapsed = 0
	local camera = workspace.CurrentCamera
	
	coroutine.wrap(function()
		while timeElapsed < recoveryTime do
			local alpha = timeElapsed / recoveryTime
			local newCFrame = originalCameraCFrame:Lerp(CFrame.new(originalCameraCFrame.Position), alpha)
			
			camera.CFrame = newCFrame
			timeElapsed = timeElapsed + task.wait(0.01)
		end
		
		recoveryTime = 0.2
		ResetRecoil()
	end)()
end

-- this section is way further down
AUTOMATIC = function(weapon: Tool, mouse: Mouse, vShootAnim: Animation, viewmodel: Model)
		local FIRERATE = gunStats[weapon.Name]["FIRERATE"]

		local buttonUpConnection
		shooting = true

		buttonUpConnection = mouse.Button1Up:Connect(function()
			disableIsHoldingEvent:FireServer()
			shooting = false
			buttonUpConnection:Disconnect()
		end)

		shootEvent:FireServer(mouse.Hit.Position)

		while shooting do
			if weapon.Bullets.Value < 1 then disableIsHoldingEvent:FireServer() break end
			vShootAnim:Play()
			VFX(viewmodel, weapon)

			updateHitPosEvent:FireServer(mouse.Hit.Position)
			ApplyRecoil(weapon)
			TracerEffect(viewmodel, weapon, mouse)

			recoveryTime = math.min(recoveryTime + 0.1, 5)

			task.wait(FIRERATE)
		end

		shooting = false
		InterpolateToOriginalCameraCFrame(originalCameraCFrame)
	end,

my approach is to kick the camera up instantly, but increase the recoveryTime with each shot (by default it is 0.2)

after the player is done shooting, i want to interpolate the camera back to its original position (which gets set on the first shot of the gun)
as the name of the function suggests, obviously

but for some sussy reason, the camera always interpolates back to the same position, even after ResetRecoil gets called (which sets the originalCamPos to nil)
additionally, after a lot of shooting, the recoil is inverted, and the camera gets kicked down instead of up

when you do not look in the same direction as the original cam pos, the camera will interpolate properly i think
but when you do it just above the originalCamPos, the camera kicks back to its original position, but the lerping is still going on and restricts any sort of mouse movement
(which is very much seen because i struggled to move the camera in the video)


i literally beg anyone to help me with this as i am struggling with this extremely
(this includes @dthecoolest )

okay update

i managed to fix the inverted recoil issue, along with the always returns to the same position

but now the issue that still persists is the one where mouse movement is restricted;

it turns out, the alpha takes quite a while to reach 1 (finish state), but for some reason the camera lerps way faster than the alpha? why is this?

i’d be happy if someone explained why that happens

local function InterpolateToOriginalCameraCFrame(originalCameraCFrame: CFrame)
	local timeElapsed = 0
	local camera = workspace.CurrentCamera
	
	coroutine.wrap(function()
		while timeElapsed < recoveryTime do
			if shooting then break end
			
			local alpha = timeElapsed / recoveryTime
			
			if camera.CFrame == originalCameraCFrame then break end
			
			warn(alpha)
			local newCFrame = camera.CFrame:Lerp(originalCameraCFrame, alpha)
			
			camera.CFrame = newCFrame
			timeElapsed = timeElapsed + task.wait(0.01)
		end
		
		recoveryTime = 0.2
		warn("Reset recoil!")
		ResetRecoil()
		print(originalCameraCFrame)
	end)()
end

okay i fixed it, it works now :^)

i ended up creating a seperate variable at the beginning of the function that stores the camera cframe, and instead i lerped that:

local function InterpolateToOriginalCameraCFrame(originalCameraCFrame: CFrame)
	local timeElapsed = 0
	local camera = workspace.CurrentCamera
	
	local initialCameraCFrame = camera.CFrame
	local epsilon = 0.02
	
	coroutine.wrap(function()
		while timeElapsed < recoveryTime do
			if shooting then break end
			
			local alpha = timeElapsed / recoveryTime
						
			warn(alpha)
			local newCFrame = initialCameraCFrame:Lerp(originalCameraCFrame, alpha)
			
			camera.CFrame = newCFrame
			
			local positionDifference = (camera.CFrame.Position - originalCameraCFrame.Position).Magnitude
			
			if positionDifference < epsilon then
				break
			end
			
			timeElapsed = timeElapsed + task.wait(0.01)
		end
		
		recoveryTime = 0.2
		ResetRecoil()
	end)()
end

this is just for spray control, incase anyone is wondering
(if the camera is roughly on the same place, break the loop)
yes, this also means the camera will never lerp back to its original position, but roughly next to it

If you actually want to release your game, bind your update loops to RunService()
its alot more optimized than while loops.
good luck tho

1 Like