Announcement

Collapse
No announcement yet.

Easy Offscreen Indicator Blueprint Node

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    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:

    Click image for larger version

Name:	Screen Shot 2015-02-18 at 11.27.10 PM.png
Views:	1
Size:	50.5 KB
ID:	1144438

    Here's a short video from my sample project:

    https://dl.dropboxusercontent.com/u/...indicator2.mov

    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:

    Code:
    #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:

    Code:
    // 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
    Cool! Thanks for sharing this!
    Twitter: @zerofiftyone_
    Website: zerofiftyone.net - My game development blog
    Button Frenzy store page: http://store.steampowered.com/app/454630

    Comment


      #3
      Thank BOTH of you for sharing this. @ZeroFiftyOne just answered my question on the hub: https://answers.unrealengine.com/que...een-possi.html

      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*

      Comment


        #4
        Originally posted by Sanborn View Post
        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.
        It's intended to be fairly generic and flexible, but if you have problems getting it to do what you need, just ping me.

        Comment


          #5
          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-}
          Quinton Delpeche
          Founder - Gobbo Games | Designer - The Colony 2174 (Board Game) | Developer - Riders of Asgard

          Comment


            #6
            Originally posted by qdelpeche View Post
            Nice ... I got to the stage with the Blueprints ... and it was a mess. 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++.

            Comment


              #7
              Thank you for your submission. I'm sure this will prove useful!

              Unpopular Minion on the Play Store (Released: January 31, 2016)

              Comment


                #8
                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.
                The Flame in the Flood - travel the river, evade predators, fight the elements.

                Comment


                  #9
                  oh wow this is awesome, thanks for posting it up for everyone

                  Comment


                    #10
                    Appreciate it, homie!

                    Comment


                      #11
                      Is this for 4.6 only?

                      Comment


                        #12
                        Originally posted by Sanborn View Post
                        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.

                        Comment


                          #13
                          Originally posted by jeff_lamarche View Post
                          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?

                          Comment


                            #14
                            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!
                            Twitter: @zerofiftyone_
                            Website: zerofiftyone.net - My game development blog
                            Button Frenzy store page: http://store.steampowered.com/app/454630

                            Comment


                              #15
                              Originally posted by zerofiftyone View Post
                              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.

                              Comment

                              Working...
                              X