First Person Camera Pro
Advanced FPS camera with head bob, weapon sway, lean, recoil system, and FOV zoom for immersive first person gameplay.
How to Use
Attach to your Camera (child of player)
Adjust mouse sensitivity and vertical clamp
Head bob activates automatically when WASD keys are pressed
Weapon sway follows mouse movement — tune amount and smoothing
Press Q/E to lean left/right
Call AddRecoil(vertical, horizontal) from your weapon script on fire
Right-click to zoom (ADS), sprint widens FOV automatically
Press Escape to toggle cursor lock
Features
- Mouse look with configurable sensitivity and vertical clamp angles
- Head bob with separate X/Y amplitude and sprint multiplier
- Weapon sway that responds to mouse movement with smoothing
- Q/E lean system with configurable angle, offset, and smoothing
- Recoil system with snap and return speeds for weapon integration
- Dynamic FOV transitions for zoom (ADS), sprint, and default states
When to Use This
Built for first-person shooters and horror games that need immersive camera feel beyond basic mouse look. Essential when your FPS needs head bob, weapon sway, ADS zoom, and recoil — all systems that typically require separate scripts but are unified here.
Common Mistakes
The camera must be a child of the player body — yaw rotation is applied to transform.parent, so attaching directly to the root object breaks horizontal rotation. Call SetSprinting() from your player controller every frame or head bob and FOV won't react to sprint state. Recoil values from AddRecoil() are additive, so firing rapidly without tuning recoilReturnSpeed will cause the camera to drift upward permanently.
Source Code
using UnityEngine;
public class FirstPersonCameraPro : MonoBehaviour
{
[Header("Mouse Look")]
[SerializeField] private float mouseSensitivity = 2f;
[SerializeField] private float verticalClampMin = -85f;
[SerializeField] private float verticalClampMax = 85f;
[SerializeField] private bool lockCursor = true;
[Header("Head Bob")]
[SerializeField] private bool enableHeadBob = true;
[SerializeField] private float bobFrequency = 8f;
[SerializeField] private float bobAmplitudeX = 0.02f;
[SerializeField] private float bobAmplitudeY = 0.04f;
[SerializeField] private float sprintBobMultiplier = 1.4f;
[Header("Weapon Sway")]
[SerializeField] private bool enableWeaponSway = true;
[SerializeField] private float swayAmount = 0.02f;
[SerializeField] private float swayMaxAmount = 0.06f;
[SerializeField] private float swaySmoothing = 6f;
[Header("Lean")]
[SerializeField] private bool enableLean = true;
[SerializeField] private KeyCode leanLeftKey = KeyCode.Q;
[SerializeField] private KeyCode leanRightKey = KeyCode.E;
[SerializeField] private float leanAngle = 15f;
[SerializeField] private float leanOffset = 0.4f;
[SerializeField] private float leanSmoothing = 8f;
[Header("Recoil")]
[SerializeField] private float recoilReturnSpeed = 8f;
[SerializeField] private float recoilSnapSpeed = 15f;
[Header("FOV")]
[SerializeField] private float defaultFOV = 70f;
[SerializeField] private float zoomFOV = 40f;
[SerializeField] private float sprintFOV = 80f;
[SerializeField] private float fovTransitionSpeed = 8f;
[SerializeField] private KeyCode zoomKey = KeyCode.Mouse1;
private Camera cam;
private float xRotation;
private float yRotation;
// Head bob
private Vector3 originalLocalPos;
private float bobTimer;
// Weapon sway
private Vector3 swayPosition;
// Lean
private float currentLean;
private float targetLean;
// Recoil
private Vector3 currentRecoil;
private Vector3 targetRecoil;
// FOV
private float targetFOV;
// External state
private bool isSprinting;
private bool isMoving;
private void Start()
{
cam = GetComponent<Camera>();
if (cam == null) cam = Camera.main;
originalLocalPos = transform.localPosition;
targetFOV = defaultFOV;
if (lockCursor)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
private void Update()
{
HandleMouseLook();
HandleRecoil();
HandleHeadBob();
HandleWeaponSway();
HandleLean();
HandleFOV();
// Apply final rotation
Quaternion lookRotation = Quaternion.Euler(xRotation + currentRecoil.x,
yRotation + currentRecoil.y, currentLean);
transform.localRotation = lookRotation;
// Apply position offsets
Vector3 posOffset = originalLocalPos;
if (enableHeadBob) posOffset += GetBobOffset();
if (enableWeaponSway) posOffset += swayPosition;
if (enableLean) posOffset += transform.right * (currentLean / leanAngle * -leanOffset);
transform.localPosition = Vector3.Lerp(transform.localPosition, posOffset, 10f * Time.deltaTime);
// Cursor toggle
if (Input.GetKeyDown(KeyCode.Escape))
{
bool locked = Cursor.lockState == CursorLockMode.Locked;
Cursor.lockState = locked ? CursorLockMode.None : CursorLockMode.Locked;
Cursor.visible = locked;
}
}
private void HandleMouseLook()
{
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
yRotation += mouseX;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, verticalClampMin, verticalClampMax);
// Apply yaw to parent (player body)
if (transform.parent != null)
transform.parent.rotation = Quaternion.Euler(0f, yRotation, 0f);
}
private void HandleRecoil()
{
targetRecoil = Vector3.Lerp(targetRecoil, Vector3.zero, recoilReturnSpeed * Time.deltaTime);
currentRecoil = Vector3.Slerp(currentRecoil, targetRecoil, recoilSnapSpeed * Time.deltaTime);
}
private void HandleHeadBob()
{
if (!enableHeadBob) return;
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
isMoving = Mathf.Abs(h) > 0.1f || Mathf.Abs(v) > 0.1f;
if (isMoving)
{
float multiplier = isSprinting ? sprintBobMultiplier : 1f;
bobTimer += Time.deltaTime * bobFrequency * multiplier;
}
else
{
bobTimer = Mathf.Lerp(bobTimer, 0f, 4f * Time.deltaTime);
}
}
private Vector3 GetBobOffset()
{
float x = Mathf.Sin(bobTimer) * bobAmplitudeX;
float y = Mathf.Sin(bobTimer * 2f) * bobAmplitudeY;
return new Vector3(x, y, 0f);
}
private void HandleWeaponSway()
{
if (!enableWeaponSway) return;
float mouseX = Input.GetAxis("Mouse X");
float mouseY = Input.GetAxis("Mouse Y");
Vector3 targetSway = new Vector3(
Mathf.Clamp(-mouseX * swayAmount, -swayMaxAmount, swayMaxAmount),
Mathf.Clamp(-mouseY * swayAmount, -swayMaxAmount, swayMaxAmount),
0f
);
swayPosition = Vector3.Lerp(swayPosition, targetSway, swaySmoothing * Time.deltaTime);
}
private void HandleLean()
{
if (!enableLean) return;
targetLean = 0f;
if (Input.GetKey(leanLeftKey)) targetLean = leanAngle;
else if (Input.GetKey(leanRightKey)) targetLean = -leanAngle;
currentLean = Mathf.Lerp(currentLean, targetLean, leanSmoothing * Time.deltaTime);
}
private void HandleFOV()
{
if (cam == null) return;
if (Input.GetKey(zoomKey))
targetFOV = zoomFOV;
else if (isSprinting)
targetFOV = sprintFOV;
else
targetFOV = defaultFOV;
cam.fieldOfView = Mathf.Lerp(cam.fieldOfView, targetFOV, fovTransitionSpeed * Time.deltaTime);
}
/// <summary>
/// Add recoil impulse. Call from weapon system on fire.
/// </summary>
public void AddRecoil(float verticalRecoil, float horizontalRecoil)
{
targetRecoil += new Vector3(-verticalRecoil,
Random.Range(-horizontalRecoil, horizontalRecoil), 0f);
}
/// <summary>
/// Set sprint state from external controller.
/// </summary>
public void SetSprinting(bool sprinting)
{
isSprinting = sprinting;
}
}