🎯 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
FCollisionQueryParamsto 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.
(e.g. camera / muzzle)"] -->|"ray"| End["End point
(Start + Direction * Range)"] End --> Hit["FHitResult:
did we hit? what? where?"]
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:
| Field | What it gives you |
|---|---|
GetActor() | The actor that was hit (may be null on no-hit) |
GetComponent() | The specific component hit |
ImpactPoint | World location where the ray struck |
ImpactNormal | Surface normal at the hit (for decals, ricochets) |
Distance | How far along the ray the hit was |
bBlockingHit | Whether 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:
| Variant | Use 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.
- Write
AActor* GetLookedAtActor(float Range) conston your character. - Line-trace from the camera forward by
RangeonECC_Visibility, ignoring self. - Return
Hit.GetActor()if valid, elsenullptr. - 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.
LineTraceSingleByChannelreturns abooland fills anFHitResult&; readGetActor,ImpactPoint,ImpactNormal,Distance.- Use
FCollisionQueryParamstoAddIgnoredActor(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.