Skip to main content

💥 Lesson 5.1: Collision, Overlaps & Hit Events

Interaction starts with collision: a pickup detecting the player, a bullet striking a wall, a trigger firing when someone enters. Unreal's collision system is powerful but full of settings that must line up — get them right and events flow; get them wrong and nothing happens, silently.

🎯 Learning Objectives

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

  • Distinguish overlap from blocking hit and know which fires which event
  • Explain collision channels, object types, and responses
  • Configure a component's collision in C++
  • Bind OnComponentBeginOverlap, OnComponentEndOverlap, and OnComponentHit
  • Write a working pickup that grants on overlap and destroys itself

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

Overlap vs Blocking Hit

Two fundamentally different interactions, two different event families:

OverlapBlocking Hit
Physical effectObjects pass through each otherObjects stop each other
FiresOnComponentBeginOverlap / EndOverlapOnComponentHit
Typical usePickups, triggers, detection zonesWalls, projectiles that impact, physics
graph TD A["Two components meet"] --> Q{"Response between them?"} Q -->|"Both set to Overlap"| O["Overlap events fire
(pass through)"] Q -->|"Either set to Block"| B["Blocking hit — they stop
Hit event fires"] Q -->|"Ignore"| N["Nothing — no events"]
Figure 1: What happens when two components meet depends on their collision responses to each other. Overlap needs BOTH to overlap; a single Block makes it a blocking hit.

📖 The rule that trips everyone up

For an overlap event to fire, both components must be set to overlap each other, AND the moving one must have Generate Overlap Events enabled. Miss any of those and you get silence — no error, just nothing. Half of all "my collision isn't working" issues are a missing overlap-generation flag.

Channels & Responses

Collision is organized by channels. Every collidable component has an object type (which channel it is) and a set of responses (how it reacts to each channel: Ignore, Overlap, or Block).

graph LR Comp["A component"] --> OT["Object Type
(e.g. Pawn, WorldStatic)"] Comp --> R["Responses to each channel:
Ignore / Overlap / Block"]
Figure 2: A component both is a channel (its object type) and responds to every channel. Two components interacting consult each other's responses.
  • Object type — the channel this component belongs to: Pawn, WorldStatic, WorldDynamic, PhysicsBody, or a custom channel you define in Project Settings.
  • Response — for each channel, one of ECR_Ignore, ECR_Overlap, ECR_Block.
  • Collision presets — named bundles of these settings ("Pawn", "OverlapAllDynamic", "BlockAll", "Trigger") so you rarely set every response by hand.

✅ Presets first, custom later

Start with a preset that matches your intent — "OverlapAllDynamic" for a pickup, "BlockAll" for a wall. Only reach for per-channel responses when a preset doesn't fit. Custom channels (Project Settings → Collision) are for gameplay-specific categories like an "Interaction" trace channel — we use one in Lesson 5.2.

Configuring Collision in C++

You set collision on the component, usually in the constructor. A trigger sphere for a pickup wants to overlap pawns and block nothing:

#include "Components/SphereComponent.h"

APickup::APickup()
{
    Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
    RootComponent = Collision;
    Collision->SetSphereRadius(80.0f);

    // Use the trigger-style preset: query-only, overlaps dynamic things.
    Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));

    // Or configure explicitly:
    Collision->SetCollisionEnabled(ECollisionEnabled::QueryOnly); // no physics, just events
    Collision->SetCollisionObjectType(ECC_WorldDynamic);
    Collision->SetCollisionResponseToAllChannels(ECR_Overlap);
    Collision->SetGenerateOverlapEvents(true);   // ← the flag people forget
}
ECollisionEnabledMeaning
NoCollisionOff entirely
QueryOnlyOverlaps & traces, but no physics (triggers, pickups)
PhysicsOnlyPhysics simulation, no queries
QueryAndPhysicsBoth (solid physical objects)

⚠️ QueryOnly for triggers

A pickup or trigger volume should be QueryOnly — it detects overlaps but doesn't shove the player around with physics. Using QueryAndPhysics on a trigger can knock actors off course. Match the collision-enabled mode to whether the thing is physical or just a sensor.

Binding Collision Events

Collision events are dynamic multicast delegates (the full mechanism is Lesson 5.4). You subscribe your function with AddDynamic, and your function's signature must match the delegate exactly. Bind in BeginPlay (the component exists and the world is live).

// Header — the handler signatures are fixed by the delegate types:
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
    bool bFromSweep, const FHitResult& SweepResult);

UFUNCTION()
void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor,
    UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
void APickup::BeginPlay()
{
    Super::BeginPlay();

    // Subscribe our UFUNCTIONs to the component's delegates.
    Collision->OnComponentBeginOverlap.AddDynamic(this, &APickup::OnBeginOverlap);
}

void APickup::OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
    bool bFromSweep, const FHitResult& SweepResult)
{
    // OtherActor is who entered our sphere.
    if (IsValid(OtherActor) && OtherActor != this)
    {
        UE_LOG(LogTemp, Log, TEXT("%s entered pickup"), *OtherActor->GetName());
    }
}

⚠️ Handlers bound with AddDynamic MUST be UFUNCTION()

AddDynamic binds by name through the reflection system, so the target function must be marked UFUNCTION() and its signature must match the delegate's parameters exactly. A missing UFUNCTION() or a mismatched signature fails to bind (often with a cryptic error). Copy the signature from the engine's delegate declaration to be safe.

A Complete Pickup

Combining collision with the UHealthComponent pattern from Lesson 3.3 and Destroy() from 3.4 — a health pickup that heals whoever has a health component and removes itself:

void AHealthPickup::OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
    bool bFromSweep, const FHitResult& SweepResult)
{
    if (!IsValid(OtherActor) || OtherActor == this) { return; }

    // Composition in action (Lesson 3.5): does the overlapper have health?
    if (UHealthComponent* Health = OtherActor->FindComponentByClass<UHealthComponent>())
    {
        Health->Heal(HealAmount);          // grant the effect
        UE_LOG(LogTemp, Log, TEXT("Healed %s by %.0f"), *OtherActor->GetName(), HealAmount);
        Destroy();                          // consume the pickup (deferred GC — Lesson 2.5)
    }
    // No health component? It's not a valid target; leave the pickup for someone who is.
}

✅ Look how the modules stack

This one handler uses collision (5.1), FindComponentByClass capability-checking (3.5), the health component (3.3), IsValid guarding (2.5), and Destroy (3.4). That layering is the point of the course — you're now composing patterns, not learning isolated tricks. This exact pickup returns, generalized, in the Module 6 hybrid pattern and the Module 12 capstone.

Hands-on Exercise & Quiz

🏋️ Exercise: A damage volume

Objective: Build a lava/hazard trigger.

  1. Create ADamageVolume : public AActor with a UBoxComponent root, QueryOnly, overlapping dynamic objects, overlap events on.
  2. Bind both OnComponentBeginOverlap and OnComponentEndOverlap.
  3. On begin overlap, if the actor has a UHealthComponent, start harming it; on end overlap, stop. (You'll wire the repeating tick with a timer in Lesson 5.3 — for now just log begin/end.)
💡 Hint — end overlap signature
UFUNCTION()
void OnEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
// Bind: Box->OnComponentEndOverlap.AddDynamic(this, &ADamageVolume::OnEndOverlap);

🎯 Quick Quiz

Question 1: Which event fires when two components pass through each other?

Question 2: Your overlap event never fires. Most likely missing flag?

Question 3: A function bound via AddDynamic must be:

Summary

🎉 Key Takeaways

  • Overlap (pass-through) fires OnComponentBeginOverlap/EndOverlap; a blocking hit fires OnComponentHit.
  • Collision = an object type (which channel you are) + responses (Ignore/Overlap/Block per channel); use presets first.
  • Configure with SetCollisionProfileName or explicit SetCollisionEnabled/ObjectType/Response; triggers are QueryOnly; don't forget SetGenerateOverlapEvents(true).
  • Bind events in BeginPlay with AddDynamic; handlers must be UFUNCTION() with exact signatures.
  • A real pickup layers collision + FindComponentByClass + a component effect + Destroy() — patterns composing.

📚 Additional Resources

🚀 What's Next?

Overlaps detect things that touch. But often you need to ask "what's over there?" without waiting for contact — shooting a ray, checking line of sight, finding what's under the cursor. That's traces, next.

🎉 Lesson complete!

The world can feel contact now. Next, let it sense at a distance.