How to implement Recoil without bugs

Hello everyone, I am trying to make a simple recoil system with TweenService and CFrame.Angles, but the problem is when the player moves around, the Tween is still trying to go to the original Camera CFrame.

Is there a better way to make Recoil without the use of:

newCF = CamCF * CFrame.Angles(0,RecoilY,0)

Thanks!

2 Likes

yes
with RunService
The camera CFrame still gets set, but every frame and in a relative way so it sort of remembers the original orientation. (sorry if it’s a bit vague, I got this by trying a bunch until it worked)
(local script:)

local RS = game:GetService("RunService")
local cam = workspace.CurrentCamera
local plr = game:GetService("Players").LocalPlayer
local mouse = plr:GetMouse()

local max = math.max

-- variables
local recoil =  math.rad(9)
local verti = 1 -- vertical multiplier
local horiz = .5 -- horizontal multiplier
local recovSpeed = 5 -- the higher, the faster the recoil recovers
local maxRecov = 1/3 -- 0 is full recovery, 1 is no recovery
maxRecov = -1-1/(maxRecov-1) -- maps 0,1 to 0,inf	https://www.desmos.com/calculator/ggdwhvw4ga

-- initialize
local offs = 0
local pOffs = 0
local zigzag = 1

RS.RenderStepped:Connect(function(dt)
	if math.abs(offs) < 1e-5 then -- performance optimization
		offs = 0
		return
	end
	local speed = recovSpeed*dt
	cam.CFrame *= CFrame.Angles(offs*verti, offs*zigzag*horiz, 0)
	offs = max(1 - speed*maxRecov, 0) * max((pOffs - offs*speed), -recoil)
	pOffs = offs
end)

local function applyRecoil()
	pOffs = 0 -- so it stops compensating for the previous shot
	offs = recoil
	zigzag *= -1 -- zigzag = 2*math.random(0,1)-1 for something random
end

-- for testing
mouse.Button1Down:Connect(applyRecoil)

minimal version so it’s clearer what it does:

local RS = game:GetService("RunService")
local cam = workspace.CurrentCamera

-- variables
local recoil =  math.rad(9)
local recovSpeed = 5 -- the higher, the faster the recoil recovers
local maxRecov = 1/3 -- 0 is full recovery, 1 is no recovery
maxRecov = -1-1/(maxRecov-1) -- maps 0,1 to 0,inf	https://www.desmos.com/calculator/ggdwhvw4ga

-- initialize
local offs = 0
local pOffs = 0

RS.RenderStepped:Connect(function(dt)
	local speed = recovSpeed*dt
	cam.CFrame *= CFrame.Angles(offs, 0, 0)
	offs = (1 - speed*maxRecov) * (pOffs - offs*speed)
	pOffs = offs
end)

local function applyRecoil()
	pOffs = 0 -- so it stops compensating for the previous shot
	offs = recoil
end

-- for testing
local plr = game:GetService("Players").LocalPlayer
local mouse = plr:GetMouse()
mouse.Button1Down:Connect(applyRecoil)

try it out: recoil.rbxl (30.1 KB)

4 Likes

update: I found a way to make the recoil less instant by interpolating the offset, instead of just directly setting it to the recoil:

local RS = game:GetService("RunService")
local cam = workspace.CurrentCamera

local max = math.max
local min = math.min

local function lerp(a, b, t)
	return a * (1 - t) + (b * t)
end

-- variables
local recoil =  math.rad(9)
local recoilTime = 1/25 -- (seconds) the lower, the snappier the recoil (instant below 1/60)
recoilTime = 1/recoilTime
local recovTime = .15 -- (seconds) the lower, the faster the recoil recovers
recovTime = 1/recovTime
local maxRecov = 1/3 -- 0 is full recovery, 1 is no recovery
local compensation = 0 -- 0 forgets the previous shots, 1 treats all shots as one
maxRecov = min(maxRecov, 1-1e-16) -- prevent divide by 0
maxRecov = -1-1/(maxRecov-1) -- maps 0,1 to 0,inf (makes tweaking easier)	https://www.desmos.com/calculator/ggdwhvw4ga 

-- initialize
local  offs = 0 -- final offset
local gOffs = 0 -- goal offset
local pOffs = 0 -- previous offset
local zigzag = .3

RS.RenderStepped:Connect(function(dt)
	local speed = recovTime*dt
	offs = lerp(offs, gOffs, min(recoilTime*dt, 1)) -- smooth the impulse to make it less seizure inducing
	cam.CFrame *= CFrame.Angles(offs, offs*zigzag, 0) -- set the offset
	
	gOffs = max(1 - speed*maxRecov, 0) * (pOffs - gOffs*min(speed, 1)) -- recover
	pOffs = gOffs -- keep track of the previous offset
end)

local function applyRecoil()
	pOffs *= compensation -- so it compensates less for the previous shots
	gOffs = recoil -- set the new goal
	zigzag *= -1
end

-- for testing
local plr = game:GetService("Players").LocalPlayer
local mouse = plr:GetMouse()
mouse.Button1Down:Connect(applyRecoil)

and a version where the offset is a vector for more possibilities
also the recoil twists the camera:

local RS = game:GetService("RunService")
local cam = workspace.CurrentCamera

local max = math.max
local min = math.min

local function lerp(a, b, t)
	return a * (1 - t) + (b * t)
end

-- variables
local recoil = math.rad(9) * Vector3.new(1, .5, 1) -- angle * (vertical, horizontal, twist) multipliers
local recoilTime = 1/25 -- (seconds) the lower, the snappier the recoil (instant below 1/60)
recoilTime = 1/recoilTime
local recovTime = .2 -- (seconds) the lower, the faster the recoil recovers
recovTime = 1/recovTime
local maxRecov = 1/3 -- 0 is full recovery, 1 is no recovery
local compensation = 0 -- 0 forgets the previous shots, 1 treats all shots as one
maxRecov = min(maxRecov, 1-1e-16) -- prevent divide by 0
maxRecov = -1-1/(maxRecov-1) -- maps 0,1 to 0,inf (makes tweaking easier)	https://www.desmos.com/calculator/ggdwhvw4ga

-- initialize
local zero = Vector3.new()
local  offs = zero -- final offset
local gOffs = zero -- goal offset
local pOffs = zero -- previous offset
local zigzag = 1

RS.RenderStepped:Connect(function(dt)
	local speed = recovTime*dt
	offs = offs:Lerp(gOffs, min(recoilTime*dt, 1)) -- smooth the impulse to make it less seizure inducing
	cam.CFrame *= CFrame.Angles(offs.X, offs.Y, offs.Z) -- set the offset
	
	gOffs = max(1 - speed*maxRecov, 0) * (pOffs - gOffs*min(speed, 1)) -- recover
	pOffs = gOffs -- keep track of the previous offset
end)

local function applyRecoil()
	pOffs *= Vector3.new(compensation, compensation, 1) -- so it compensates less for the previous shots (except twist, because you can't affect that with your mouse)
	gOffs = Vector3.new(recoil.X, zigzag*recoil.Y, -zigzag*recoil.Z) -- set the new goal
	zigzag *= -1
end

-- for testing
local plr = game:GetService("Players").LocalPlayer
local mouse = plr:GetMouse()
mouse.Button1Down:Connect(applyRecoil)

recoil smooth attack.rbxl (31.3 KB)

4 Likes