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

Game Development Math for Unity3D June 2018 45 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.

  • 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

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
  • hit.rigidbody the Rigidbody of the hit object (null if no Rigidbody)

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;
            Debug.Log("Hit: " + hit.collider.name + " at distance " + hit.distance);
        }
        else
        {
            endPoint = startPoint + transform.forward * maxRange;
        }

        laser.SetPosition(0, startPoint);
        laser.SetPosition(1, endPoint);
    }
}

2D Raycasting

// 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);
}

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. 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;
}

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.

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)
{
    RaycastHit hit;
    if (Physics.Linecast(enemy.position, player.position, out hit))
    {
        return hit.transform == player;
    }
    return false;
}

Shape Casting: SphereCast, BoxCast, CapsuleCast

A thin mathematical ray sometimes misses objects it should logically hit. Shape casting solves this by sweeping a 3D shape (sphere, box, or capsule) along the ray path instead of a single point.

Physics.SphereCast()

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

Physics.BoxCast()

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
);

Physics.CapsuleCast()

bool hit = Physics.CapsuleCast(
    point1,    // top sphere center
    point2,    // bottom sphere center
    radius,    // capsule radius
    direction,
    out RaycastHit hitInfo,
    maxDistance
);

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.

// 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;

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

LayerMask as a Serialized Field

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

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

Advanced 2D Ground Check

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;

    [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()
    {
        isGrounded = CheckGrounded();

        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()
    {
        RaycastHit2D hit = Physics2D.CircleCast(
            feetTransform.position,
            groundCheckRadius,
            Vector2.down,
            groundCheckDistance,
            groundLayer
        );

        return hit.collider != null;
    }

    private void OnDrawGizmos()
    {
        if (feetTransform == null) return;
        Gizmos.color = isGrounded ? Color.green : Color.red;
        Gizmos.DrawWireSphere(
            feetTransform.position + Vector3.down * groundCheckDistance,
            groundCheckRadius
        );
    }
}

Exercise: Duck Shooter 3D

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;
    [SerializeField] private GameObject hitEffectPrefab;

    private int score = 0;

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Shoot();
        }
    }

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

        if (Physics.Raycast(ray, out hit, maxShootRange))
        {
            if (hit.collider.CompareTag("Duck"))
            {
                if (hitEffectPrefab != null)
                {
                    Quaternion effectRotation = Quaternion.LookRotation(hit.normal);
                    Instantiate(hitEffectPrefab, hit.point, effectRotation);
                }

                Destroy(hit.collider.gameObject);

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

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.

Overlap Functions

// 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);

// 2D variants
Collider2D[] colliders2D = Physics2D.OverlapCircleAll(center, radius, layerMask);
Collider2D[] colliders2D = Physics2D.OverlapBoxAll(center, size, angle, layerMask);
// 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)
        {
            float distance   = Vector3.Distance(epicenter, col.transform.position);
            float falloff    = 1f - Mathf.Clamp01(distance / radius);
            float finalDamage = damage * falloff;
            health.TakeDamage(finalDamage);
        }
    }
}

AddExplosionForce()

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
);

Creating a Complete Bomb System

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;

        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
                );
            }

            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);
            }
        }

        if (explosionEffectPrefab != null)
        {
            Instantiate(explosionEffectPrefab, epicenter, Quaternion.identity);
        }

        Destroy(gameObject);
    }

    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);
    }
}

Summary

  • 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.
Unity3D Raycasting Physics Linecast LayerMask C# 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