i’m just looking for any insight i could possibly get.. i’m not looking for free answers.. i have attempted to make recoil twice, i’ve gotten pretty “far” on my current attempt but ive been constantly stuck on one major part.
does anyone know how the recovery part of recoil should happen, when you stop shooting, the camera settles back down quickly to 0 or wherever your first bullet was fired. gizmos and debugs provide me with information so i know there’s problems, but i have never seen how this part of recoil (or any of it) is handled properly by others so i have no solutions in my head or any idea where to begin.
if i add recovery, it fights my recoil effect/recoil strength, so it wipes out my entire recoil. so i add a grace period between shots, but then when i do this, there’s never any small recovery inbetween shots, so it looks completely unrealistic when bullets go up by so much, when the bullets should (i believe) settle slightly every shot.
yet many fps games have done it perfectly. you fire a bullet, you have quick instant fast recovery. yet at the same time, it doesn’t overpower and destroy the gradual, increased recoil effect.
in my video, there is recovery, a full recovery happens at the end, but there’s no “spring” or “camera settle” in between shots. they just keep going higher and higher which isn’t right (apologies for bad quality).
any advice or information would be greatly appreciated.
It’s tricky because actual gun recoil is literally tons of force (thousands of pounds of “weight,” if you will, far more force than your hand could ever hold in place) applied to the gun for a tiny fraction of a second, imparting velocity and spin to the entire gun frame that your hand then damps out and returns to normal.
In games this it is usually done as some animation approximation, where the gun is moved briskly initially through some kind of cycle as it climbs and rotates in your hand and slows back down again.
Shots fired while recoil is still recovering are obviously going to go somewhere else than the original aimpoint, and that’s where the opportunity for game design comes in.
It basically comes down to what you want the system to serve:
look cool, feel powerful
make it harder to aim subsequent shots
balance your weapons against each other
etc
And then all games kinda do it differently. CS:GO has perhaps the most famous system, one with the most written about it (see Youtube), but other games have other systems.
To blend multiple different curves of displacement you can either do it in code (multiplying Quaternions perhaps), or else stack a hierarchy of transforms and drive the local rotation of each Transform to do what you need: flip, rotate, bend, pull back, etc.
I will take a look at the unity package you’ve provided, thanks.
However, the hand part you mentioned at the start is quite confusing. So if I got it right, you’re basically saying that the approach of simply “start recovery after X seconds and target an bullet position to return to” generally speaking is a completely incorrect way of thinking about recoil in general?
As I’m not close to the point of realism when it comes to these mechanics, I’ve just been trying to match the recoil of games like Apex Legends, Call of Duty, etc. But for example on a FPS like Apex, after each shot you can notice a slight “settle” after each shot, but it’s not a full recovery. Then once you finish a bullet spray, the camera goes straight down to the pre-shooting camera rotation. I feel like my project has that (in a poorly made way by me), but with the huge problem in my main post.
The important thing about recoil and the magnitude of forces involved is that the gun essentially goes from at rest to full speed climbing and spinning in one frame of gameplay.
The recoil force only occurs during the time the bullet is accelerating inside the barrel. The moment the bullet leaves the barrel force on the gun drops back to zero, as all the remaining propellent gases exit without having to shove the bullet out. But now the gun has a bunch of speed upwards and usually spin backwards, caused because the barrel sight line is not aligned with the vertical center of mass, so there is a net torque applied.
A single bullet’s recoil isn’t like a firehose where the longer it sprays water the more it climbs, although shot-to-shot on a repeating gun can appear to have this accumulating and building effect, which CS:GO uses along with a 2D distortion pattern on subsequent shots, a pattern custom to each gun type, which lets you master it to keep all of your bullets on target through the burst.
Like I said, if you’re asking how to blend various offsets, such as instantaneous climb and gradual climb, putting them on separate Transforms is probably the easieset.
Are you trying to simulate this with physics forces? If so, don’t. This isn’t how you do those kind of effects, they only feel “physical” but are rarely actually based on physical behaviour but rather what “feels” good.
In its simplest form a recoil effect is simply an offset to where the projectile will travel. Say you aim at a target and we simplify the recoil to merely the gun going up (along Y axis) with no sideways motion, what happens technically is:
var actualShootDirection = directionToTarget + recoilOffset;
recoilOffset.y += 0.1f; // add some recoil after every shot
// fire projectile here
// later in the Update loop there's logic that lerps the recoilOffset back to 0,0,0
// in its simplest form that could be this:
if (recoilOffset.y > 0f)
recoilOffset.y += -0.01f;
else
recoilOffset.y = 0f;
This means the recoil comes down linearly over the next 9 frames. If the weapon shoots faster than that, say every 6 frames, then the recoil gradually increases.
There’s of course more complexity, lerping is just one way to make it smoother, a common trick being to pass in a constant lerp value (or Time.deltaTime) so that the closer the offset is to zero the slower the recovery. You can also go nuts with this using AnimationCurve for instance to visually draw how the recoil recovery should behave.
Pretty sure that’s what most games do. The rest they cover up with superior animation, audio, visual fx, shaders to make if “feel” right.
You can use the same recoil offset to tell the camera to rotate by an appropriate amount upwards and follow the actual aim+recoil direction.
If you make the camera’s gameObject a child of another empty gameObject then your mouse look script can modify the parent object and your recoil script can modify the child. Both will work independently and the recoil will be purely cosmetic. Your bullet can fire in the direction of the parent object independent of the recoil. This is how Quake did it. The larger weapons tended to have larger kick but lower fire rate and so the player doesn’t notice that the recoil isn’t effecting their aim.
^ ^ ^ This is an extremely good point. There’s actually a few different things:
shaking the camera itself
climbing the weapon instantaneously
climbing the weapon gradually as you blast out the magazine’s contents
climbing the aimpoint (does not need to correspond to the weapon!)
returning the aimpoint to zero
All of these things can be ganged together, or treated and tracked separately.
They can also have an actual gameplay impact or not.
Another common thing is an accuracy reticle, like four tickmarks that widen when you are in an “inaccurate” condition (immediately post firing, or such as when you’re running / jumping), and narrow when you focus or even ADS. Those systems usually just add random offsets to the final bullet path.
As mentioned above, you should separate out every force. They should be in an array that is blended (added) together to get the final result. Each could have their own AnimationCurve to adjust and offsets. For example, one entry in the array could be “earthquake”, that when not 0,0,0, will affect the final result without requiring a GameObject hierarchy setup.
Use Cinemachine VirtualCamera which has this blending for active cameras. And use Animation Rigging package to free yourself of the hierarchy parent/child requirements.
You can have a “noise camera” (CinemachineVirtualCamera with Noise setting) with AmplitudeGain set to 0 until you fire, then set it quickly to 1 then slowly back to zero.
i’m pretty embarrassed to post my code but this is my main recoil script. i have other classes that are linked to this but i guess this is the main one.
using UnityEngine;
public class Recoil : MonoBehaviour
{
public PlayerLook playerLook;
private Weapon currentWeapon;
[Header("Input Cancellation")]
[Tooltip("if mouse delta magnitude exceeds this during recoil, cancel recovery and hand off to base rotation .")]
public float inputCancelThreshold = 0.08f;
// Internal recoil state (target offsets)
private float targetRecoilPitch; // negative means the kick will go up
private float targetRecoilYaw;
// smoothed offsets we actually need to apply
private float smoothRecoilPitch;
private float smoothRecoilYaw;
// velocities for SmoothDamp
private float kickPitchVel, kickYawVel;
private float recoverPitchVel, recoverYawVel;
private float lastShotTime;
private bool triggerHeld = false;
private float lastTriggerReleaseTime = -Mathf.Infinity;
private float lastTriggerPressTime = -Mathf.Infinity;
private float previousTriggerPressTime = -Mathf.Infinity;
private bool isSpammingSlowWeapon = false;
// input tracking
private float lastMouseDeltaMag;
private float verticalCurrentMultiplier = 1f;
public float GetSmoothPitch() { return smoothRecoilPitch; }
public float GetSmoothYaw() { return smoothRecoilYaw; }
public float GetLastShotTime()
{
return lastShotTime;
}
// poorly made method for triggerheld for recoil recovery.
public void SetTriggerHeld(bool held)
{
if (!triggerHeld && held)
{
previousTriggerPressTime = lastTriggerPressTime;
lastTriggerPressTime = Time.time;
if (currentWeapon != null && currentWeapon.secondsPerShot >= 1.0f)
{
if (previousTriggerPressTime > 0)
{
float timeBetweenClicks = lastTriggerPressTime - previousTriggerPressTime;
isSpammingSlowWeapon = timeBetweenClicks < (currentWeapon.secondsPerShot * 0.5f);
}
}
}
else if (triggerHeld && !held)
{
lastTriggerReleaseTime = Time.time;
isSpammingSlowWeapon = false;
}
triggerHeld = held;
}
void Update()
{
if (currentWeapon == null) return;
// raw input only
float dx = Input.GetAxisRaw("Mouse X");
float dy = Input.GetAxisRaw("Mouse Y");
lastMouseDeltaMag = Mathf.Sqrt(dx * dx + dy * dy);
HandleRecoilRecovery();
ApplyOffsetsToPlayerLook();
}
public void AddRecoil()
{
if (currentWeapon == null) return;
float now = Time.time;
float timeSinceLastShot = now - lastShotTime;
if (timeSinceLastShot > currentWeapon.secondsPerShot * currentWeapon.burstResetTimeFactor) // using "currentWeapon" which is from my Weapon.cs script that is attached to every gun, so each gun has unique values
{
verticalCurrentMultiplier = currentWeapon.verticalStartMultiplier;
}
else
{
verticalCurrentMultiplier = Mathf.Min(
verticalCurrentMultiplier + currentWeapon.verticalRampPerShot,
currentWeapon.verticalMaxMultiplier
);
}
lastShotTime = now;
float baseVerticalKick = Random.Range(currentWeapon.verticalKickRange.x, currentWeapon.verticalKickRange.y);
float vKick = baseVerticalKick * verticalCurrentMultiplier;
targetRecoilPitch -= vKick;
float yawKick = Random.Range(currentWeapon.horizontalKickRange.x, currentWeapon.horizontalKickRange.y);
targetRecoilYaw += yawKick;
}
// this is a fix so that when you are fighting recoil (pushing mouse down), it still recovers back to default, not below the first bullet fired in the loop.
public void AbsorbRecoil(ref float mouseX, ref float mouseY)
{
bool changed = false;
const float eps = 0.0001f;
// pitch (pitch is veritcal)
if ((targetRecoilPitch < -eps || smoothRecoilPitch < -eps) && mouseY < -eps)
{
float debtY = Mathf.Max(-targetRecoilPitch, -smoothRecoilPitch);
float inputY = -mouseY;
float absorbY = Mathf.Min(debtY, inputY);
if (absorbY > 0)
{
targetRecoilPitch += absorbY;
smoothRecoilPitch += absorbY;
mouseY += absorbY;
if (targetRecoilPitch > 0f) targetRecoilPitch = 0f;
if (smoothRecoilPitch > 0f) smoothRecoilPitch = 0f;
kickPitchVel = 0f;
recoverPitchVel = 0f;
changed = true;
}
}
// yaw
float recoilX = (Mathf.Abs(targetRecoilYaw) >= Mathf.Abs(smoothRecoilYaw))
? targetRecoilYaw
: smoothRecoilYaw;
bool opposingX = (recoilX > eps && mouseX < -eps) || (recoilX < -eps && mouseX > eps);
if (opposingX)
{
float debtX = Mathf.Max(Mathf.Abs(targetRecoilYaw), Mathf.Abs(smoothRecoilYaw));
float inputX = Mathf.Abs(mouseX);
float absorbX = Mathf.Min(debtX, inputX);
if (absorbX > 0)
{
float sign = Mathf.Sign(recoilX);
targetRecoilYaw -= absorbX * sign;
smoothRecoilYaw -= absorbX * sign;
mouseX -= absorbX * Mathf.Sign(mouseX);
if (Mathf.Abs(targetRecoilYaw) < eps) targetRecoilYaw = 0f;
if (Mathf.Abs(smoothRecoilYaw) < eps) smoothRecoilYaw = 0f;
kickYawVel = 0f;
recoverYawVel = 0f;
changed = true;
}
}
if (changed)
{
ApplyOffsetsToPlayerLook();
}
}
public void ApplyOffsetsToPlayerLook()
{
if (!playerLook) return;
playerLook.RecoilPitchOffset = smoothRecoilPitch;
playerLook.RecoilYawOffset = smoothRecoilYaw;
}
private void HandleRecoilRecovery()
{
if (currentWeapon == null) return;
bool hasSignificantRecoil = Mathf.Abs(smoothRecoilPitch) > 0.05f || Mathf.Abs(smoothRecoilYaw) > 0.05f;
if (lastMouseDeltaMag > inputCancelThreshold && hasSignificantRecoil)
{
// Hand off the recoil which is here atm - VERY IMPORTANT, need to put it onto the base rotation of the player
// to make sure there's no "snap" visually, but any lingering recoil needs to go. - cant tell if this is right
if (playerLook)
{
playerLook.AddBaseRotation(smoothRecoilPitch, smoothRecoilYaw);
}
// reset everything completely, cant leave lingering values
targetRecoilPitch = 0f;
targetRecoilYaw = 0f;
smoothRecoilPitch = 0f;
smoothRecoilYaw = 0f;
kickPitchVel = kickYawVel = recoverPitchVel = recoverYawVel = 0f;
// should not be doing recover so need a return here
return;
}
// ----------------------------------
float dt = Mathf.Min(Time.deltaTime, 1f / 30f);
smoothRecoilPitch = Mathf.SmoothDamp(
smoothRecoilPitch,
targetRecoilPitch,
ref kickPitchVel,
currentWeapon.kickSmoothTime,
currentWeapon.kickMaxSpeedDegPerSec,
dt
);
smoothRecoilYaw = Mathf.SmoothDamp(
smoothRecoilYaw,
targetRecoilYaw,
ref kickYawVel,
currentWeapon.kickSmoothTime,
currentWeapon.kickMaxSpeedDegPerSec,
dt
);
// 3) gating cadence - since there are slow weapons in the game
if (triggerHeld)
{
bool isSlowFiringSniper = currentWeapon.secondsPerShot >= 1.0f;
if (!isSlowFiringSniper)
{
float timeSinceShot = Time.time - lastShotTime;
float grace = Mathf.Max(0f, currentWeapon.secondsPerShot * currentWeapon.recoverGraceFactor);
bool streamActive = timeSinceShot < grace;
if (streamActive) // current bullet stream which is a loop. need to make sure game knows "what's a stream" so it doesnt cancel recoil out and overpower it
{
recoverPitchVel = 0f;
recoverYawVel = 0f;
return;
}
}
}
else
{
bool isSlowFiringSniper = currentWeapon.secondsPerShot >= 1.0f; // secondsPerShot is fire Rate of the specific weapon
bool canRecover = isSlowFiringSniper || (Time.time >= lastTriggerReleaseTime + currentWeapon.recoveryDelay);
if (!canRecover)
{
recoverPitchVel = 0f;
recoverYawVel = 0f;
return;
}
}
// now it need to "softly" recover toward zero
float newTargetPitch = Mathf.SmoothDamp(
targetRecoilPitch, 0f, ref recoverPitchVel, currentWeapon.verticalRecoverTime);
float newTargetYaw = Mathf.SmoothDamp(
targetRecoilYaw, 0f, ref recoverYawVel, currentWeapon.horizontalRecoverTime);
if (Mathf.Abs(newTargetPitch) < 0.001f) newTargetPitch = 0f;
if (Mathf.Abs(newTargetYaw) < 0.001f) newTargetYaw = 0f;
targetRecoilPitch = newTargetPitch;
targetRecoilYaw = newTargetYaw;
}
public void ResetRecoil()
{
targetRecoilPitch = targetRecoilYaw = 0f;
smoothRecoilPitch = smoothRecoilYaw = 0f;
kickPitchVel = kickYawVel = recoverPitchVel = recoverYawVel = 0f;
if (playerLook) // playerLook is cinemachine camera script
{
playerLook.RecoilPitchOffset = 0f;
playerLook.RecoilYawOffset = 0f;
}
verticalCurrentMultiplier = currentWeapon != null ? currentWeapon.verticalStartMultiplier : 1f;
}
public void SetCurrentWeapon(Weapon weapon)
{
currentWeapon = weapon;
}
}
i'm pretty much deleting majority of it so i can redo recovery entirely. my problem is, i keep finding bugs or issues, so i keep chucking more and more variables on top. so i just have 15 variables constantly fixing new issues. yet somehow 80% of it still works like i want it to. but i'm just reusing every method i find on unity docs that are relevant to what i need and then i keep reusing the same things like MoveTowards, SmoothDamp, Mathf.Abs etc because I dont know anything better.
My bad, but when you talk about aimPoint, do you mean bullet origin like where the bullets fire towards? since my bullets are actually real physics with gravity and velocity (no rigidbody) and I use this as my origin:
ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));
Vector3 aimDirection = ray.direction;
I suppose my aimpoint term is ambiguous… in a hitscan gun it would be the boresight line. If you have gravity and bullet drop it is something else, eg, where you point the barrel to make the bullet land on the target, etc.
My main point there was that every gun recoil system is different and composed of some number separate components, sometimes linked together, sometimes not linked. For example, sometimes a recent gunfire event only briefly reduces accuracy (which can again be done a zillion different ways) completely separately from the visual recoil.