Chapter 7: Mastering Raycasting — Detection, Shooting, and Ground Checks

Game DevelopmentMath for Unity3DJune 201845 min read

Introduction to Raycasting

Raycasting is one of the most powerful and versatile tools in a Unity developer's toolkit. The concept is simple: you cast an invisible ray from a point in a direction, and Unity reports everything that ray touches in the physics world. Despite its simplicity, raycasting is the foundation of an enormous variety of gameplay systems: shooting mechanics, line-of-sight AI, ground detection for platformers, mouse picking, terrain-following cameras, proximity sensors, and much more.

One critical thing to understand before diving in: raycasts only interact with Colliders. An object without a Collider component is completely invisible to a raycast. The Renderer, Rigidbody, and every other component are irrelevant — only the Collider matters. This means you can have invisible trigger volumes that block raycasts, and visible meshes that are transparent to them, simply by controlling which objects have Colliders.

Common uses of raycasting in games:

  • Shooting: cast a ray from the gun muzzle (or screen center for FPS) and detect what it hits
  • Ground detection: cast a ray downward from character feet to check if they are grounded
  • Line of sight: cast a ray from an enemy toward the player — if it hits something else first, the player is behind cover
  • Mouse picking: cast a ray from the camera through the mouse cursor to select 3D objects
  • Terrain scanning: cast rays downward to snap objects to terrain surface
  • Proximity sensors: cast rays in multiple directions to detect nearby walls or obstacles

Physics.Raycast()

The fundamental raycasting method is Physics.Raycast(). It returns true if the ray hits anything, or false if it hits nothing within the specified distance.

// Simple form — returns true/false only
bool hit = Physics.Raycast(origin, direction, maxDistance);

// Full form — also returns information about what was hit
RaycastHit hitInfo;
bool hit = Physics.Raycast(origin, direction, out hitInfo, maxDistance);

The RaycastHit struct contains detailed information about the collision point:

  • hit.collider — the Collider that was hit
  • hit.point — the world-space position where the ray struck the surface
  • hit.normal — the surface normal at the hit point (useful for bullet decals, ricochet)
  • hit.distance — how far along the ray the hit occurred
  • hit.transform — the Transform of the hit object (shortcut for hit.collider.transform)
  • hit.rigidbody — the Rigidbody of the hit object (null if no Rigidbody)
  • hit.textureCoord — UV coordinates at the hit point (requires MeshCollider with convex=false)

Practical Example: Simple Laser Pointer

using UnityEngine;

public class LaserPointer : MonoBehaviour
{
    [SerializeField] private float maxRange = 50f;
    [SerializeField] private LineRenderer laser;

    private void Update()
    {
        RaycastHit hit;
        Vector3 startPoint = transform.position;
        Vector3 endPoint;

        if (Physics.Raycast(startPoint, transform.forward, out hit, maxRange))
        {
            endPoint = hit.point;

            // Instantiate a spark effect at the hit point
            Debug.Log("Hit: " + hit.collider.name + " at distance " + hit.distance);

            // Align a decal with the surface normal
            // decal.position = hit.point;
            // decal.rotation = Quaternion.LookRotation(hit.normal);
        }
        else
        {
            // Ray didn't hit anything — extend to max range
            endPoint = startPoint + transform.forward * maxRange;
        }

        // Update LineRenderer to visualize the laser
        laser.SetPosition(0, startPoint);
        laser.SetPosition(1, endPoint);
    }
}

2D Raycasting

The 2D equivalent uses Physics2D.Raycast() which returns a RaycastHit2D struct directly (not a bool). Check if the hit is valid by testing hit.collider != null:

// 2D Raycast
RaycastHit2D hit = Physics2D.Raycast(origin, direction, maxDistance);

if (hit.collider != null)
{
    Debug.Log("2D hit: " + hit.collider.name);
    Debug.Log("Hit point: " + hit.point);
    Debug.Log("Hit normal: " + hit.normal);
}

Note that in 2D, origin and direction are Vector2 values, and the ray travels in the X-Y plane.

Physics.RaycastAll()

The standard Physics.Raycast() stops at the first Collider it hits. Sometimes you need to know about all colliders the ray passes through — for example, a bullet that penetrates multiple enemies, or a vision cone that needs to collect all visible objects. Physics.RaycastAll() returns an array of all hits along the ray.

RaycastHit[] hits = Physics.RaycastAll(origin, direction, maxDistance);

// Sort hits by distance so we process them front to back
System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));

foreach (RaycastHit hit in hits)
{
    Debug.Log("Hit " + hit.collider.name + " at " + hit.distance + " meters");

    // For a penetrating bullet, reduce damage with each object pierced
    ApplyDamage(hit, baseDamage);
    baseDamage *= 0.7f; // 30% damage reduction per object
}

There is also a non-allocating version for performance-critical code: Physics.RaycastNonAlloc(), which writes results into a pre-allocated array you provide rather than creating a new one each call. This is important in hot code paths to avoid garbage collection spikes.

// Pre-allocate once (e.g., in Start or as a field)
private RaycastHit[] hitBuffer = new RaycastHit[10];

private void Update()
{
    int hitCount = Physics.RaycastNonAlloc(origin, direction, hitBuffer, maxDistance);
    for (int i = 0; i < hitCount; i++)
    {
        Debug.Log(hitBuffer[i].collider.name);
    }
}

Physics.Linecast()

Physics.Linecast() is a variant that casts between two explicit points in world space, rather than from a point in a direction with a max distance. It is conceptually simpler when you already know both endpoints.

// Cast between two known points
bool blocked = Physics.Linecast(startPoint, endPoint, out RaycastHit hit);

// Classic use: line-of-sight check between enemy and player
bool HasLineOfSight(Transform enemy, Transform player)
{
    Vector3 directionToPlayer = player.position - enemy.position;
    float   distanceToPlayer  = directionToPlayer.magnitude;

    RaycastHit hit;
    if (Physics.Linecast(enemy.position, player.position, out hit))
    {
        // If the first thing hit is the player, we have line of sight
        return hit.transform == player;
    }
    return false;
}

Linecast is ideal for line-of-sight because it reads like exactly what you are doing conceptually: "is there anything blocking the line between A and B?" The ray-based equivalent requires calculating direction and distance separately, which is more verbose.

Shape Casting: SphereCast, BoxCast, CapsuleCast

A thin mathematical ray sometimes misses objects it should logically hit — for example, a narrow ray cast downward for ground detection might slip through a gap in procedurally generated terrain, or a melee attack ray might miss a wide enemy by passing just beside them. Shape casting solves this by sweeping a 3D shape (sphere, box, or capsule) along the ray path instead of a single point.

Physics.SphereCast()

// Sweep a sphere of given radius along the ray
bool hit = Physics.SphereCast(
    origin,        // start of sweep
    radius,        // sphere radius
    direction,     // direction of sweep
    out RaycastHit hitInfo,
    maxDistance    // maximum sweep distance
);

SphereCast is the most commonly used shape cast. Typical uses:

  • Ground detection: a sphere of radius matching the character controller's foot provides more reliable results than a thin ray, especially on uneven terrain and ramps.
  • Melee hitboxes: sweep a sphere forward from the weapon to detect hits within a volume.
  • Wider vision rays: use a fat sphere for "peripheral vision" AI detection.

Physics.BoxCast()

// Sweep a box along the ray
bool hit = Physics.BoxCast(
    center,        // center of box at start
    halfExtents,   // half-extents of the box (Vector3)
    direction,
    out RaycastHit hitInfo,
    orientation,   // Quaternion for box rotation
    maxDistance
);

BoxCast is excellent for platform games where you need to sweep the character's bounding box to check for obstacles or ledges, and for anything that requires rectangular detection (doorways, corridors).

Physics.CapsuleCast()

// Sweep a capsule — defined by two sphere centers and a radius
bool hit = Physics.CapsuleCast(
    point1,    // top sphere center
    point2,    // bottom sphere center
    radius,    // capsule radius
    direction,
    out RaycastHit hitInfo,
    maxDistance
);

CapsuleCast matches the shape of Unity's CharacterController component exactly, making it ideal for checking character movement paths and detecting obstacles the character would collide with.

Filtering with LayerMask

By default, a raycast hits objects on all layers. In most games this is not what you want — you generally want to check specific subsets: only the ground, only enemies, everything except the player, and so on. LayerMask provides this filtering.

Setting Up Layers

Layers are configured in Edit > Project Settings > Tags and Layers. You can define up to 32 layers (layers 0–7 are reserved for Unity). Common patterns: one layer for "Ground", one for "Enemies", one for "Player", one for "Interactable".

Using LayerMask in Code

// Create a mask that includes only the "Ground" and "Terrain" layers
int groundMask = LayerMask.GetMask("Ground", "Terrain");

// Use the mask in a raycast — only hits Ground and Terrain objects
RaycastHit hit;
if (Physics.Raycast(origin, direction, out hit, maxDistance, groundMask))
{
    Debug.Log("Hit ground at " + hit.point);
}

// Invert the mask with ~ — hits EVERYTHING EXCEPT Ground and Terrain
int everythingExceptGround = ~groundMask;
if (Physics.Raycast(origin, direction, out hit, maxDistance, everythingExceptGround))
{
    Debug.Log("Hit non-ground object: " + hit.collider.name);
}

// You can also combine layer masks with bitwise OR
int combined = LayerMask.GetMask("Enemies") | LayerMask.GetMask("Props");

LayerMask as a Serialized Field

You can expose a LayerMask field in the Inspector, which gives you a convenient dropdown to select layers without writing any mask-building code:

[SerializeField] private LayerMask groundLayer;
[SerializeField] private LayerMask enemyLayer;

// Use directly in Physics functions — no GetMask() needed
Physics.Raycast(origin, direction, out hit, maxDistance, groundLayer);

This is the recommended approach for production code: designers can adjust which layers the raycast targets directly in the Inspector without touching code.

Advanced 2D Ground Check

The most robust way to detect whether a 2D character is grounded is a downward SphereCast (or CircleCast in 2D) from the character's feet. Using OnCollisionStay2D for this purpose is unreliable — it can miss frames and produce incorrect grounded states on slopes and edges.

using UnityEngine;

public class GroundedCharacter2D : MonoBehaviour
{
    [Header("Ground Check")]
    [SerializeField] private LayerMask groundLayer;
    [SerializeField] private float groundCheckRadius = 0.2f;
    [SerializeField] private float groundCheckDistance = 0.05f;
    [SerializeField] private Transform feetTransform; // Empty GameObject at character feet

    [Header("Movement")]
    [SerializeField] private float moveForce = 8f;
    [SerializeField] private float jumpForce = 12f;

    private Rigidbody2D rb;
    private bool isGrounded;

    private void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
        // Update ground state every frame
        isGrounded = CheckGrounded();

        // Jump input — read in Update to catch button press
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
        }
    }

    private void FixedUpdate()
    {
        float horizontal = Input.GetAxis("Horizontal");
        rb.AddForce(new Vector2(horizontal * moveForce, 0f), ForceMode2D.Force);
    }

    private bool CheckGrounded()
    {
        // Cast a circle downward from the feet position
        RaycastHit2D hit = Physics2D.CircleCast(
            feetTransform.position,    // origin
            groundCheckRadius,         // circle radius
            Vector2.down,              // direction
            groundCheckDistance,       // max distance
            groundLayer                // only check ground layer
        );

        return hit.collider != null;
    }

    // Visualize the ground check in the Scene view for debugging
    private void OnDrawGizmos()
    {
        if (feetTransform == null) return;
        Gizmos.color = isGrounded ? Color.green : Color.red;
        Gizmos.DrawWireSphere(
            feetTransform.position + Vector3.down * groundCheckDistance,
            groundCheckRadius
        );
    }
}

The OnDrawGizmos() method visualizes the ground check sphere in the Scene view — green when grounded, red when airborne. Always add gizmo visualization to physics check code during development; it saves enormous debugging time.

Exercise: 2D Ground Check Platformer

Using the script above, build a complete 2D platformer character:

  • Create a 2D scene with a main platform (Box Collider 2D), some floating platforms, and some slopes.
  • Add a Capsule sprite as the player with a Rigidbody2D and Capsule Collider 2D.
  • Create an empty child GameObject at the bottom of the capsule — name it "Feet".
  • Assign it to feetTransform in the Inspector.
  • Set the platform objects to a "Ground" layer and assign that layer in groundLayer.
  • Test that: the character can walk left and right; they can jump only when on the ground; they cannot double-jump.

Experiment with the groundCheckRadius (0.1–0.3 is typical) and groundCheckDistance (0.02–0.1). Too small and you may not detect ground on uneven surfaces; too large and you may detect ground while airborne.

Exercise: Duck Shooter 3D

Build a 3D shooting gallery: rubber ducks appear on the scene, and the player clicks to shoot them with a raycast from the camera.

Setup

  • Create a 3D scene with a ground plane and a few platforms at varying heights.
  • Create a Duck prefab: a capsule or duck model with a Collider, tag it "Duck".
  • Add a Canvas with a Text element for the score.
  • Add a Camera GameObject (or use Main Camera) and attach the shooter script.

Duck Spawner

using UnityEngine;

public class DuckSpawner : MonoBehaviour
{
    [SerializeField] private GameObject duckPrefab;
    [SerializeField] private Transform[] spawnPoints;
    [SerializeField] private float spawnInterval = 2f;
    [SerializeField] private int   maxDucks      = 8;

    private float timer;

    private void Update()
    {
        timer += Time.deltaTime;
        if (timer >= spawnInterval && CountActiveDucks() < maxDucks)
        {
            SpawnDuck();
            timer = 0f;
        }
    }

    private void SpawnDuck()
    {
        Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
        Instantiate(duckPrefab, spawnPoint.position, spawnPoint.rotation);
    }

    private int CountActiveDucks()
    {
        return GameObject.FindGameObjectsWithTag("Duck").Length;
    }
}

Shooting Controller

using UnityEngine;
using UnityEngine.UI;

public class DuckShooter : MonoBehaviour
{
    [SerializeField] private Camera playerCamera;
    [SerializeField] private Text   scoreText;
    [SerializeField] private LayerMask duckLayer;
    [SerializeField] private float  maxShootRange = 100f;

    [Header("Feedback")]
    [SerializeField] private GameObject hitEffectPrefab;

    private int score = 0;

    private void Update()
    {
        // Show crosshair highlight when hovering over a duck
        HighlightDuckUnderCursor();

        if (Input.GetMouseButtonDown(0))
        {
            Shoot();
        }
    }

    private void Shoot()
    {
        // Build a ray from the camera through the mouse cursor
        Ray ray = playerCamera.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, maxShootRange))
        {
            if (hit.collider.CompareTag("Duck"))
            {
                // Spawn hit effect at impact point, aligned to surface normal
                if (hitEffectPrefab != null)
                {
                    Quaternion effectRotation = Quaternion.LookRotation(hit.normal);
                    Instantiate(hitEffectPrefab, hit.point, effectRotation);
                }

                // Destroy the duck
                Destroy(hit.collider.gameObject);

                // Update score
                score++;
                scoreText.text = "Score: " + score;
            }
        }
    }

    private void HighlightDuckUnderCursor()
    {
        Ray ray = playerCamera.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, maxShootRange, duckLayer))
        {
            // Change cursor or material to indicate hoverable duck
            // Cursor.SetCursor(aimCursor, Vector2.zero, CursorMode.Auto);
        }
    }
}

The key line is playerCamera.ScreenPointToRay(Input.mousePosition). This converts the 2D mouse cursor position into a 3D ray that originates at the camera's near clip plane and travels through that screen position into the world. This is the standard technique for all mouse-based 3D interaction in Unity.

Continuous Collision Detection

A thin raycast has a related problem on Rigidbodies: very fast-moving physics objects (bullets, fast projectiles) can "tunnel" through thin colliders in a single physics step because the physics engine only checks for overlaps at discrete timesteps. At 0.02s per step, an object moving at 100 m/s travels 2 meters per step — easily passing through a 0.1m wall without registering a collision.

The solution is Continuous Collision Detection (CCD), which calculates the sweep path between frames and checks for collisions along it:

// In Start() on the fast-moving Rigidbody:
rb.collisionDetectionMode = CollisionDetectionMode.Continuous;

// Or even more thorough (but more expensive):
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;

CCD modes have a performance cost. Use Continuous on fast-moving objects that hit static colliders; use ContinuousDynamic when two fast-moving objects need to collide with each other. Leave everything else on Discrete (the default). Never turn on CCD for every object in your scene — apply it surgically only where tunneling actually occurs.

Overlap Functions

Sometimes you don't want to sweep a ray at all — you want to know which colliders currently exist inside a region. Unity's Overlap functions answer exactly this question.

// Return all colliders inside a sphere
Collider[] colliders = Physics.OverlapSphere(center, radius, layerMask);

// Return all colliders inside a box
Collider[] colliders = Physics.OverlapBox(center, halfExtents, rotation, layerMask);

// Return all colliders inside a capsule
Collider[] colliders = Physics.OverlapCapsule(point1, point2, radius, layerMask);

// 2D variants
Collider2D[] colliders2D = Physics2D.OverlapCircleAll(center, radius, layerMask);
Collider2D[] colliders2D = Physics2D.OverlapBoxAll(center, size, angle, layerMask);

Practical uses for Overlap functions:

  • Proximity alerts: check if any enemy is within a given radius of the player
  • Area-of-effect abilities: collect all enemies in an AoE radius and apply damage/effects
  • Pickup range detection: find all interactable items within reach
  • Spawn safety checks: verify a spawn point is clear before placing an object there
// Find all enemies within 5 meters of an AoE ability
void ApplyAreaDamage(Vector3 epicenter, float radius, float damage)
{
    int enemyLayer = LayerMask.GetMask("Enemies");
    Collider[] affected = Physics.OverlapSphere(epicenter, radius, enemyLayer);

    foreach (Collider col in affected)
    {
        HealthComponent health = col.GetComponent<HealthComponent>();
        if (health != null)
        {
            // Damage falls off with distance from epicenter
            float distance   = Vector3.Distance(epicenter, col.transform.position);
            float falloff    = 1f - Mathf.Clamp01(distance / radius);
            float finalDamage = damage * falloff;
            health.TakeDamage(finalDamage);
        }
    }
}

AddExplosionForce()

Unity provides a dedicated method for applying radial explosion physics: Rigidbody.AddExplosionForce(). Rather than requiring you to calculate force vectors for every affected Rigidbody manually, this method handles the spatial math: objects closer to the explosion receive more force, and objects farther away receive less.

rb.AddExplosionForce(
    explosionForce,       // total force at the epicenter
    explosionPosition,    // world position of the explosion
    explosionRadius,      // maximum radius of effect
    upwardsModifier,      // extra upward bias (0 = no bias, 1 = realistic upward scatter)
    ForceMode.Impulse     // typically Impulse for instant effect
);

The upwardsModifier shifts the explosion's effective origin upward, causing objects to be launched upward rather than purely outward. This looks more dramatic and realistic for game explosions. A value of 1–3 is typical.

Creating a Complete Bomb System

Let's build a full explosion system using OverlapSphere and AddExplosionForce:

using UnityEngine;

public class Bomb : MonoBehaviour
{
    [Header("Explosion Settings")]
    [SerializeField] private float explosionForce   = 800f;
    [SerializeField] private float explosionRadius  = 8f;
    [SerializeField] private float upwardsModifier  = 2f;
    [SerializeField] private float fuseTime         = 3f;

    [Header("Visual Effects")]
    [SerializeField] private GameObject explosionEffectPrefab;
    [SerializeField] private LayerMask  affectedLayers;

    private float timer;
    private bool  hasExploded;

    private void Update()
    {
        if (hasExploded) return;

        timer += Time.deltaTime;
        if (timer >= fuseTime)
        {
            Explode();
        }
    }

    private void Explode()
    {
        if (hasExploded) return;
        hasExploded = true;

        Vector3 epicenter = transform.position;

        // Find all colliders in the explosion radius
        Collider[] colliders = Physics.OverlapSphere(epicenter, explosionRadius, affectedLayers);

        foreach (Collider col in colliders)
        {
            Rigidbody affectedRb = col.attachedRigidbody;
            if (affectedRb != null && !affectedRb.isKinematic)
            {
                affectedRb.AddExplosionForce(
                    explosionForce,
                    epicenter,
                    explosionRadius,
                    upwardsModifier,
                    ForceMode.Impulse
                );
            }

            // Deal damage if the object has a health component
            HealthComponent health = col.GetComponent<HealthComponent>();
            if (health != null)
            {
                float distance    = Vector3.Distance(epicenter, col.transform.position);
                float damageFalloff = 1f - Mathf.Clamp01(distance / explosionRadius);
                health.TakeDamage(100f * damageFalloff);
            }
        }

        // Spawn explosion visual effect
        if (explosionEffectPrefab != null)
        {
            Instantiate(explosionEffectPrefab, epicenter, Quaternion.identity);
        }

        // Destroy the bomb object
        Destroy(gameObject);
    }

    // Visualize explosion radius in Scene view
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = new Color(1f, 0.3f, 0f, 0.3f);
        Gizmos.DrawSphere(transform.position, explosionRadius);
        Gizmos.color = new Color(1f, 0.3f, 0f, 1f);
        Gizmos.DrawWireSphere(transform.position, explosionRadius);
    }
}

This script is fully self-contained: place it on any GameObject (a sphere, a mesh, an invisible trigger) and it will explode after the fuse time, launching all physics objects in range and dealing damage to anything with a HealthComponent. The gizmo visualization makes it easy to tune the explosion radius visually in the Scene view.

Debugging Raycasts Visually

A common frustration with raycasting is that rays are invisible at runtime. Unity provides two tools for visualizing them during development:

// Draw a ray in the Scene view for one frame (only visible in Editor)
Debug.DrawRay(origin, direction * maxDistance, Color.red);

// Draw a line between two points in the Scene view
Debug.DrawLine(startPoint, endPoint, Color.green);

// Persist for multiple frames
Debug.DrawRay(origin, direction * maxDistance, Color.yellow, duration: 2f);

Make sure the Gizmos button is enabled in the Scene view toolbar to see these. A common workflow is to add Debug.DrawRay() calls next to every Physics.Raycast() during development, then remove or comment them out in the final build.

Summary

Raycasting is one of the most frequently used physics tools in real Unity projects. To recap the key points from this chapter:

  • Raycasts only hit Colliders — no Collider means invisible to raycasts.
  • Physics.Raycast() for single hits; Physics.RaycastAll() for all hits along a ray.
  • Physics.Linecast() when you know both endpoints — great for line-of-sight checks.
  • Shape casting (SphereCast, BoxCast, CapsuleCast) prevents thin-ray misses and gives you volumetric detection.
  • LayerMask filters raycasts to specific layers — essential for performance and correctness.
  • Camera.ScreenPointToRay() converts mouse position to a 3D ray — the foundation of mouse picking.
  • Physics.OverlapSphere() and friends find all colliders in a region without any sweeping.
  • AddExplosionForce() handles radial physics forces with distance falloff built in.
  • Debug.DrawRay() makes invisible rays visible during development.

In the next chapter we move on to trigonometry — sine, cosine, and Atan2 — and build a complete turret aiming system that demonstrates how trig functions power orientation, circular movement, and angle calculation in Unity.

Unity3DRaycastingPhysicsLinecastLayerMaskC#Detection

Need help with Unity3D development?

I'm a senior developer with 16+ years experience, including AAA projects at Ubisoft. Let's discuss how I can help with your game or interactive project.

Start a Conversation