FPS Weapon Mechanics Prototype

Procedural recoil physics, dynamic spread bloom, and modular ScriptableObject firearm systems in Unity 3D

Unity 3DUnity 3DC#C#Custom PhysicsCustom PhysicsProcedural AnimationProcedural AnimationScriptableObjectsScriptableObjectsParticle VFXParticle VFX
Recoil Model
Procedural Spring-Damper
Weapon Architecture
Modular ScriptableObjects
Ballistics Engine
Raycast & Particle VFX

// Weapon Systems & Visual Demonstration

FPS Weapon Mechanics In-Engine Screenshot
Unity 3D Procedural Viewmodel, Recoil Physics & Dynamic Ballistic BloomTactical Weapon Sandbox

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;
}