Recoil Implementations

I’ve been trying to implement realistic recoil for my guns. So far I have really only managed to create recoil algorithms that produce a harmonic motion camera rotation thing (oscillating between up and down constantly) instead of a nice kick on the gunshot and smooth recoil recovery.

What recoil algorithms has anyone found successful?

22 Likes

Add some damping to the rate that your gun recovers from recoil, and it won’t bounce as much if at all.

1 Like

One thing I sometime do is take some arbitrary recoil value and then multiply the camera’s CFrame with some CFrame.Angles(0,0,recoil) value. To give it a smooth recovery you can just make the initial shot recoil reach its goal quicker than the recovery recoil value. heres an example:

local recoil = 0.025
function shoot() -- we will ignore all other aspects of this function other than the camera recoil
for i = 1,5 do -- initial camera knockback
workspace.Camera.CFrame = workspace.Camera.CFrame * CFrame.Angles(0,0,recoil) -- bumping up the camera
game:GetService("RunService").Heartbeat:wait()
end
for i = 1,10 do -- camera knockback recovery
workspace.Camera.CFrame = workspace.Camera.CFrame * CFrame.Angles(0,0,recoil/2) -- we have to divide the recoil value in half since we are repeating this loop twice as many times as the knockback loop
game:GetService("RunService").Heartbeat:wait()
end

let me know if this solved your problem

18 Likes

If you’re into those “recoil patterns” then i’ve got a pretty nice script if you would like to use it.

Run = game:GetService("RunService")
--[[ This table controls the recoil that the gun will follow through each shot. 
For example, {3, 12, -1, 0.77, -0.1}. 
The way it works is the camera will move up by 12 for the first 3 shots of the gun. 
After, it will move down by 1 (or up by -1 to be more technical). 
The 0.77 is the alpha that will be used for lerping (how fast the recoil will move). 
And the final number, -0.1, will be the ratio that the recoil will move by to the left / right. In this case, it'll move 12 * 0.1 times to the right, then -1 * 0.1 times again.]]
local RecoilPattern = {
	{3, 12, -1, 0.77, -0.1},
	{6, 12, -1, 0.77, 0.1},
	{8, 12, -1, 0.77, -0.1},
	{10, 12, -1, 0.77, 0.1},
}
local RecoilReset = 0.5 -- Time it takes for recoil pattern to reset back to 1

local curshot = 0
local lastclick = tick()

function lerp(a, b, t) -- Gets a number between two points using an alpha
    return a * (1 - t) + (b * t)
end

function ShootRecoil()
	curshots = (tick() - lastclick > RecoilReset and 1 or curshots + 1) -- Either reset or or increase the current shot we're at
	lastclick = tick()
	for i, v in pairs(RecoilPattern) do
		if curshots <= v[1] then -- Found the current recoil we're at
			spawn(function()
				local num = 0
				while math.abs(num - v[2]) > 0.01 do
					num = lerp(num, v[2], v[4])
					local rec = num / 10
					Camera.CFrame = Camera.CFrame * CFrame.Angles(math.rad(rec), math.rad(rec * v[5]), 0)
					Run.RenderStepped:Wait()
				end
				while math.abs(num - v[3]) > 0.01 do
					num = lerp(num, v[3], v[4])
					local rec = num / 10
					Camera.CFrame = Camera.CFrame * CFrame.Angles(math.rad(rec), math.rad(rec * v[5]), 0)
					Run.RenderStepped:Wait()
				end
			end)
			break
		end
	end
end

Hope it’s helpful.

128 Likes

Good stuff thank you!

Why are you using CoordinateFrame? just type CFrame, its much shorter

2 Likes

I really dislike that Roblox is okay with calling its programming concepts multiple things. Liek game.ReplicatedStorage and game:GetService(“ReplicatedStorage”). Pick one!

2 Likes

Okay, but it’s literally shorter and writing out the whole thing just takes up space & time

I feel ya, but it’s abnormal for an engine.

1 Like

I don’t think that’s the case. ReplicatedStorage is a valid descendant of the DataModel, so it can be accessed by dot syntax. GetService is the preferred and intended method of retrieving a service (this also accounts for services not actually in the DataModel or the ones named “Instance”). Some services are implicitly loaded but it’s still good practice to use GetService when indexing services.

Unless I’m missing something or misunderstand your post.

3 Likes

They’re not different things. If you use a dot notation to get a service, there’s a chance that it won’t exist - this is why you use GetService, if the service doesn’t exist yet, it will create the service and return it.

1 Like

Camera.CoordinateFrame is deprecated in favor of Camera.CFrame. You shouldn’t use Camera.CoordinateFrame in new projects.

The difference between game.ReplicatedStorage and game:GetService(“ReplicatedStorage”) is that the first will return the child called “ReplicatedStorage” parented directly under the game model, which could point to an entirely different child than the ReplicatedStorage service or error entirely, if you rename ReplicatedStorage / another service to be called ReplicatedStorage. The latter, game:GetService(“ReplicatedStorage”), is much safer because it is guaranteed to give you back a ReplicatedStorage object, regardless of what it is actually called in the game hierarchy and regardless of whether it was present before the call.

That being said, let’s stay on topic of the actual support problem.

6 Likes

Cool the more I know!

1 Like

I’ve edited the script fixing the problem.

Sorry for notifying anyone or reviving this conversation. I just want to know (since I tried out this script) how this lerped the camera very smoothly. I don’t see any CFrame:Lerp in it.

Who said you need CFrame:Lerp() to do things smoothly?

It’s pretty easy to implement the functionality of CFrame:Lerp() by yourself.

Do I just fire the ShootRecoil function per shot of my gun? (Auto and Semi)

For me, this solution didn’t work. There was no recoil recovery, and I felt the recoil was a little unrealistic. After analyzing Phantom Forces’ recoil system, I learned that recoil does the following:

  • First, the first shot creates an initial “bump” vertically (bumps the camera up)
  • Second, there is a recovery phase where the camera is tweened back down to its original position
  • While spraying, the camera “bumps” very very slightly side-to-side and up and down, at a seemingly random pattern

I worked with it for a while, and figured out the following code:

local RecoilReset = 0.5 -- amount of time it takes for recoil to reset
local recoveryTime = 0.05; -- the amount of time in seconds it takes for recoil control to be triggered

local bump = 1 -- the base amount of bump while spraying (will increase with more recoil)

local Recoil = 25 -- change this to whatever you want your initial bump to be (higher recoil = more bump)

local RecoilPattern = {
	{1, Recoil, -1, 0.77, -0.1}, -- initial bump
	{6, bump/2, -1, 0.77, -1 * bump* Recoil/25}, -- side to side spray
	{11, bump/2, -1, 0.77, bump* Recoil/25}, -- side to side spray
}

local curshot = 0
local lastshot = tick()

local function lerp(a, b, t) -- Gets a number between two points using an alpha
	return a * (1 - t) + (b * t)
end

local function ShootRecoil()
	curshots = (tick() - lastshot > RecoilReset and 1 or curshots + 1) -- Either reset or or increase the current shot we're at
	lastshot = tick()
	
	if curshots == 1 then
		
		spawn(function()
                        -- this function handles recovery after initial bump.
			wait(recoveryTime)
			
			local recoveryAmount = -1/2 * Recoil
			local recoveryLeft = recoveryAmount
			
			local currentRecovery = 0.5 * recoveryAmount

			
			while recoveryLeft < -0.01 do
				print("ran, with " .. recoveryLeft .. " left")
				currentRecovery = currentRecovery/1.5
				recoveryLeft = recoveryLeft - currentRecovery
				
				Camera.CFrame = Camera.CFrame * CFrame.Angles(math.rad(currentRecovery), 0, 0)
				
				Run.RenderStepped:Wait()
			end
			
		end)
		
	else -- randomize the amount of left-to-right recoil
		
		-- by randomly adding a curshot, we can randomize the side-to-side bump to make it more realistic + uncontrolled
		local rand = math.random()

		curshots = curshots + math.round(rand)
	end
	
	if curshots >= 11 then -- added this to continue side-to-side recoil cycle
		curshots = 2
	end
	
	
	
	for i, v in pairs(RecoilPattern) do
		if curshots <= v[1] then -- Found the current recoil we're at
			spawn(function()

				local num = 0
				while math.abs(num - v[2]) > 0.01 do
					num = lerp(num, v[2], v[4])
					local rec = num / 10
					Camera.CFrame = Camera.CFrame * CFrame.Angles(math.rad(rec), math.rad(rec * v[5]), 0)
					Run.RenderStepped:Wait()
				end
				while math.abs(num - v[3]) > 0.01 do
					num = lerp(num, v[3], v[4])
					local rec = num / 10
					Camera.CFrame = Camera.CFrame * CFrame.Angles(math.rad(rec), math.rad(rec * v[5]), 0)
					Run.RenderStepped:Wait()
				end
			end)
			break
		end
	end
	
end

With this code, you can simply call ShootRecoil() every time you want to recoil the camera. Hopefully this helps anyone who the original solution did not help.

25 Likes

I’m not sure if you’ve found what you’re looking for, but if not, take a look at my reply above.

1 Like

This recoil is actually a lot more realistic than the other script. Thanks for making this!
Only thing I’d change is to have two separate variables for the horizontal movement and the vertical movement.

--I edited the first bit of the script to make it like that, and also made some other contributions:

local RecoilReset = 0.5 -- amount of time it takes for recoil to reset
local recoveryTime = 0.1; -- the amount of time in seconds it takes for recoil control to be triggered

local leftandRight = 10 -- the base amount of bump while spraying (will increase with more recoil)
local bump = 10 -- change this to whatever you want your initial bump to be (higher recoil = more bump)
local Recoil = 20 


local RecoilPattern = {
	{1, Recoil, -1, 0.77, -0.1}, -- initial bump
	{6, bump/2, -bump/2, 0.77, -1 * leftandRight/25}, -- side to side spray
	{11, bump/2, -bump/2, 0.77, leftandRight/25}, -- side to side spray
}
2 Likes