Projectile System
Configurable projectile with speed, damage, lifetime, and optional homing. Integrates with Object Pool for zero-alloc shooting.
How to Use
Create a projectile prefab with a Trigger Collider
Attach Projectile script
Set speed, damage, and lifetime
Spawn from weapon: Instantiate(bulletPrefab, muzzle.position, muzzle.rotation)
For pooling: ObjectPool.Instance.Spawn("bullet", pos, rot)
Enable homing for seeking missiles
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
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; }
}