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

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