Shooting Projectiles in Unity: Bullets, Rigidbody Speed and Pooling

Illustration: glowing bullets firing from a blaster with motion streaks toward a target

Shooting is at the heart of a huge number of games, and the first version most people write has problems: bullets pass through walls, fire rate depends on frame rate, and the game stutters after a few hundred shots. This guide covers shooting projectiles in Unity the right way: when to use Rigidbody bullets vs raycasts, a fire rate that’s frame-rate independent, reliable hits for fast bullets, and object pooling so you can fire all day without garbage collection spikes.

Projectile or Hitscan?

  • Projectile: a real object travels through the world. Players can see and dodge it. Good for rockets, arrows, fireballs, slow bullets in shmups and top-down shooters.
  • Hitscan: a raycast hits instantly. No travel time. Good for fast guns (rifles, lasers) in first person shooters.

Many games mix them: hitscan for the gun, plus a tracer effect for looks. Choose based on whether players should be able to dodge.

The Gun: Aiming and Fire Rate

using UnityEngine;
using UnityEngine.InputSystem;

public class Gun : MonoBehaviour
{
    [SerializeField] InputActionReference fireAction;
    [SerializeField] Transform muzzle;
    [SerializeField] Bullet bulletPrefab;
    [SerializeField] float fireRate = 8f;        // shots per second
    [SerializeField] float spreadDegrees = 2f;
    [SerializeField] bool automatic = true;

    float nextShotTime;

    void OnEnable()  => fireAction.action.Enable();
    void OnDisable() => fireAction.action.Disable();

    void Update()
    {
        bool wantsFire = automatic ? fireAction.action.IsPressed()
                                   : fireAction.action.WasPressedThisFrame();
        if (!wantsFire || Time.time < nextShotTime) return;

        nextShotTime = Time.time + 1f / fireRate;
        Shoot();
    }

    void Shoot()
    {
        Quaternion spread = Quaternion.Euler(Random.Range(-spreadDegrees, spreadDegrees),
                                             Random.Range(-spreadDegrees, spreadDegrees), 0f);
        Bullet b = BulletPool.Get(bulletPrefab);
        b.Launch(muzzle.position, muzzle.rotation * spread, gameObject);
    }
}

Fire rate uses a “next allowed time”, so it doesn’t depend on frame rate. At very high fire rates (more shots per second than frames), you’d fire several bullets per frame; for most games one per frame is fine.

Aim the muzzle where the player is looking. In first person, a common trick is to raycast from the camera centre, find the hit point, and rotate the muzzle toward it, so bullets land on the crosshair even though the gun is offset. In top-down games, aim at the mouse position on the ground; the top-down shooter guide shows the mouse-to-world code.

A Rigidbody Bullet

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class Bullet : MonoBehaviour
{
    [SerializeField] float speed = 40f;
    [SerializeField] float lifetime = 3f;
    [SerializeField] int damage = 10;

    Rigidbody rb;
    GameObject owner;
    float dieAt;

    void Awake() => rb = GetComponent<Rigidbody>();

    public void Launch(Vector3 position, Quaternion rotation, GameObject shooter)
    {
        owner = shooter;
        transform.SetPositionAndRotation(position, rotation);
        rb.position = position;
        rb.rotation = rotation;
        rb.linearVelocity = rotation * Vector3.forward * speed;
        rb.angularVelocity = Vector3.zero;
        dieAt = Time.time + lifetime;
    }

    void Update()
    {
        if (Time.time >= dieAt) BulletPool.Release(this);
    }

    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject == owner) return;
        if (collision.collider.TryGetComponent(out IDamageable target))
            target.TakeDamage(new DamageInfo(damage, collision.GetContact(0).point, owner));
        BulletPool.Release(this);
    }
}

Settings on the bullet’s Rigidbody:

  • Use Gravity off for straight bullets, on for arcing grenades and arrows.
  • Collision Detection: Continuous Dynamic (or Continuous Speculative) for fast bullets. More below.
  • Interpolate on, so bullets look smooth at high frame rates.

The IDamageable and DamageInfo types come from the damage system guide. Any target that implements the interface takes damage, without the bullet knowing what it hit.

Put bullets on their own layer and turn off Bullet vs Bullet collisions in the collision matrix, so bullets don’t hit each other. Same for bullets hitting the shooter’s own team.

Fast Bullets That Don’t Miss

At 40 units per second and the default 50 Hz physics, a bullet moves 0.8 units per physics step. Thin walls and small enemies get skipped. Options:

  1. Continuous collision detection. Continuous Dynamic sweeps the collider between steps. It costs more, so use it only on bullets and the player.
  2. Raycast ahead. Each FixedUpdate, cast from the current position along the velocity for the distance travelled this step. If it hits, handle the hit at that point. This is reliable for any speed and cheap:
void FixedUpdate()
{
    float step = rb.linearVelocity.magnitude * Time.fixedDeltaTime;
    if (Physics.Raycast(rb.position, rb.linearVelocity.normalized, out RaycastHit hit, step, hitMask,
                        QueryTriggerInteraction.Ignore))
    {
        // handle hit at hit.point, then release
    }
}

With the raycast approach you can make the bullet’s collider a trigger or remove it entirely. Very fast bullets (hundreds of units per second) should just be hitscan.

Hitscan With a Raycast

void ShootHitscan()
{
    Ray ray = new Ray(cam.transform.position, cam.transform.forward);
    if (Physics.Raycast(ray, out RaycastHit hit, 200f, hitMask, QueryTriggerInteraction.Ignore))
    {
        if (hit.collider.TryGetComponent(out IDamageable target))
            target.TakeDamage(new DamageInfo(damage, hit.point, gameObject));
        // spawn an impact effect at hit.point, aligned with hit.normal
    }
}

For visuals, draw a tracer with a LineRenderer or a fast-moving particle from the muzzle to hit.point. The raycast tutorial covers masks, RaycastAll for piercing shots and debugging.

Pooling Bullets

Instantiating and destroying a bullet every shot creates garbage and costs CPU. Pool them with Unity’s built-in ObjectPool<T>:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;

public static class BulletPool
{
    static readonly Dictionary<Bullet, ObjectPool<Bullet>> pools = new();
    static readonly Dictionary<Bullet, ObjectPool<Bullet>> owners = new();

    public static Bullet Get(Bullet prefab)
    {
        if (!pools.TryGetValue(prefab, out var pool))
        {
            pool = new ObjectPool<Bullet>(
                () => Object.Instantiate(prefab),
                b => b.gameObject.SetActive(true),
                b => b.gameObject.SetActive(false),
                b => { if (b) Object.Destroy(b.gameObject); },
                collectionCheck: false, defaultCapacity: 32, maxSize: 256);
            pools[prefab] = pool;
        }
        var bullet = pool.Get();
        owners[bullet] = pool;
        return bullet;
    }

    public static void Release(Bullet b)
    {
        if (b.gameObject.activeSelf && owners.TryGetValue(b, out var pool)) pool.Release(b);
    }
}

The activeSelf check stops a bullet being released twice (for example, hitting something on the same frame its lifetime ends), which would corrupt the pool. When you change scenes, the pooled bullets are destroyed with the scene, so clear the dictionaries on scene load or make the pool a scene component. The object pooling guide explains these trade-offs.

2D Shooting

Everything works the same in 2D with the 2D types: Rigidbody2D, OnCollisionEnter2D or OnTriggerEnter2D, Physics2D.Raycast, and velocity set as transform.right * speed if your sprites face right. Set the Rigidbody2D’s Gravity Scale to 0 and Collision Detection to Continuous for fast bullets.

🔧 Weapon and effects packs

Muzzle flashes, impacts and weapon sounds make shooting satisfying. See the Asset Store tools and packs we recommend.

Troubleshooting

Bullets pass through walls or enemies

Use Continuous Dynamic collision detection, or raycast ahead each physics step.

Bullets hit the player who fired them

Spawn the bullet outside the player’s collider, ignore the owner in the hit check, or use layers so bullets don’t collide with their shooter.

Fire rate changes with frame rate

Use a next-shot time as shown, not a frame counter.

Bullets fly off at odd angles

The muzzle’s forward axis isn’t pointing down the barrel. Check the muzzle’s blue arrow in Local mode.

Game stutters after lots of shooting

Garbage from Instantiate and Destroy. Pool bullets and impact effects.

FAQ

Should bullets be triggers or solid colliders?

Triggers if you don’t want bullets to push things. Solid colliders if they should bump physics objects. With the raycast-ahead method you may not need a collider at all.

How fast can Rigidbody bullets go?

With continuous detection, quite fast. Beyond roughly 100 to 200 units per second, use raycast-ahead or hitscan.

How do I make shotgun spread?

Loop and fire several bullets in one Shoot call, each with its own random spread rotation.

Should I use AddForce to launch bullets?

Setting linearVelocity directly is simpler and gives an exact speed. AddForce with ForceMode.VelocityChange works the same way.

How do I stop bullets hitting each other?

Put them on a Bullet layer and untick Bullet x Bullet in the collision matrix.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *