What is the proper way to display an 2d icon that shows an object can be used?

I’m working on a first person game that has objects in the world that you can “use” once you are close to them and facing them. Currently I am doing this by checking for overlap of a box that is attached to the camera with the actors in the world that inherit from my “useable” class.

I’d like to display an icon on the screen, similar to Resident Evil 7 or Soma, that alerts the player that they are able to interact with the object from a distance. I currently have text that just appears on the screen from the debug log.

What is the right way to display a 2d icon on top of the object? I’d like to change my class to have a UPROPERTY that you pass in which will be the 2d icon to display, which would be a texture I suppose.

Thanks!

Attached is an example picture from RE7, and my code for my player.


#include “MainCharacter.h”

#include “GameFramework/CharacterMovementComponent.h”
#include “Components/CapsuleComponent.h”
#include “Components/InputComponent.h”
#include “Camera/CameraComponent.h”
#include “Engine/World.h”
#include “Components/BoxComponent.h”

#include “ReactToUseInterface.h”

AMainCharacter::AMainCharacter()
{
// change the capsule component size
GetCapsuleComponent()->InitCapsuleSize(15, 80);

// create camera
FirstPersonCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
FirstPersonCameraComponent->SetupAttachment(GetCapsuleComponent());
FirstPersonCameraComponent->RelativeLocation = FVector(0.f, 0.f, 165.f - 80.f);
FirstPersonCameraComponent->bUsePawnControlRotation = true;

// create box to detect overlap of useable actors and attach it to the camera
BoxComponentDetectUseableActors = CreateDefaultSubobject<UBoxComponent>(TEXT("DetectUseableBox"));
BoxComponentDetectUseableActors->InitBoxExtent(FVector(30, 15, 30));
BoxComponentDetectUseableActors->SetupAttachment(FirstPersonCameraComponent);
BoxComponentDetectUseableActors->RelativeLocation = FVector(30.f, 0.f, 0.f);
BoxComponentDetectUseableActors->OnComponentBeginOverlap.AddDynamic(this, &AMainCharacter::OnOverlapBegin);
BoxComponentDetectUseableActors->OnComponentEndOverlap.AddDynamic(this, &AMainCharacter::OnOverlapEnd);

// adjust movement speeds
GetCharacterMovement()->MaxWalkSpeed = 150;
GetCharacterMovement()->NavAgentProps.bCanCrouch = true;
GetCharacterMovement()->MaxWalkSpeedCrouched = 100;

ActiveObject = nullptr;
bZoomingIn = false;
PrimaryActorTick.bCanEverTick = true;
ZoomFactor = 0;

}

void AMainCharacter::BeginPlay()
{
Super::BeginPlay();
}

void AMainCharacter::CrouchOn() {
Crouch();
}

void AMainCharacter::CrouchOff() {
UnCrouch();
}

void AMainCharacter::MoveForward(float input)
{
FRotator Rotation = GetControlRotation();
Rotation.Pitch = 0;
FVector MovementDirection = FRotationMatrix(Rotation).GetScaledAxis(EAxis::X);

AddMovementInput(MovementDirection, input);

}

void AMainCharacter::MoveRight(float input)
{
FRotator Rotation = GetControlRotation();
Rotation.Pitch = 0;
FVector MovementDirection = FRotationMatrix(Rotation).GetScaledAxis(EAxis::Y);

AddMovementInput(MovementDirection, input);

}

void AMainCharacter::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult &SweepResult) {
if (OtherActor && OtherActor->GetClass()->ImplementsInterface(UReactToUseInterface::StaticClass())) {
if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::White, TEXT(“Found usable!”));
ActiveObject = OtherActor;
}
}

void AMainCharacter::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) {
if (ActiveObject != nullptr) {
if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::White, TEXT(“Discarded usable.”));
ActiveObject = nullptr;
}
}

void AMainCharacter::Tick(float DeltaTime)
{
if (bZoomingIn) ZoomFactor += DeltaTime / .5f;
else ZoomFactor -= DeltaTime / .25f;
ZoomFactor = FMath::Clamp<float>(ZoomFactor, 0.0f, 1.0f);

FirstPersonCameraComponent-&gt;FieldOfView = FMath::Lerp&lt;float&gt;(90.0f, 30.0f, ZoomFactor);

}

void AMainCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);

InputComponent-&gt;BindAxis("MoveForward", this, &AMainCharacter::MoveForward);
InputComponent-&gt;BindAxis("MoveRight", this, &AMainCharacter::MoveRight);
InputComponent-&gt;BindAxis("Turn", this, &AMainCharacter::AddControllerYawInput);
InputComponent-&gt;BindAxis("LookUp", this, &AMainCharacter::AddControllerPitchInput);

InputComponent-&gt;BindAction("Use", IE_Pressed, this, &AMainCharacter::UseObject);

InputComponent-&gt;BindAction("Crouch", IE_Pressed, this, &AMainCharacter::CrouchOn);
InputComponent-&gt;BindAction("Crouch", IE_Released, this, &AMainCharacter::CrouchOff);

InputComponent-&gt;BindAction("Zoom", IE_Pressed, this, &AMainCharacter::ZoomIn);
InputComponent-&gt;BindAction("Zoom", IE_Released, this, &AMainCharacter::ZoomOut);

}

void AMainCharacter::UseObject() {
/*
// This is code for when you trace to an actor to use it. It didn’t work well as the tolerance for looking at the object is
// exact.
FHitResult HitResult(ForceInit);
FVector StartTrace = FirstPersonCameraComponent->GetComponentLocation();
FVector ForwardVector = FirstPersonCameraComponent->GetForwardVector();
FVector EndTrace = (ForwardVector * 100.f) + StartTrace;
FCollisionQueryParams TraceParams(ForceInit);

if (GetWorld()-&gt;LineTraceSingleByChannel(HitResult, StartTrace, EndTrace, ECC_Visibility, TraceParams)) {
    ActiveObject = HitResult.GetActor();

    if (ActiveObject != nullptr) {
        if (ActiveObject-&gt;GetClass()-&gt;ImplementsInterface(UReactToUseInterface::StaticClass())) {
            IReactToUseInterface *ReactingObject = Cast&lt;IReactToUseInterface&gt;(ActiveObject);
            ReactingObject-&gt;ReactToUse();
        }
    }
}*/
if (ActiveObject != nullptr) {
    if (ActiveObject-&gt;GetClass()-&gt;ImplementsInterface(UReactToUseInterface::StaticClass())) {
        IReactToUseInterface *ReactingObject = Cast&lt;IReactToUseInterface&gt;(ActiveObject);
        ReactingObject-&gt;ReactToUse();
    }
}

}

void AMainCharacter::ZoomIn() {
bZoomingIn = true;
}

void AMainCharacter::ZoomOut() {
bZoomingIn = false;
}

  • provide a way for your HUD class to know if a game object is selected
  • draw the icon in the HUD based on the world location of the selected game object
  • there are multiple way to project world location >> screen location, but if you have a HUD widget C++ class you can translate with “UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition()”
  1. I can do this by having an overlap check which triggers the widget to draw.
    2/3. How do you draw the widget on screen in the correct location? I tried this:

static FVector2D ScreenPosition;
if (UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition(GetWorld()->GetFirstPlayerController(), GetWorld()->GetFirstPlayerController()->GetPawn()->GetActorLocation(), ScreenPosition)) {
RotatableWidget->SetPositionInViewport(ScreenPosition, true);
}

I figured out how to do it. For others thinking about this:

  • Make your interface for the objects that are interactable have a UUserWidget and a function to get the actor and that widget
  • On overlap, trigger tick to start displaying it like so:

// if there is an active object
if (ActiveObject != nullptr) {
if (ActiveObject->GetWidget() != nullptr) {
FVector loc1 = ActiveObject->GetActor()->GetActorLocation();
//if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::White, FString::Printf(TEXT(“loc1 = %f, %f”), loc1.X, loc1.Y));

        FVector loc2 = FirstPersonCameraComponent-&gt;GetComponentLocation();
        //if (GEngine) GEngine-&gt;AddOnScreenDebugMessage(-1, 5.0f, FColor::White, FString::Printf(TEXT("loc2 = %f, %f"), loc2.X, loc2.Y));

        float dist = FVector::DistXY(loc1, loc2);

        //if (GEngine) GEngine-&gt;AddOnScreenDebugMessage(-1, 5.0f, FColor::White, FString::Printf(TEXT("Distance = %f"), dist));

        if (dist &gt; 100) dist = 100;

        FVector2D scale((100 - dist) / 100, (100 - dist) / 100);

        ActiveObject-&gt;GetWidget()-&gt;SetRenderScale(scale);

        FVector2D ScreenPosition;
        if (UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition(GetWorld()-&gt;GetFirstPlayerController(), loc1, ScreenPosition)) {
            //if (GEngine) GEngine-&gt;AddOnScreenDebugMessage(-1, 5.0f, FColor::White, FString::Printf(TEXT("Screen position = %f, %f"), ScreenPosition.X, ScreenPosition.Y));
            ActiveObject-&gt;GetWidget()-&gt;SetPositionInViewport(ScreenPosition, false);
        }
    }
}