Projectiles, damage and VFX
Assets/
Scripts/
Prefabs/
Data/
you already havethis lesson adds
A projectile is a prefab that moves forward, hits something, deals damage and dies. Four responsibilities, one small script.
The Knight fires down an isometric lane; the dummy takes the hit; the projectile goes back into the pool it came from.
The projectile
using UnityEngine;
[RequireComponent(typeof(Collider))]
public class Projectile : MonoBehaviour
{
[SerializeField] private float speed = 18f;
[SerializeField] private float lifeTime = 4f;
[SerializeField] private GameObject hitVfx;
private float damage;
private GameObject owner; // do not hit the shooter
public void Launch(GameObject shooter, float dmg)
{
owner = shooter;
damage = dmg;
Destroy(gameObject, lifeTime); // clean up if it never hits
}
void Update()
{
transform.position += transform.forward * speed * Time.deltaTime;
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject == owner) return;
if (other.TryGetComponent(out CharacterStats target))
target.TakeDamage(damage);
if (hitVfx != null)
Instantiate(hitVfx, transform.position, Quaternion.identity);
Destroy(gameObject);
}
}- 1
, scale 0.3, name it P_Fireball.
- 2
Tick Is Trigger on its Collider — a trigger reports overlaps without physically bouncing.
- 3
Attach Projectile.cs, give it an emissive material, drag it into Assets/Prefabs.
- 4
Delete it from the scene. Prefabs live in Project, not Hierarchy.
Firing it from a skill
// in SkillData.cs
public GameObject projectilePrefab;
// in SkillCaster.TryCast
if (s.projectilePrefab != null)
{
GameObject go = Instantiate(s.projectilePrefab, castPoint.position, castPoint.rotation);
go.GetComponent<Projectile>().Launch(gameObject, s.damage);
}Object pooling — stop the stutter
Instantiate and Destroy allocate memory. Fire 20 projectiles a second and the garbage collector will hitch. A pool reuses the same objects forever.
using System.Collections.Generic;
using UnityEngine;
public class SimplePool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
[SerializeField] private int prewarm = 20;
private readonly Queue<GameObject> pool = new Queue<GameObject>();
void Awake()
{
for (int i = 0; i < prewarm; i++)
{
GameObject go = Instantiate(prefab, transform);
go.SetActive(false);
pool.Enqueue(go);
}
}
public GameObject Get(Vector3 pos, Quaternion rot)
{
GameObject go = pool.Count > 0 ? pool.Dequeue() : Instantiate(prefab, transform);
go.transform.SetPositionAndRotation(pos, rot);
go.SetActive(true);
return go;
}
public void Release(GameObject go)
{
go.SetActive(false);
pool.Enqueue(go);
}
}Replace Destroy(gameObject) in the projectile with a call back to pool.Release(gameObject).
Unity also ships UnityEngine.Pool.ObjectPool<T> if you prefer the built-in version.
Pool anything that spawns more than a few times a second: bullets, hit sparks, damage numbers.
VFX that reads well
Tick Stop Action → Destroy on the Particle System so one-shot effects clean themselves up.
Impact sparks last 0.2–0.4 s. Longer than that and the screen turns to soup.
A tiny camera shake (0.1 s) plus a 0.05 s hit-stop sells the impact better than any particle.
using System.Collections;
IEnumerator HitStop(float seconds)
{
Time.timeScale = 0.05f;
yield return new WaitForSecondsRealtime(seconds); // Realtime! timeScale is frozen
Time.timeScale = 1f;
}
// call it
StartCoroutine(HitStop(0.05f));Common mistakes
The projectile hits the shooter first: it spawns inside the Knight's own collider and OnTriggerEnter fires on the caster. Spawn it from a castPoint just in front of the body, and ignore hits whose transform.root is the caster.
A pooled projectile comes back with its old state: reset position, rotation and every timer in OnEnable, or the second shot dies at once or flies the wrong way.
Destroy(gameObject, lifeTime) is still there after switching to the pool, so a projectile that misses destroys the pool's object and the pool runs dry. Replace both Destroy calls with pool.Release, the timed one through Invoke or a coroutine.
Fast projectiles tunnel through thin colliders: at 30 m/s and 30 FPS a bullet moves a whole metre per frame and can skip a wall. Move it through the Rigidbody with MovePosition and set Collision Detection to Continuous, or keep the speed sane.