Skip to main content

๐ŸŽฏ 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.Raycast and read the RaycastHit
  • 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:

Raycast hitscan versus OverlapSphere melee A top-down diagram split into two halves. On the left, a player fires a single ray in a straight line; the ray passes an ignored wall behind and stops at the first enemy it hits, which is highlighted. On the right, the same player has a translucent circle drawn around it representing an OverlapSphere; every enemy inside the circle is highlighted, while an enemy outside the circle is not. Hitscan โ€” Physics.Raycast A line: the first thing it hits Player HIT shielded ray stops at first hit Wall on ignored layer โ†’ not hit Melee โ€” Physics.OverlapSphere A shape: everything inside it radius Player HIT HIT out of range
Figure 1: Hitscan (left) is a single ray that stops at the first collider it meets; melee (right) is a volume test that returns every collider inside the region at once.

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. A LayerMask is 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?

flowchart TD Q{"What kind of attack?"} --> A["Instant, pinpoint
(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.

  1. Put your enemies and barrels on an Enemy layer and any walls on an Environment layer.
  2. Add the HitscanWeapon script to your player/camera, assign the muzzle transform, and tick Enemy + Environment in the hittable mask.
  3. Call Fire() from an Input Action (Module 3) bound to left-click.
  4. Use Debug.DrawLine to 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.
  • RaycastHit hands you collider, point, normal, and distance from 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 instant Physics.OverlapSphere.
  • Projectiles are physical objects that resolve on OnCollisionEnter; set linearVelocity in 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.