
Last week, I created some procedural animations using Inverse Kinematics, and created some interesting spider-like creatures. So while on the topic of procedural animations, I thought it would be cool to implement gun recoil to create a more diverse range of animations when shooting a gun in the first person.
The obvious first step was to create a first person controller. The simplest and most common way of doing this (especially in Unity) is to create a capsule as the player, and attach a camera to it as a child. Rotating the mouse up/down will rotate the camera itself, and rotating the mouse left/right will rotate the entire capsule. This way, the forward direction of the player will always be where the camera is facing, making later programming tasks much easier.

Now with a basic character controller out of the way, the next thing to work on was the smooth motion of the gun when moving around, as well as the aiming mechanic.
To move the position of the weapon was easy... Just simply lerp to a position while the player is walking, or aiming.
When not aiming...
Vector3 Pos = new Vector3(-RightMoveAction.action.ReadValue<float>() * weaponMovementAmount, 0, -ForwardMoveAction.action.ReadValue<float>() * weaponMovementAmount);
weapon.transform.localPosition = Vector3.Lerp(weapon.transform.localPosition, weaponPos + Pos, Time.deltaTime * weaponLerpSpeed);
When aiming...
weapon.transform.position = Vector3.Lerp(weapon.transform.position, aimPos.position, Time.deltaTime * aimSpeed);
The rotation was a little harder to implement. Since the weapon is a child of the camera, it was not as simple as lerping the rotation of the weapon to the rotation of the camera. Basically, whenever there is input from the mouse, add that delta to the rotation, and lerp it so it's smooth. And while the player is aiming, the rotation will be static, facing straight out from the camera.
When not aiming...
Quaternion weaponQuatX = Quaternion.AngleAxis(-MouseAction.action.ReadValue<Vector2>().x * weaponRotationAmount, Vector3.up);
Quaternion weaponQuatY = Quaternion.AngleAxis(-MouseAction.action.ReadValue<Vector2>().y * weaponRotationAmount, Vector3.left);
weapon.transform.localRotation = Quaternion.Lerp(weapon.transform.localRotation, weaponQuatX * weaponQuatY, Time.deltaTime * weaponLerpSpeed);
When aiming...
weapon.transform.localRotation = Quaternion.Lerp(weapon.transform.localRotation, Quaternion.identity, Time.deltaTime * aimSpeed);
On top of this, I wanted to lerp the field of view of the camera when aiming, so the camera would smoothly zoom in when the player aims the weapon.
m_mainCamera.fieldOfView = Mathf.Lerp(m_mainCamera.fieldOfView, aimFOVAmount, FOVLerpSpeed * Time.deltaTime);
Before I moved onto creating the shoot function, I made a simple bullet hole that would spawn in, and disappear after some time using IEnumerators...
IEnumerator Timer()
{
float waitTime = m_disappearTimer;
float elapsedTime = 0;
while (elapsedTime < waitTime)
{
elapsedTime += Time.deltaTime;
yield return null;
}
StartCoroutine(Disappear());
yield return null;
}
IEnumerator Disappear()
{
float waitTime = m_invisTime;
float elapsedTime = 0;
while(elapsedTime < waitTime)
{
elapsedTime += Time.deltaTime;
Color newCol = m_spriteRenderer.color;
newCol.a = Mathf.Lerp(1, 0, elapsedTime / waitTime);
m_spriteRenderer.color = newCol;
yield return null;
}
Destroy(gameObject);
yield return null;
}
When the bullet spawns, it runs the "Timer" enumerator, and when the timer ends, it starts the "Disappear" enumerator which slowly reduces the alpha of the sprite, until it is invisible, then it destroys itself.
I then made a simple sway script that acts as the recoil when the gun is being fired. It is basically just lerping between random values that you can set in the editor.
Sway.cs Script
private Vector3 currentRotation;
private Vector3 targetRotation;
private Vector3 originalPosition;
private Vector3 currentPosition;
private Vector3 targetPosition;
[Header("Rotation")]
[SerializeField] private float recoilX;
[SerializeField] private float recoilY;
[SerializeField] private float recoilZ;
[Header("Position")]
[SerializeField] private float recoilPosZ;
[Header("Settings")]
[SerializeField] private float snappiness;
[SerializeField] private float returnSpeed;
[Tooltip("If the recoil affects the position")]
[SerializeField] private bool modifyPosition;
// Start is called before the first frame update
void Start()
{
originalPosition = transform.localPosition;
}
// Update is called once per frame
void Update()
{
targetRotation = Vector3.Lerp(targetRotation, Vector3.zero, returnSpeed * Time.deltaTime);
currentRotation = Vector3.Slerp(currentRotation, targetRotation, snappiness * Time.deltaTime);
transform.localRotation = Quaternion.Euler(currentRotation);
if (modifyPosition)
{
targetPosition = Vector3.Lerp(targetPosition, originalPosition, returnSpeed * Time.deltaTime);
currentPosition = Vector3.Lerp(currentPosition, targetPosition, snappiness * Time.deltaTime);
transform.localPosition = currentPosition;
}
}
Next, I just had to make a script that allows the gun to actually shoot. This script handles the spawning of the muzzle flash, bullet hole and sound effect, as well as calling the functions to sway the weapon and camera. I also added an option to constant fire, meaning if the player held down the left mouse button, it would constantly fire like an automatic weapon.
RecoilController.cs script
public class RecoilController : MonoBehaviour
{
private AudioSource m_audioSource;
[Header("Gun Fire Settings")]
[SerializeField] private Sway m_cameraSway;
[SerializeField] private Sway m_gunSway;
[SerializeField] public Sway m_camSwaySettings;
[SerializeField] private GameObject muzzleFlash;
[SerializeField] private Transform flashTransform;
[SerializeField] private AudioClip shootSound;
[SerializeField] private bool m_constantFire;
[SerializeField] private float m_constantFireTimer = 0.3f;
[Header("Other")]
[SerializeField] public GameObject m_weapon;
[SerializeField] public AudioClip m_loadWeaponSound;
[SerializeField] private GameObject bulletHole;
[SerializeField] private GameObject m_mainCamera;
// Start is called before the first frame update
void Start()
{
m_audioSource = GetComponent<AudioSource>();
}
public void RecoilFire()
{
m_cameraSway.SetTargetRotation();
m_gunSway.SetTargetRotation();
m_gunSway.SetTargetPosition();
if (muzzleFlash || flashTransform)
{
Vector3 flashRot = new Vector3(-30, -90, 0);
Instantiate(muzzleFlash, flashTransform.position, flashTransform.rotation * Quaternion.Euler(flashRot), flashTransform.parent);
m_audioSource.PlayOneShot(shootSound);
}
if (bulletHole)
{
RaycastHit hit;
if (Physics.Raycast(m_mainCamera.transform.position, m_mainCamera.transform.forward, out hit))
{
if (hit.collider)
{
Instantiate(bulletHole, hit.point + (hit.normal * 0.01f), Quaternion.FromToRotation(Vector3.forward, hit.normal));
}
}
}
}
public void FireWeapon()
{
if (m_constantFire)
StartCoroutine(ConstantFire());
else
RecoilFire();
}
public IEnumerator ConstantFire()
{
float waitTime = m_constantFireTimer;
float elapsedTime = 0;
RecoilFire();
while (elapsedTime < waitTime)
{
elapsedTime += Time.deltaTime;
yield return null;
}
StartCoroutine(ConstantFire());
yield return null;
}
public void CancelConstantFire()
{
StopAllCoroutines();
}
}
Because I'm not an artist, I had to use assets from the Unity Asset Store. Here is a list of assets I used...
Guns Pack: Low Poly Guns Collection
The final results...