Part of these game systems:
beginner Health & Combat

Projectile System

Configurable projectile with speed, damage, lifetime, and optional homing. Integrates with Object Pool for zero-alloc shooting.

Unity 2022.3+ · 2.2 KB · Projectile.cs

How to Use

1

Create a projectile prefab with a Trigger Collider

2

Attach Projectile script

3

Set speed, damage, and lifetime

4

Spawn from weapon: Instantiate(bulletPrefab, muzzle.position, muzzle.rotation)

5

For pooling: ObjectPool.Instance.Spawn("bullet", pos, rot)

6

Enable homing for seeking missiles

7

Assign impact VFX prefab for hit effects

Features

  • Configurable speed, damage, and lifetime per projectile
  • Optional homing behavior with adjustable strength and range
  • LayerMask-based hit filtering for selective collision
  • Impact VFX spawning on hit with automatic cleanup
  • IPoolable interface for zero-allocation object pool integration
  • UnityEvent callback on hit with the struck GameObject

When to Use This

Essential for shooters, tower defense, and any game with ranged attacks — bullets, arrows, fireballs, or seeking missiles. Works great in both FPS and top-down shooter contexts. Combine with Object Pool for high-volume shooting without garbage collection spikes.

Common Mistakes

The projectile needs a Collider with 'Is Trigger' checked, and targets need colliders on the correct hitLayers. If homing doesn't work, ensure target objects are on the hitLayers mask and within homingRange. When using object pooling, call SetPoolTag() after spawning so the projectile returns to the pool instead of being destroyed.

Source Code

Projectile.cs
C#
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// Configurable projectile with speed, damage, lifetime, and optional homing.
/// Implements IPoolable for object pool integration.
/// </summary>
public class Projectile : MonoBehaviour, IPoolable
{
    [Header("Movement")]
    [SerializeField] private float speed = 20f;
    [SerializeField] private float lifetime = 5f;

    [Header("Damage")]
    [SerializeField] private float damage = 10f;
    [SerializeField] private LayerMask hitLayers = ~0;

    [Header("Homing")]
    [SerializeField] private bool isHoming = false;
    [SerializeField] private float homingStrength = 5f;
    [SerializeField] private float homingRange = 20f;

    [Header("Impact")]
    [SerializeField] private GameObject impactEffect;
    [SerializeField] private bool destroyOnHit = true;

    [Header("Events")]
    public UnityEvent<GameObject> OnHit;

    private float timer;
    private Transform homingTarget;
    private string poolTag;

    /// <summary>Set the pool tag for despawning.</summary>
    public void SetPoolTag(string tag) => poolTag = tag;

    private void OnEnable()
    {
        timer = 0f;

        if (isHoming)
            FindHomingTarget();
    }

    private void Update()
    {
        timer += Time.deltaTime;
        if (timer >= lifetime)
        {
            DestroyProjectile();
            return;
        }

        // Homing
        if (isHoming && homingTarget != null)
        {
            Vector3 dir = (homingTarget.position - transform.position).normalized;
            Vector3 newForward = Vector3.Lerp(transform.forward, dir, homingStrength * Time.deltaTime);
            transform.forward = newForward;
        }

        // Move forward
        transform.position += transform.forward * speed * Time.deltaTime;
    }

    private void OnTriggerEnter(Collider other)
    {
        if (((1 << other.gameObject.layer) & hitLayers) == 0) return;

        // Apply damage
        HealthSystem health = other.GetComponent<HealthSystem>();
        if (health != null)
            health.TakeDamage(damage);

        OnHit?.Invoke(other.gameObject);

        // Spawn impact effect
        if (impactEffect != null)
            Instantiate(impactEffect, transform.position, Quaternion.identity);

        if (destroyOnHit)
            DestroyProjectile();
    }

    private void FindHomingTarget()
    {
        Collider[] colliders = Physics.OverlapSphere(transform.position, homingRange, hitLayers);
        float closest = float.MaxValue;
        homingTarget = null;

        foreach (var col in colliders)
        {
            if (col.gameObject == gameObject) continue;
            float dist = Vector3.Distance(transform.position, col.transform.position);
            if (dist < closest)
            {
                closest = dist;
                homingTarget = col.transform;
            }
        }
    }

    private void DestroyProjectile()
    {
        if (!string.IsNullOrEmpty(poolTag) && ObjectPool.Instance != null)
            ObjectPool.Instance.Despawn(poolTag, gameObject);
        else
            Destroy(gameObject);
    }

    // IPoolable
    public void OnSpawn() { timer = 0f; }
    public void OnDespawn() { homingTarget = null; }
}