๐ฏ Lesson 7.2: Hit Detection: Raycasts & Hitboxes
Last lesson you built the IDamageable contract and a reusable Health component โ but you dealt damage by clicking. Now we make attacks real. A gunshot is a raycast fired down the barrel; a sword swing is an overlap test around the blade; a fireball is a projectile that reports what it touches. Every one of them ends the same way: GetComponent<IDamageable>()?.TakeDamage().
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Fire an instant hitscan attack with
Physics.Raycastand read theRaycastHit - Use a LayerMask so a ray hits only the things it should
- Detect melee hits with a trigger collider or a one-shot
Physics.OverlapSphere - Understand how a projectile resolves its hit via
OnCollisionEnter - Route every method through the same
IDamageable.TakeDamage()call
Estimated Time: 45 minutes ยท Prerequisite: Lesson 7.1 (Health & the IDamageable interface); Fundamentals physics (Rigidbody, colliders)
In This Lesson
Two Styles of Hit Detection
Almost every attack in games is one of two shapes. A hitscan hit is a line: it travels instantly from a point in a direction and reports the first thing it strikes โ bullets, laser sights, "is the enemy in front of me?" A volume hit is a shape: it asks "what is inside this region right now?" โ a sword's arc, an explosion's radius, a shockwave.
Here is the difference at a glance, drawn as a top-down view of the same scene:
Different queries, same ending: once you have the collider you struck, you fetch its IDamageable and call TakeDamage. The interface from Lesson 7.1 is what makes all three attack styles converge on one line.
Hitscan with Physics.Raycast
Physics.Raycast shoots an invisible line from an origin along a direction and tells you what it hit through an out RaycastHit. For a gun, the origin is the muzzle (or camera) and the direction is where it points.
using UnityEngine;
public class HitscanWeapon : MonoBehaviour
{
[SerializeField] Transform muzzle; // where the shot originates
[SerializeField] float range = 100f;
[SerializeField] float damage = 25f;
[SerializeField] LayerMask hittable; // set in Inspector (see next section)
public void Fire()
{
// out var hit captures what we struck
if (Physics.Raycast(muzzle.position, muzzle.forward, out RaycastHit hit, range, hittable))
{
Debug.DrawLine(muzzle.position, hit.point, Color.red, 0.5f); // visualise in Scene view
// The whole point of Lesson 7.1: we don't care WHAT we hit
hit.collider.GetComponent<IDamageable>()?.TakeDamage(damage);
}
}
}
Read the arguments left to right: from muzzle.position, along muzzle.forward, put the result in hit, only within range, and only against the hittable layers. The ?. means if the thing we hit is a wall (no IDamageable), nothing happens โ no crash.
๐ Definition
RaycastHit: a struct Unity fills in describing the hit โ hit.collider (what was struck), hit.point (the world position of contact, great for spawning impact VFX), hit.normal (the surface direction, for aligning decals), and hit.distance. You get all of it from one raycast.
โ ๏ธ Don't raycast from Update every frame for a semi-auto weapon
Fire the ray in response to an input action (the new Input System from Module 3), not blindly in Update. A single trigger pull should cast once. For automatic fire, gate it behind a fire-rate timer so you cast at the weapon's cadence, not the frame rate.
Layer Masks: Hitting Only What You Mean
Without a filter, a ray hits everything with a collider โ including the player's own body, trigger volumes, and invisible geometry. A LayerMask tells the physics query which layers to consider and which to ignore.
First, put objects on layers (top-right Layer dropdown in the Inspector โ add custom layers via Layer โธ Add Layerโฆ). Typical setup: an Enemy layer, a Player layer, an Environment layer. Then expose a LayerMask field and tick the layers you want in the Inspector:
[SerializeField] LayerMask hittable; // tick "Enemy" and "Environment"
Passing that mask as the last argument of Physics.Raycast makes the ray sail straight through anything on an unticked layer. This is how a player's own collider never blocks their shot, and how you make a sniper round pass through a chain-link fence but stop on a wall.
๐ก Building a mask in code. ALayerMaskis really a bitmask. To build one from a name:int mask = LayerMask.GetMask("Enemy", "Environment");. To ignore a layer instead, invert it:~LayerMask.GetMask("Player")means "everything except Player." The Inspector field is just a friendlier way to author the same bits.
โ Pro Tip
There is also a project-wide safety net: Edit โธ Project Settings โธ Physics โธ Layer Collision Matrix. Unticking a pair there stops those layers colliding at all. Use the matrix for broad rules (players never collide with other players' hurtboxes) and per-query LayerMasks for weapon-specific filtering.
Melee: Triggers & OverlapSphere
A sword doesn't fire a line โ it sweeps through space and should hit everyone in its arc. There are two common approaches.
Approach A โ a trigger collider hitbox
Give the weapon (or a child "blade" object) a collider with Is Trigger ticked, enabled only during the swing's contact frames (an Animation Event from Module 1 is perfect for turning it on and off). Anything that enters reports itself via OnTriggerEnter:
using System.Collections.Generic;
using UnityEngine;
public class MeleeHitbox : MonoBehaviour
{
[SerializeField] float damage = 40f;
// track who we've already hit this swing so one slash = one hit each
readonly HashSet<IDamageable> hitThisSwing = new();
void OnTriggerEnter(Collider other)
{
var target = other.GetComponent<IDamageable>();
if (target == null || hitThisSwing.Contains(target)) return;
hitThisSwing.Add(target);
target.TakeDamage(damage);
}
// call this from an Animation Event at the start of each swing
public void BeginSwing() => hitThisSwing.Clear();
}
The HashSet stops the classic bug where a slow swing registers the same enemy on several physics frames and shreds them instantly. Clear it at the start of every swing so each new attack can hit again.
Approach B โ an instant OverlapSphere
For an explosion, a ground-pound, or a simple "hit everything in front of me" swing, you don't need a persistent collider at all. Physics.OverlapSphere returns every collider inside a sphere right now:
public void GroundPound()
{
Collider[] hits = Physics.OverlapSphere(transform.position, 3f, hittable);
foreach (Collider c in hits)
c.GetComponent<IDamageable>()?.TakeDamage(damage);
}
One call, all targets in radius, then loop and damage each through the same interface. There is also Physics.OverlapBox for rectangular areas and OverlapCapsule for capsule shapes โ pick the shape that matches your attack.
โ ๏ธ OverlapSphere allocates an array every call
Calling it once per swing is fine. If you ever need it every frame, use the non-allocating Physics.OverlapSphereNonAlloc(center, radius, results, mask) with a pre-sized Collider[] buffer, so you don't create garbage for the collector to clean up. We cover exactly this kind of optimization in Module 12.
Projectiles in Brief
A projectile โ a fireball, an arrow, a grenade โ is a real GameObject with a Rigidbody that travels through the world and resolves its hit on contact. Because it is physical, you use collision callbacks rather than a query. Give it velocity when you spawn it, and let physics carry it:
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class Projectile : MonoBehaviour
{
[SerializeField] float damage = 30f;
[SerializeField] float speed = 20f;
[SerializeField] float lifetime = 5f;
void Start()
{
GetComponent<Rigidbody>().linearVelocity = transform.forward * speed;
Destroy(gameObject, lifetime); // clean up if it never hits anything
}
void OnCollisionEnter(Collision collision)
{
collision.collider.GetComponent<IDamageable>()?.TakeDamage(damage);
Destroy(gameObject); // consumed on impact
}
}
Note linearVelocity โ in Unity 6 the old Rigidbody.velocity was renamed to linearVelocity. Same idea: set the projectile's speed once and let the physics engine move it. When it touches anything, it deals damage through โ you guessed it โ the same IDamageable call.
Which style should you reach for?
(gun, laser)"] Q --> B["Area / arc, right now
(sword, explosion)"] Q --> C["Travels, can be dodged
(arrow, fireball)"] A --> A1["Physics.Raycast
+ LayerMask"] B --> B1["Trigger hitbox
or OverlapSphere"] C --> C1["Rigidbody projectile
+ OnCollisionEnter"] A1 --> END["GetComponent<IDamageable>()
?.TakeDamage(amount)"] B1 --> END C1 --> END
Figure 2: Three detection methods for three attack shapes โ all funnelling into the single interface call from Lesson 7.1.
Hands-on Challenge
๐๏ธ Exercise 1: A working hitscan blaster
Objective: Shoot the barrels and enemies from Lesson 7.1 for real.
- Put your enemies and barrels on an
Enemylayer and any walls on anEnvironmentlayer. - Add the
HitscanWeaponscript to your player/camera, assign themuzzletransform, and tickEnemy+Environmentin thehittablemask. - Call
Fire()from an Input Action (Module 3) bound to left-click. - Use
Debug.DrawLineto see the ray in the Scene view. Confirm shots pass through the player's own body (unticked layer) but stop on the first enemy.
๐ก Hint: my shots hit nothing / hit myself
If nothing is hit, your hittable mask is probably empty โ tick the layers in the Inspector. If you hit yourself, put the player on its own layer and leave that layer out of the mask.
โ Success check
A click drains the health bar of exactly the first enemy in the crosshair. Walls block the ray; the player's own collider is ignored; barrels behind an enemy stay untouched.
๐๏ธ Exercise 2: A ground-pound that hits a crowd
Add a GroundPound() method using Physics.OverlapSphere with a 3-unit radius and your hittable mask. Surround the player with several enemies and barrels, trigger the pound, and confirm every target inside the radius takes damage in a single call while the one just outside survives. Bonus: draw the radius with Gizmos.DrawWireSphere in OnDrawGizmosSelected so you can see it in the Scene view.
๐ฏ Quick Quiz
Question 1: A shotgun blast should hit only enemies and walls, never the player firing it. What controls that?
Question 2: Which query returns every collider inside a region at once?
Question 3: Why does the MeleeHitbox track hits in a HashSet per swing?
Summary
๐ Key Takeaways
- Hitscan is a line:
Physics.Raycast(origin, direction, out hit, range, mask)returns the first collider struck. RaycastHithands youcollider,point,normal, anddistancefrom a single cast.- A LayerMask filters which layers a query considers โ the clean way to stop shots hitting the shooter.
- Melee is a volume: a trigger-collider hitbox (with a per-swing
HashSet) or an instantPhysics.OverlapSphere. - Projectiles are physical objects that resolve on
OnCollisionEnter; setlinearVelocityin Unity 6. - Every method converges on
GetComponent<IDamageable>()?.TakeDamage()โ the interface makes the ending identical.
๐ What's Next?
Combat now works: you can hurt anything, any way. But an action game needs stuff โ potions to heal, ammo to reload, weapons to swap. In Lesson 7.3: An Inventory System we return to Module 2's ScriptableObjects to model items as data assets, then build an Inventory that holds stackable slots and raises an event to refresh the UI.
๐ฏ One interface, three weapons
Gun, sword, fireball โ different queries, one destination. Because Lesson 7.1 gave you a shared contract, every attack you'll ever add already knows how to deal damage.