Zoiks! Look at that. My many years of Obj-C took their toll (no stack objects on Obj-C). I’ll fix this when I get a chance. Thanks for the heads up.
May I also ask why you multiply the inverted diff? Seems like you wanted an Add there, basically if it’s not on screen… put it on screen then flip results.
Hi there,
Sry for my English i am from Germany.
I found a Way to make the Arrow works in all directions 360° and shows always in the right direction. No 0.0 Coordinates. (Sure u need this for a branch to say, front or back)
Use a Vector Lerp (World coordinates from Objekt and Player) with Alpha 10. This will Mirror the World Coordinates from the Mark Object.
It will calculate a Straight line from the Mark Object to the player and behind him.
That way, u got alway Mirror Coordinates.
After this u have to invert some stuff und leave the arows on the screen ends.
Works Perfect. I hope this will help a lot of people who got Problems with that. Take me the hole Weekend to check this out
Greeting from Germany
Any chance you could share your example with us please? Having some trouble with this as I am in a 6-DOF environment and mirroring the InLocation seems a logical way of approaching it, but I cannot seem to get decent results.
Any suggestions on how to make this usable for local two player split screen?
Using this for a school project and it’s great! Just need to figure out how to have each one limited to their given screen space.
Thanks!
Guys, you are evil You made me break my “don’t write any C++ code yourself” rule!
While the code is neat, it only works under very specific conditions.
- The viewpoint is only *named * camera in the comments, but it’s not the camera. It’s the controlled pawn.
- Therefore it does not work with 3rd person or Top Down cameras, where the camera’s location is different from the APawn.
- With non-character pawns (e.g. WheeledVehicle), it breaks down completely (because it fails to cast to ACharacter).
TL/DR, here is a version which uses the CameraManager and does not need the pawn at all.
I’m actually bad at C++, so please cross-check before copying
// HUDBlueprintLibrary.cpp
#include "YOUR_MODULE_NAME.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;
if (!GEngine) return;
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);
if (!PlayerController) return;
APlayerCameraManager* ViewPointActor = (WorldContextObject ? UGameplayStatics::GetPlayerCameraManager(WorldContextObject, 0) : NULL);
// APawn* ViewPointActor = PlayerController->GetPawn();
if (!ViewPointActor) return;
// FVector Location = ViewPointActor->GetActorLocation();
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, 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 - Location;
FVector Inverted = DiffVector * -1.f;
FVector NewInLocation = Location * Inverted;
NewInLocation.Z -= 5000;
PlayerController->ProjectWorldLocationToScreen(NewInLocation, OutScreenPosition);
OutScreenPosition.Y = (EdgePercent * ViewportCenter.X) * 2.f;
OutScreenPosition.X = -ViewportCenter.X - OutScreenPosition.X;
}
PlayerController->ProjectWorldLocationToScreen(InLocation, OutScreenPosition);//*ScreenPosition);
// 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)
{
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;
}
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;
}
None of these versions worked with UMG widgets. So I had to write one myself, based on the last published on by
#include "WidgetLayoutLibrary.h"
#include "SlateBlueprintLibrary.h"
void USLR_BlueprintLibrary::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;
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
}
UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition(
PlayerController, ContrivedParallelLocation, OutScreenPosition);
FVector2D ViewportSize = UWidgetLayoutLibrary::GetViewportSize(WorldContextObject);
USlateBlueprintLibrary::ScreenToViewport(PlayerController, ViewportSize, ViewportSize);
const FVector2D ViewportCenter = FVector2D(ViewportSize.X / 2, ViewportSize.Y / 2);
// 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;
}
To use it you have to add “UMG” here:
PublicDependencyModuleNames.AddRange(new string] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" });
Thanks for that, I spaced off the fact that this is posted in the blueprint section and needs to be accessible directly from UMG blueprint. We have C++ at the top of every class so our UMG stuff doesn’t directly call this. Your approach is more direct.
can anyone shed some light on how to use this in VR? I think ProjectWorldLocationToWidgetPosition doesn’t work.
Does anyone have an updated version of this? It doesn’t seem to work for actors behind the camera if they are much higher than the camera.
Fixed memory leaks and the issue of it not working for locations behind the camera
.h
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "HUDBlueprintLibrary.generated.h"
UCLASS()
class YOURPROJECTNAME_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
* @param ViewportCenterLoc - for offsetting center of the screen, leave at (0.5, 0.5) for no offset
* @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, const FVector2D ViewportCenterLoc, FVector2D& OutScreenPosition, float& OutRotationAngleDegrees, bool &bIsOnScreen );
};
.cpp
#include "HUDBlueprintLibrary.h"
#include <Kismet/GameplayStatics.h>
#include <GameFramework/Character.h>
#include <Engine/Engine.h>
void UHUDBlueprintLibrary::FindScreenEdgeLocationForWorldLocation( UObject* WorldContextObject, const FVector& InLocation, const float EdgePercent, const FVector2D ViewportCenterLoc, FVector2D& OutScreenPosition, float& OutRotationAngleDegrees, bool& bIsOnScreen )
{
bIsOnScreen = false;
OutRotationAngleDegrees = 0.f;
FVector2D ScreenPosition = FVector2D();
if( !GEngine )
{
return;
}
const FVector2D ViewportSize = FVector2D( GEngine->GameViewport->Viewport->GetSizeXY() );
const FVector2D ViewportCenter = FVector2D( ViewportSize.X * ViewportCenterLoc.X, ViewportSize.Y * ViewportCenterLoc.Y );
UWorld* World = GEngine->GetWorldFromContextObject( WorldContextObject );
if( !World )
{
return;
}
APlayerController* PlayerController = ( WorldContextObject ? UGameplayStatics::GetPlayerController( WorldContextObject, 0 ) : NULL );
if( !PlayerController )
{
return;
}
FVector CameraLoc;
FRotator CameraRot;
PlayerController->GetPlayerViewPoint( CameraLoc, CameraRot );
const FVector CameraToLoc = InLocation - CameraLoc;
FVector Forward = CameraRot.Vector();
FVector Offset = CameraToLoc.GetSafeNormal();
float DotProduct = FVector::DotProduct( Forward, Offset );
bool bLocationIsBehindCamera = ( DotProduct < 0 );
if( bLocationIsBehindCamera )
{
FVector Inverted = CameraToLoc * -1.f;
FVector NewInLocation = CameraLoc + Inverted;
PlayerController->ProjectWorldLocationToScreen( NewInLocation, ScreenPosition );
ScreenPosition.X = ViewportSize.X - ScreenPosition.X;
ScreenPosition.Y = ViewportSize.Y - ScreenPosition.Y;
}
else
{
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 && !bLocationIsBehindCamera )
{
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 = FVector2D( ViewportCenter.X + ( Sin * 150.f ), ViewportCenter.Y + Cos * 150.f );
float m = Cos / Sin;
FVector2D ScreenBounds = ViewportCenter * EdgePercent;
if( Cos > 0 )
{
ScreenPosition = FVector2D( ScreenBounds.Y / m, ScreenBounds.Y );
}
else
{
ScreenPosition = FVector2D( -ScreenBounds.Y / m, -ScreenBounds.Y );
}
if( ScreenPosition.X > ScreenBounds.X )
{
ScreenPosition = FVector2D( ScreenBounds.X, ScreenBounds.X * m );
}
else if( ScreenPosition.X < -ScreenBounds.X )
{
ScreenPosition = FVector2D( -ScreenBounds.X, -ScreenBounds.X * m );
}
ScreenPosition += ViewportCenter;
OutScreenPosition = ScreenPosition;
}
HI all,
First, thank you very much guys for this amazingly great stuffs and resources.
I tried to use the last code made by @ ksztanan but I only have two little problems (I have a TPS Camera) :
-
When I look upward, my widget goes on the top of screen despite the enemy is behind me.
-
I set “Edge Percent” at “0.9” and “Viewport Center Loc” at “0.5 ; 0.5” but on the top and left zones of my Viewport (my screen) the widget seems to be a little less on the edges while on bottom and right zones of my screen it perfectly sticks with the viewport edges.
Can someone help me to know where should I look for to resolve those two problems please ?
Thanks a lot!
@ZioYuri78 @ @anonymous_user_fb692cdc Seems to work only for 1080p resolution, do you know why that’s the case?
Sorry, I don’t have anything else to test with just now.
Hi !
First, i’m very bad at geometry and trigonometry, Atan/Cos/Sin, i dont understand any of these, so, this post and this code is sooooooo helpfull, THANKS guys !
After few hours to search why it’s only working for 1080p, i found out !
You need to divide the “Out Screen Position” by the “Viewport Scale” like on the screen :
For those of you who want a very simple blueprint only solution for top-down games. I made my own minimalistic system. This ONLY works in top-down games though. But if you are making a top-down game, this is (I think) a much simpler and cheaper solution than making complex calculations and having to require c++ code.
Just make this into a global function. And call it in the widget that you want to always be visible on the edge of your screen. I haven’t added the none edge appearance yet though, but that should be extremely easy to add if you need that.
Your code works like a charm, many thanks to you
The reason is that, the widget that you are moving on the screen has a size. You need to decrement the half size of it, to make it centered while moving. I move the widget in a canvas panel slot. You can I see that I get the size of the widget and divide by 2 to get the half size and then decrement the half size from the calculated position
// Find the edge location in terms of screen space
FVector2D OutScreenPosition;
float OutRotationAngleDegrees;
FindScreenEdgeLocationForWorldLocation(TargetLocation, 0.98f, FVector2D(0.5f, 0.5f), OutScreenPosition, OutRotationAngleDegrees);
// Convert to local space of the Canvas Panel
FVector2D LocalCoordinate;
USlateBlueprintLibrary::ScreenToWidgetLocal(this, MainCanvasPanel->GetCachedGeometry(), OutScreenPosition, LocalCoordinate);
// Update position in Canvas panel with the size offset
UCanvasPanelSlot* WidgetAsSlot = Cast<UCanvasPanelSlot>(PlayerNumIndicatorMap[PlayerNum]->Slot);
const float SizeOffsetX = WidgetAsSlot->GetSize().X / 2.f;
const float SizeOffsetY = WidgetAsSlot->GetSize().Y / 2.f;
WidgetAsSlot->SetPosition(LocalCoordinate + FVector2D(-SizeOffsetX, -SizeOffsetY));
// Update the rotation
PlayerNumIndicatorMap[PlayerNum]->SetRenderTransformAngle(OutRotationAngleDegrees);
i know this thread is ancient but here’s my take on making offscreen indicators using only blueprint nodes: https://www.youtube.com/watch?v=2Xz92_lPYQQ