Skip to content

Instantly share code, notes, and snippets.

@windopper
Created October 25, 2025 05:06
Show Gist options
  • Select an option

  • Save windopper/063de331667cae326587a5909aff9bdd to your computer and use it in GitHub Desktop.

Select an option

Save windopper/063de331667cae326587a5909aff9bdd to your computer and use it in GitHub Desktop.

Revisions

  1. windopper created this gist Oct 25, 2025.
    107 changes: 107 additions & 0 deletions CollisionLibrary.cpp
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,107 @@
    // reference https://dev.epicgames.com/community/learning/tutorials/pBv0/how-to-perform-cone-shaped-traces-in-unreal-engine

    // CollisionLibrary.h
    public:
    UFUNCTION(BlueprintCallable, Category = "Collision", Meta = (bIgnoreSelf = "true", WorldContext = "WorldContextObject", AutoCreateRefTerm = "ActorsToIgnore", DisplayName = "Multi Cone Trace By Channel", AdvancedDisplay = "TraceColor, TraceHitColor, DrawTime", Keywords = "sweep"))
    static bool ConeTraceMulti(const UObject* WorldContextObject, const FVector Start, const FRotator Direction, float ConeHeight, float ConeHalfAngle, ETraceTypeQuery TraceChannel, bool bTraceComplex, const TArray<AActor*>& ActorsToIgnore, EDrawDebugTrace::Type DrawDebugType, TArray<FHitResult>& OutHits, bool bIgnoreSelf, FLinearColor TraceColor = FLinearColor::Red, FLinearColor TraceHitColor = FLinearColor::Green, float DrawTime = 5.0f);

    // CollisionLibrary.cpp
    bool UCollisionLibrary::ConeTraceMulti(
    const UObject* WorldContextObject,
    const FVector Start,
    const FRotator Direction,
    float ConeHeight,
    float ConeHalfAngle,
    ETraceTypeQuery TraceChannel,
    bool bTraceComplex,
    const TArray<AActor*>& ActorsToIgnore,
    EDrawDebugTrace::Type DrawDebugType,
    TArray<FHitResult>& OutHits,
    FLinearColor TraceColor,
    FLinearColor TraceHitColor,
    float DrawTime)
    {
    OutHits.Reset();

    ECollisionChannel CollisionChannel = UEngineTypes::ConvertToCollisionChannel(TraceChannel);
    FCollisionQueryParams Params(SCENE_QUERY_STAT(ConeTraceMulti), bTraceComplex);
    Params.bReturnPhysicalMaterial = true;
    Params.AddIgnoredActors(ActorsToIgnore);

    UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
    if (!World)
    {
    return false;
    }

    TArray<FHitResult> TempHitResults;
    const FVector End = Start + (Direction.Vector() * ConeHeight);
    const double ConeHalfAngleRad = FMath::DegreesToRadians(ConeHalfAngle);
    // r = h * tan(theta / 2)
    const double ConeBaseRadius = ConeHeight * tan(ConeHalfAngleRad);
    const FCollisionShape SphereSweep = FCollisionShape::MakeSphere(ConeBaseRadius);

    // Perform a sweep encompassing an imaginary cone.
    World->SweepMultiByChannel(TempHitResults, Start, End, Direction.Quaternion(), CollisionChannel, SphereSweep, Params);

    // Filter for hits that would be inside the cone.
    for (FHitResult& HitResult : TempHitResults)
    {
    const FVector HitDirection = (HitResult.ImpactPoint - Start).GetSafeNormal();
    const double Dot = FVector::DotProduct(Direction.Vector(), HitDirection);
    // theta = arccos((A • B) / (|A|*|B|)). |A|*|B| = 1 because A and B are unit vectors.
    const double DeltaAngle = FMath::Acos(Dot);

    // Hit is outside the angle of the cone.
    if (DeltaAngle > ConeHalfAngleRad)
    {
    continue;
    }

    const double Distance = (HitResult.ImpactPoint - Start).Length();
    // Hypotenuse = adjacent / cos(theta)
    const double LengthAtAngle = ConeHeight / cos(DeltaAngle);

    // Hit is beyond the cone. This can happen because we sweep with spheres, which results in a cap at the end of the sweep.
    if (Distance > LengthAtAngle)
    {
    continue;
    }

    OutHits.Add(HitResult);
    }

    #if ENABLE_DRAW_DEBUG
    if (DrawDebugType != EDrawDebugTrace::None)
    {
    // Cone trace.
    const double ConeSlantHeight = FMath::Sqrt((ConeBaseRadius * ConeBaseRadius) + (ConeHeight * ConeHeight)); // s = sqrt(r^2 + h^2)
    DrawDebugCone(World, Start, Direction.Vector(), ConeSlantHeight, ConeHalfAngleRad, ConeHalfAngleRad, 32, TraceColor.ToFColor(true), (DrawDebugType == EDrawDebugTrace::Persistent), DrawTime);

    // Uncomment to see the trace we're actually performing.
    // DrawDebugSweptSphere(World, Start, End, ConeBaseRadius, TraceColor.ToFColor(true), (DrawDebugType == EDrawDebugTrace::Persistent), DrawTime);

    // Successful hits.
    for (const FHitResult& Hit : OutHits)
    {
    DrawDebugLineTraceSingle(World, Hit.TraceStart, Hit.ImpactPoint, DrawDebugType, true, Hit, TraceHitColor, TraceHitColor, DrawTime);
    }

    // Uncomment to see hits from the sphere sweep that were filtered out.
    // for (const FHitResult& Hit : TempHitResults)
    // {
    // if (!OutHits.ContainsByPredicate([Hit](const FHitResult& Other)
    // {
    // return (Hit.GetActor() == Other.GetActor()) &&
    // (Hit.ImpactPoint == Other.ImpactPoint) &&
    // (Hit.ImpactNormal == Other.ImpactNormal);
    // }))
    // {
    // DrawDebugLineTraceSingle(World, Hit.TraceStart, Hit.ImpactPoint, DrawDebugType, false, Hit, FColor::Red, FColor::Red, DrawTime);
    // }
    // }
    }
    #endif // ENABLE_DRAW_DEBUG

    return (OutHits.Num() > 0);
    }