For , our UMG-based HUD has to be able to point at off-screen elements to indicate the direction of incoming damage and also to show the location of offscreen goals and enemies.
Figuring out where to draw those offscreen indicators turned out to be a gnarlier problem than I anticipated. I started in Blueprint, but ended up with such a mess of nodes and wires that I moved to C++. I implemented the logic as a blueprint-callable function in a node library. The end-result of is a pretty nice little Blueprint node that wraps up all the yucky involved with calculating an offscreen or onscreen indicator for a World location.
Here’s the node as it appears in Blueprint:
Here’s a short video from my sample project:
[video]https://dl.dropboxusercontent.com/u/5075634/indicator2.mov[/video]
The placement of the red on-screen indicator, and the placement AND rotation of the black offscreen indicator are all handled by a single node. The node takes a world location vector and returns screen coordinates, a boolean indicating whether the location is on-screen or a pointer to off-screen, and a rotation angle for rotating your UMG element to point toward the offscreen item.
The logic used when the location being drawn is behind the player is a little bit of a hack - I drop the world location on the Z-axis and massage the projected screen coordinates a bit so that it gets drawn at the bottom of the screen. Despite that, it results in a fairly smooth transition as you rotate around. There’s almost definitely room for improvement there, but I’m pretty happy with the result even if the feels like a hack.
I place no restrictions on the use of this. You can use it, modify it, redistribute it, or ignore it with no obligations. I hope it’s helpful to somebody.
The example project, including the Node, is available on github. I will gladly accept pull requests for improvements or bug fixes.
If you just want to download it as a zip file, that’s right here.
Here is the Blueprint node’s .h file:
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "HUDBlueprintLibrary.generated.h"
/**
*
*/
UCLASS()
class OFFSCREENINDICATOR_API UHUDBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Converts a world location to screen position for HUD drawing. This differs from the results of FSceneView::WorldToScreen in that it returns a position along the edge of the screen for offscreen locations
*
* @param InLocation - The world space location to be converted to screen space
* @param EdgePercent - How close to the edge of the screen, 1.0 = at edge, 0.0 = at center of screen. .9 or .95 is usually desirable
* @outparam OutScreenPosition - the screen coordinates for HUD drawing
* @outparam OutRotationAngleDegrees - The angle to rotate a hud element if you want it pointing toward the offscreen indicator, 0° if onscreen
* @outparam bIsOnScreen - True if the specified location is in the camera view (may be obstructed)
*/
UFUNCTION(BlueprintPure, meta=(WorldContext="WorldContextObject", CallableWithoutWorldContext), Category="HUD|Util")
static void FindScreenEdgeLocationForWorldLocation(UObject* WorldContextObject, const FVector& InLocation, const float EdgePercent, FVector2D& OutScreenPosition, float& OutRotationAngleDegrees, bool &bIsOnScreen);
};
And here is the .cpp file:
// Fill out your copyright notice in the Description page of Project Settings.
#include "OffscreenIndicator.h"
#include "HUDBlueprintLibrary.h"
void UHUDBlueprintLibrary::FindScreenEdgeLocationForWorldLocation(UObject* WorldContextObject, const FVector& InLocation, const float EdgePercent, FVector2D& OutScreenPosition, float& OutRotationAngleDegrees, bool &bIsOnScreen)
{
bIsOnScreen = false;
OutRotationAngleDegrees = 0.f;
FVector2D *ScreenPosition = new FVector2D();
const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY());
const FVector2D ViewportCenter = FVector2D(ViewportSize.X/2, ViewportSize.Y/2);
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject);
if (!World) return;
APlayerController* PlayerController = (WorldContextObject ? UGameplayStatics::GetPlayerController(WorldContextObject, 0) : NULL);
ACharacter *PlayerCharacter = static_cast<ACharacter *> (PlayerController->GetPawn());
if (!PlayerCharacter) return;
FVector Forward = PlayerCharacter->GetActorForwardVector();
FVector Offset = (InLocation - PlayerCharacter->GetActorLocation()).SafeNormal();
float DotProduct = FVector::DotProduct(Forward, Offset);
bool bLocationIsBehindCamera = (DotProduct < 0);
if (bLocationIsBehindCamera)
{
// For behind the camera situation, we cheat a little to put the
// marker at the bottom of the screen so that it moves smoothly
// as you turn around. Could stand some refinement, but results
// are decent enough for most purposes.
FVector DiffVector = InLocation - PlayerCharacter->GetActorLocation();
FVector Inverted = DiffVector * -1.f;
FVector NewInLocation = PlayerCharacter->GetActorLocation() * Inverted;
NewInLocation.Z -= 5000;
PlayerController->ProjectWorldLocationToScreen(NewInLocation, *ScreenPosition);
ScreenPosition->Y = (EdgePercent * ViewportCenter.X) * 2.f;
ScreenPosition->X = -ViewportCenter.X - ScreenPosition->X;
}
PlayerController->ProjectWorldLocationToScreen(InLocation, *ScreenPosition);
// Check to see if it's on screen. If it is, ProjectWorldLocationToScreen is all we need, return it.
if (ScreenPosition->X >= 0.f && ScreenPosition->X <= ViewportSize.X
&& ScreenPosition->Y >= 0.f && ScreenPosition->Y <= ViewportSize.Y)
{
OutScreenPosition = *ScreenPosition;
bIsOnScreen = true;
return;
}
*ScreenPosition -= ViewportCenter;
float AngleRadians = FMath::Atan2(ScreenPosition->Y, ScreenPosition->X);
AngleRadians -= FMath::DegreesToRadians(90.f);
OutRotationAngleDegrees = FMath::RadiansToDegrees(AngleRadians) + 180.f;
float Cos = cosf(AngleRadians);
float Sin = -sinf(AngleRadians);
ScreenPosition = new FVector2D(ViewportCenter.X + (Sin * 150.f), ViewportCenter.Y + Cos * 150.f);
float m = Cos / Sin;
FVector2D ScreenBounds = ViewportCenter * EdgePercent;
if (Cos > 0)
{
ScreenPosition = new FVector2D(ScreenBounds.Y/m, ScreenBounds.Y);
}
else
{
ScreenPosition = new FVector2D(-ScreenBounds.Y/m, -ScreenBounds.Y);
}
if (ScreenPosition->X > ScreenBounds.X)
{
ScreenPosition = new FVector2D(ScreenBounds.X, ScreenBounds.X*m);
}
else if (ScreenPosition->X < -ScreenBounds.X)
{
ScreenPosition = new FVector2D(-ScreenBounds.X, -ScreenBounds.X*m);
}
*ScreenPosition += ViewportCenter;
OutScreenPosition = *ScreenPosition;
}