Download

Easy Offscreen Indicator Blueprint Node

For Republic Sniper, 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 math involved with calculating an offscreen or onscreen indicator for a World location.

Here’s the node as it appears in Blueprint:

Screen Shot 2015-02-18 at 11.27.10 PM.png

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 math 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;
	
}


2 Likes

Cool! Thanks for sharing this!

Thank BOTH of you for sharing this. @ZeroFiftyOne just answered my question on the hub: How to BP on-screen indication of off-screen possible collisions? - UE4 AnswerHub

Doesn’t look like it will be too hard to modify for my purposes, I may have to submit a pull to disable the black object indicator but need to study it tonight.

hugs

It’s intended to be fairly generic and flexible, but if you have problems getting it to do what you need, just ping me.

Nice … I got to the stage with the Blueprints … and it was a mess. 8-{

This is definitely going to be helpful … thanks. tips hat 8-}

Heh, yeah. Blueprint is awesome, but it’s not good when you have lots of math. I got lost in my own node tree, hence the C++. :slight_smile:

Thank you for your submission. I’m sure this will prove useful!

Awesome! We ended up building similar for indicators, only clamped to an ellipse so that we stay within screen safe & indicators can avoid fighting other HUD elements.

oh wow this is awesome, thanks for posting it up for everyone :slight_smile:

Appreciate it, homie!

Is this for 4.6 only?

The node itself should compile on any version of UE4, I think - but I admit that I haven’t tried on versions other than on 4.6 and 4.7.

Oh, perhaps I see the issue. So I’m a total noob fumbling my way through UE4 the last few months. I’ve only been using Blueprints and I do NOT have Visual Studio installed. I imagine I need to install it and it will compile the first time I try to open the project in 4.7?

Is there any way to repurpose this node so that it outputs XYZ coordinates? I’d like to be able to position meshes or particles at screen edges.

Any help appreciated!

I’m not quite sure I understand what you’re wanting. You want the nearest onscreen world location to an offscreen world location? I’m not sure what the “algorithm” for that would be. You could take the 2D screen coordinates and do a ray cast until you hit something. You could draw a line from the offscreen actor to the center of the screen and find the first onscreen location.

I guess I’m not fully visualizing what you want to do, so I’m not sure if this code can do it or if it can be easily modified to do it.

Jeff - thank you for this example node! Your code has been a useful reference in developing my own implementation.

However, it seems that the “behind-the-player” logic only works for certain relative locations. In your example project, try moving the TaggableBox to X: -1250, Y: -950, and Z: 195. Then, turn so that the box is behind your pawn. The smooth interpolation along the bottom of the screen breaks entirely. Instead, the indicator jumps from the left side of the screen to the right and back several times.

Does anybody know how to generalize the logic to work for all locations?

I’ve seen this. It mostly works in our game, but there are edge cases where it’s an issue.

I’ve got another approach I want to try, but haven’t had time, as i’ve been working on another project the last couple weeks. Playing Deus Ex Human Revolution and watching the way they display on-screen goal markers, I think they essentially calculate the relative location from a top down view, and then basically rotate that 90° on the X axis, so things behind you are the bottom fo the screen, things in front of you are at the top.

Probably simpler math to do it that way, too but, like I said, I haven’t had time ti experiment with it, and what we have is about 99% for our situation, so it’s not a high priority right now.

Thank you for the insightful speculation on Deus Ex’s system! I’ll definitely be looking into that top-down approach.

EDIT: Sorry to be a bother. Would you mind briefly expanding on the top-down approach you’ve been thinking about? I’m having a hard time visualizing how that would work.

Fantastic work! Was thinking about a system like this myself, but you have saved me the headache. Very nice implementation!

Awesome!

Today i started try make something similar in blueprints, i have achivied my purpose but work only with one object, deepening in your project have helped me a lot understand how to handle all my stuff!

EDIT:

Solved.