Skip to main content

🎯 Lesson 5.2: Traces & Spatial Queries

A trace fires an invisible ray (or shape) through the world and reports what it hits. It's how you do hitscan weapons, line-of-sight checks, "what am I looking at," ground detection, and interaction prompts. Master traces and a huge category of gameplay opens up.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Perform a line trace with LineTraceSingleByChannel
  • Read results from FHitResult (actor, location, normal, component)
  • Use FCollisionQueryParams to ignore the instigator
  • Distinguish trace by channel from by object type, and single vs multi
  • Perform a shape sweep and visualize traces with debug draws

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

What a Trace Is

A trace is a query: "starting here, going there, what do I hit?" The engine walks the ray against collision and returns hit information. Unlike overlaps (Lesson 5.1), a trace happens on demand — you call it exactly when you want an answer.

graph LR Start["Start point
(e.g. camera / muzzle)"] -->|"ray"| End["End point
(Start + Direction * Range)"] End --> Hit["FHitResult:
did we hit? what? where?"]
Figure 1: A line trace from Start to End. The engine reports the first blocking hit along the way in an FHitResult.

📖 Traces respect collision responses

A trace uses the same channel system as Lesson 5.1. Tracing "by the Visibility channel" hits things set to block Visibility. This is why collision setup and traces are one topic: a wall that blocks Visibility stops a line-of-sight trace; one that ignores it doesn't. Custom trace channels (Project Settings) let you make gameplay-specific queries like an "Interaction" trace.

The Line Trace

The workhorse is UWorld::LineTraceSingleByChannel. A "what am I looking at" trace from the camera:

bool AMyCharacter::TraceFromCamera(FHitResult& OutHit, float Range) const
{
    // Build the ray from the camera's viewpoint.
    const FVector Start = Camera->GetComponentLocation();
    const FVector End   = Start + Camera->GetForwardVector() * Range;

    FCollisionQueryParams Params;
    Params.AddIgnoredActor(this);   // don't hit ourselves (next section)

    // Trace against the Visibility channel; fills OutHit; returns true if blocked.
    const bool bHit = GetWorld()->LineTraceSingleByChannel(
        OutHit, Start, End, ECC_Visibility, Params);

    // Visualize it (Lesson 2.6): green line, red hit point.
    DrawDebugLine(GetWorld(), Start, End, bHit ? FColor::Red : FColor::Green, false, 1.0f);
    if (bHit)
    {
        DrawDebugPoint(GetWorld(), OutHit.ImpactPoint, 12.0f, FColor::Red, false, 1.0f);
    }

    return bHit;
}

✅ The signature pattern

Trace functions return a bool (did it hit a blocker?) and fill an out-parameter FHitResult& — the by-reference out-param idiom from Lesson 1.4. The debug draw is the payoff of Lesson 2.6: you can literally see the ray, which turns "why isn't this hitting?" into an obvious picture.

Reading FHitResult

FHitResult is a rich struct. The fields you'll use most:

FieldWhat it gives you
GetActor()The actor that was hit (may be null on no-hit)
GetComponent()The specific component hit
ImpactPointWorld location where the ray struck
ImpactNormalSurface normal at the hit (for decals, ricochets)
DistanceHow far along the ray the hit was
bBlockingHitWhether it was a blocking hit
FHitResult Hit;
if (TraceFromCamera(Hit, 5000.0f))
{
    AActor* HitActor = Hit.GetActor();
    if (IsValid(HitActor))
    {
        // Damage anything with a health component (composition again, Lesson 3.5):
        if (UHealthComponent* Health = HitActor->FindComponentByClass<UHealthComponent>())
        {
            Health->ApplyDamage(25.0f);
        }
        // Spawn an impact effect oriented to the surface:
        // ...use Hit.ImpactPoint and Hit.ImpactNormal...
    }
}

⚠️ Always check the actor is valid

Even when a trace hits, GetActor() can be null (you can hit world geometry that isn't an actor), and the actor could be mid-destruction. Guard with IsValid() before dereferencing — the same discipline from Module 2, now in a hot combat path where a crash is very visible.

Query Params & Ignoring Self

By default a trace from your own actor might hit your own collision first — you'd shoot yourself. FCollisionQueryParams tunes the query; the most important use is ignoring actors.

FCollisionQueryParams Params;
Params.AddIgnoredActor(this);              // ignore the shooter
Params.AddIgnoredActor(GetOwner());        // and its owner, e.g. for a weapon
Params.bTraceComplex = false;              // trace simple collision (faster)
Params.bReturnPhysicalMaterial = true;     // get surface type (footsteps, decals)

GetWorld()->LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, Params);

📖 Simple vs complex collision

bTraceComplex = false traces against the cheap simplified collision shape; true traces the actual triangle mesh — more accurate but slower. Use simple for gameplay traces you do often (like every shot), and complex only when you need per-triangle precision (like a sniper hitting a specific mesh detail).

Channel vs Object, Single vs Multi, Sweeps

Traces come in a matrix of variants. You'll reach for different ones by situation:

VariantUse when
...ByChannel"Hit whatever blocks this channel" (line of sight, shots)
...ByObjectType"Hit only these object types" (only pawns, only physics bodies)
...Single...You want the first blocking hit
...Multi...You want all hits along the ray (piercing shots)
Sweep...Trace a shape (sphere/box/capsule), not a thin line

A sphere sweep — useful for "forgiving" aim or a thick projectile — passes a shape:

FHitResult Hit;
const FVector Start = GetActorLocation();
const FVector End   = Start + GetActorForwardVector() * 1000.0f;

// A 30-unit radius sphere swept along the ray — easier to land than a thin line.
GetWorld()->SweepSingleByChannel(
    Hit, Start, End, FQuat::Identity, ECC_Visibility,
    FCollisionShape::MakeSphere(30.0f));

DrawDebugSphere(GetWorld(), End, 30.0f, 16, FColor::Cyan, false, 1.0f);

⚠️ Don't trace every frame carelessly

Traces are cheaper than iterating all actors, but a Multi complex trace every Tick on many actors still adds up. Trace on events (a shot, an interaction press) where you can; when you must trace continuously (an interaction prompt), keep it a single simple trace and consider doing it a few times per second rather than every frame.

Hands-on Exercise & Quiz

🏋️ Exercise: An interaction tracer

Objective: Build the "what am I looking at" query used for interaction prompts.

  1. Write AActor* GetLookedAtActor(float Range) const on your character.
  2. Line-trace from the camera forward by Range on ECC_Visibility, ignoring self.
  3. Return Hit.GetActor() if valid, else nullptr.
  4. Draw the debug line so you can see it. (In Lesson 5.5 you'll ask the returned actor "are you interactable?" via an interface.)
✅ Reference
AActor* AMyCharacter::GetLookedAtActor(float Range) const
{
    const FVector Start = Camera->GetComponentLocation();
    const FVector End   = Start + Camera->GetForwardVector() * Range;

    FCollisionQueryParams Params;
    Params.AddIgnoredActor(this);

    FHitResult Hit;
    const bool bHit = GetWorld()->LineTraceSingleByChannel(
        Hit, Start, End, ECC_Visibility, Params);
    DrawDebugLine(GetWorld(), Start, End, FColor::Yellow, false, 0.1f);

    return (bHit && IsValid(Hit.GetActor())) ? Hit.GetActor() : nullptr;
}

🎯 Quick Quiz

Question 1: What does a single line trace return and fill?

Question 2: How do you stop a trace from hitting the actor that fired it?

Question 3: You want a piercing shot that hits every target along the ray. Use a:

Summary

🎉 Key Takeaways

  • A trace is an on-demand query — "from here to there, what do I hit?" — respecting the same channels as collision.
  • LineTraceSingleByChannel returns a bool and fills an FHitResult&; read GetActor, ImpactPoint, ImpactNormal, Distance.
  • Use FCollisionQueryParams to AddIgnoredActor(this) and pick simple vs complex collision.
  • Variants: ByChannel vs ByObjectType, Single vs Multi, and Sweep for shapes.
  • Trace on events when possible; visualize with DrawDebug* to debug spatial logic.

📚 Additional Resources

🚀 What's Next?

Detection and queries are covered. Now the dimension of time: delays, cooldowns, repeating effects — the timer system, which lets you schedule work without polling every frame in Tick.

🎉 Lesson complete!

You can query space. Next, schedule across time.