FPS Weapon Mechanics Prototype
Procedural recoil physics, dynamic spread bloom, and modular ScriptableObject firearm systems in Unity 3D
// Weapon Systems & Visual Demonstration

Project overview
The FPS Weapon Mechanics Prototype is a technical demonstration built in Unity 3D and C# focused on creating responsive, tactile first-person shooter gunplay without relying on pre-baked keyframed animation clips.
The architecture uses procedural physics, decoupled ScriptableObject configuration assets, and multi-variable ballistic dispersion formulas.
Core gunplay systems
| System | Component | Technical implementation |
|---|---|---|
| Procedural recoil | Spring-damper physics | Second-order rotational and translational kickback curves with dynamic return damping |
| Dynamic spread bloom | Ballistics calculator | Multi-variable dispersion cone affected by movement velocity, player stance, and continuous fire |
| Modular weapon pipeline | ScriptableObject assets |
Data-driven firearm definitions (fire rate, muzzle velocity, damage, recoil impulse, reload times) |
| Ballistics and hitscan | Hybrid raycast engine | High-velocity raycast projection with bullet drop calculation and penetration testing |
| Weapon sway and rigging | Procedural IK rigging | Mouse delta smoothing and procedural breathing sway applied to weapon viewmodel bones |
Technical implementation details
1. Procedural spring-damper recoil physics
Rather than blending static keyframed animations, the weapon controller computes positional and rotational displacement using second-order spring physics equations:
View C# implementation: WeaponRecoilController
public class WeaponRecoilController : MonoBehaviour
{
[Header("Spring Damping Settings")]
[SerializeField] private float snappiness = 15f;
[SerializeField] private float returnSpeed = 18f;
[Header("Recoil Kick Vectors")]
[SerializeField] private Vector3 kickRotation = new(-2.5f, 0.8f, 0.4f);
[SerializeField] private Vector3 kickPosition = new(0f, 0f, -0.05f);
private Vector3 currentRecoilRotation;
private Vector3 targetRecoilRotation;
private Vector3 currentRecoilPosition;
private Vector3 targetRecoilPosition;
void Update()
{
// Interpolate target vectors toward resting point (zero)
targetRecoilRotation = Vector3.Lerp(targetRecoilRotation, Vector3.zero, returnSpeed * Time.deltaTime);
currentRecoilRotation = Vector3.Slerp(currentRecoilRotation, targetRecoilRotation, snappiness * Time.deltaTime);
transform.localRotation = Quaternion.Euler(currentRecoilRotation);
targetRecoilPosition = Vector3.Lerp(targetRecoilPosition, Vector3.zero, returnSpeed * Time.deltaTime);
currentRecoilPosition = Vector3.Lerp(currentRecoilPosition, targetRecoilPosition, snappiness * Time.deltaTime);
transform.localPosition = currentRecoilPosition;
}
public void ApplyFireRecoil(float recoilMultiplier = 1.0f)
{
float randomHorizontal = Random.Range(-kickRotation.y, kickRotation.y);
targetRecoilRotation += new Vector3(kickRotation.x, randomHorizontal, kickRotation.z) * recoilMultiplier;
targetRecoilPosition += kickPosition * recoilMultiplier;
}
}
2. Multi-variable dynamic spread cone calculation
Bullet dispersion expands dynamically using a multi-factor formula combining stance, movement speed, and sustained fire bloom:
View C# implementation: CalculateCurrentSpread
public float CalculateCurrentSpread(GunData gun, CharacterController controller, bool isAiming, float heat)
{
float spread = gun.baseSpread;
// Movement speed multiplier
float horizontalSpeed = new Vector2(controller.velocity.x, controller.velocity.z).magnitude;
spread += horizontalSpeed * 0.15f;
// Stance modifier
if (controller.isGrounded && Input.GetKey(KeyCode.LeftControl))
{
spread *= 0.65f; // 35% accuracy bonus when crouching
}
else if (!controller.isGrounded)
{
spread *= 2.5f; // Airborne jump penalty
}
// Aim down sights (ADS) modifier
if (isAiming)
{
spread *= 0.4f;
}
// Continuous fire heat accumulation
spread += heat * gun.bloomPerShot;
return Mathf.Clamp(spread, 0.05f, 15.0f);
}
3. Modular ScriptableObject architecture
All weapon parameters (fire rates, bullet speeds, recoil profiles, magazine sizes, audio clips, and muzzle VFX) are encapsulated inside immutable GunData ScriptableObject assets. Designing a new weapon or adjusting game balance requires creating an asset file without modifying codebase scripts:
View C# implementation: GunData ScriptableObject
[CreateAssetMenu(fileName = "NewGunData", menuMenuName = "Weapons/Gun Data")]
public class GunData : ScriptableObject
{
public string weaponName;
public float damage;
public float fireRate;
public float maxRange;
public int magazineSize;
public float reloadDuration;
[Header("Dispersion")]
public float baseSpread;
public float bloomPerShot;
public float bloomDecaySpeed;
[Header("Audio & Visuals")]
public AudioClip fireSound;
public GameObject muzzleFlashPrefab;
public GameObject bulletImpactPrefab;
}