Announcement

Collapse
No announcement yet.

Easy Offscreen Indicator Blueprint Node

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

    #46
    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


    Code:
    // 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;
    
    }

    Comment


      #47
      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 Enlo'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, Enlo and others who contributed this routine and changes, it simplified my task.

      Code:
      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;
      }

      Comment


        #48
        None of these versions worked with UMG widgets. So I had to write one myself, based on the last published on by eagletree

        Code:
        #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:
        Code:
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" });

        Comment


          #49
          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.

          Comment


            #50
            can anyone shed some light on how to use this in VR? I think ProjectWorldLocationToWidgetPosition doesn't work.

            Comment

            Working...
            X