Easy Offscreen Indicator Blueprint Node

I realize this thread is ancient, but there is virtually nothing functional out there in the search to do this job, and I’m sure people new to programming probably appreciate that this is here. I made some changes for a 3D flight game.

This was a nice find, but didn’t work too well for a flight game as noted by the author and others in this thread. It doesn’t handle the situation where the enemy is over 90 degrees to the side (behind your camera), above or below, and worse, if your enemy is 180 degrees or so from you, it flags the enemy as being visible onscreen. For our HUD, I needed a compass style arrow and thus the ScreenAngle return was just what I needed if I could deal with those problems. I chose a hack to get a reasonable angle back for my “compass”. All I did was, if the enemy was over 90 degrees away from player forward vector (noted by the bLocationIsBehindCamera being true), I would project that location forward until it was on a parallel plane perpendicular to the camera’s forward vector. This allowed Kneebiters code to give me a good angle. The actual screen coordinates that result, still leave something to be desired, but they would look okay if they were interp’d. I’m not using them so I didn’t do that. I also added a “FacingBackward” boolean return, because then the caller can know if the enemy is actually behind them in that 180 range, and do anything special with the point they would want to do. I started with 's camera version because that’s more appropriate for a flight game since most are in 3rd person and usually have a roll-back to move a long ways back from the pawn. It will still completely fail if the enemy is exactly 180 behind you, but that’s momentary and all it will do is turn off the HUD for a frame (the way we are using it).

Note the angle is still not perfect, but it leads you nicely back to the enemy in any case. If you enable the debug spheres, it gives a feel for how close or far the angle is when projected. It was good enough for me. Also I renamed the return boolean in a non-standard way (ShouldntDisplay) to remind us that the HUD should not be displayed since the enemy is on screen if that is true.

Thanks to Kneebiter, and others who contributed this routine and changes, it simplified my task.


void MyClass::FindScreenEdgeLocationForWorldLocation(UObject* WorldContextObject, const FVector& InLocation, const float EdgePercent, FVector2D& OutScreenPosition, float& OutRotationAngleDegrees, bool &bIsOnScreen, bool &FacingBackward)
{
    bIsOnScreen = false;
    OutRotationAngleDegrees = 0.f;
    FacingBackward=false;
    FVector ContrivedParallelLocation=InLocation;
    if (!GEngine) return;
    const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY());
    const FVector2D  ViewportCenter = FVector2D(ViewportSize.X / 2, ViewportSize.Y / 2);
    UWorld* World = GEngine->GetWorldFromContextObjectChecked(WorldContextObject);
    if (!World) return;
    APlayerController* PlayerController = (WorldContextObject ? UGameplayStatics::GetPlayerController(WorldContextObject, 0) : NULL);
    if (!PlayerController) return;
    APlayerCameraManager* ViewPointActor = (WorldContextObject ? UGameplayStatics::GetPlayerCameraManager(WorldContextObject, 0) : NULL);
    if (!ViewPointActor) return;
    FVector Location = ViewPointActor->GetCameraLocation();
    FVector Forward = ViewPointActor->GetActorForwardVector();
    FVector Offset = (InLocation - Location).GetSafeNormal();
    float DotProduct = FVector::DotProduct(Forward, Offset);
    bool bLocationIsBehindCamera = (DotProduct < 0);
    if (bLocationIsBehindCamera)
    {
        // For behind the camera situation, I project the objects location to the plane perpendicular to the viewport of the camera
        // then put it a few centimeters forward of the camera so it doesn't appear on in the view but does return a reasonable angle
        // for a circular pointer (like a compass pointing toward the enemy.
        FacingBackward=true;
        float DistanceToCamera=(InLocation-Location).Size(); //Get distance to camera
        ContrivedParallelLocation=InLocation+(ViewPointActor->GetActorForwardVector() * (DistanceToCamera+10000.f)); //Project forward
//        DrawDebugSphere(
//                        GetWorld(),
//                        ContrivedParallelLocation,
//                        15000,
//                        64,
//                        FColor(255,255,255),false,2.f,0
//                        );
    }
    if(!PlayerController->ProjectWorldLocationToScreen(ContrivedParallelLocation, OutScreenPosition))  GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Projected FALSE"));
    // Check to see if it's on screen. If it is, ProjectWorldLocationToScreen is all we need, return it.
    if ((OutScreenPosition.X >= 0.f && OutScreenPosition.X <= ViewportSize.X
        && OutScreenPosition.Y >= 0.f && OutScreenPosition.Y <= ViewportSize.Y)&&(!FacingBackward))
    {
            bIsOnScreen = true;
            return;
    }
    OutScreenPosition -= ViewportCenter;
    float AngleRadians = FMath::Atan2(OutScreenPosition.Y, OutScreenPosition.X);
    AngleRadians -= FMath::DegreesToRadians(90.f);
    OutRotationAngleDegrees = FMath::RadiansToDegrees(AngleRadians) + 180.f;
    float Cos = cosf(AngleRadians);
    float Sin = -sinf(AngleRadians);
    OutScreenPosition = FVector2D(ViewportCenter.X + (Sin * 180.f), ViewportCenter.Y + Cos * 180.f);
    float m = Cos / Sin;
    FVector2D ScreenBounds = ViewportCenter * EdgePercent;
    if (Cos > 0)
    {
        OutScreenPosition = FVector2D(ScreenBounds.Y / m, ScreenBounds.Y);
    }
    else
    {
        OutScreenPosition = FVector2D(-ScreenBounds.Y / m, -ScreenBounds.Y);
    }
    if (OutScreenPosition.X > ScreenBounds.X)
    {
        OutScreenPosition = FVector2D(ScreenBounds.X, ScreenBounds.X*m);
    }
    else if (OutScreenPosition.X < -ScreenBounds.X)
    {
        OutScreenPosition = FVector2D(-ScreenBounds.X, -ScreenBounds.X*m);
    }
    OutScreenPosition += ViewportCenter;
}


1 Like