Character Health Bar UI using C++

In this tutorial video, I explain how to set up in-world widgets that are attached to actors, using the UWidgetComponent class.

Dog.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Dog.generated.h"

UCLASS()
class EXAMPLE_API ADog : public AActor
{
	GENERATED_BODY()
	
public:	
	ADog( const FObjectInitializer& ObjectInitializer );

	virtual void Tick(float DeltaTime) override;

	float GetHealth() const { return Health; }
	void SetHealth( float val ) { Health = val; }

	float GetMaxHealth() const { return MaxHealth; }
	void SetMaxHealth( float val ) { MaxHealth = val; }

protected:
	virtual void BeginPlay() override;

	UPROPERTY( VisibleAnywhere )
		class UWidgetComponent* HealthWidgetComp;

	FVector MovementVelocity;

	float Health;
	float MaxHealth = 120;

	float HealthTweenDirection;
};

Dog.cpp

#include "Dog.h"
#include <Components/WidgetComponent.h>
#include "HealthBar.h"

ADog::ADog( const FObjectInitializer& ObjectInitializer )
	: Super( ObjectInitializer )
{
	PrimaryActorTick.bCanEverTick = true;

	if ( RootComponent == nullptr )
	{
		RootComponent = ObjectInitializer.CreateDefaultSubobject<USceneComponent>( this, TEXT( "Root" ) );
	}

	HealthWidgetComp = ObjectInitializer.CreateDefaultSubobject<UWidgetComponent>( this, TEXT( "HealthBar" ) );
	HealthWidgetComp->AttachToComponent( RootComponent, FAttachmentTransformRules::KeepRelativeTransform );

	Health = MaxHealth;
}

void ADog::BeginPlay()
{
	Super::BeginPlay();

	UHealthBar* HealthBar = Cast<UHealthBar>( HealthWidgetComp->GetUserWidgetObject() );
	HealthBar->SetOwnerDog( this );

	// Make sure every dog starts with different health
	Health = FMath::RandRange( 0.0f, MaxHealth );
	HealthTweenDirection = FMath::RandBool() ? 1 : -1;

	// Move in a random direction at a random speed
	const float DirRads = FMath::RandRange( 0.0f, 2 * PI );
	MovementVelocity = FVector( FMath::Cos( DirRads ), FMath::Sin( DirRads ), 0 ) * 10.0f;

	// Face that way too
	RootComponent->SetWorldRotation( FRotator::MakeFromEuler( FVector( 0, 0, DirRads / PI * 180 - 90) ) );
}

void ADog::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	SetActorLocation( GetActorLocation() + MovementVelocity * DeltaTime );

	// Bounce health between min and max to show off health bar
	static const float TweenSpeed = 10.0f;
	Health = FMath::Clamp<float>( Health + DeltaTime * HealthTweenDirection * TweenSpeed, 0, MaxHealth );
	if ( ( Health == MaxHealth && HealthTweenDirection == 1 )
		|| ( Health == 0 && HealthTweenDirection == -1 ) )
	{
		HealthTweenDirection *= -1;
	}
}

HealthBar.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Dog.h"
#include "HealthBar.generated.h"

UCLASS( Abstract )
class EXAMPLE_API UHealthBar : public UUserWidget
{
	GENERATED_BODY()
public:
	void SetOwnerDog( ADog* InDog ) { OwnerDog = InDog; }
	
protected:
	void NativeTick( const FGeometry& MyGeometry, float InDeltaTime ) override;

	TWeakObjectPtr<ADog> OwnerDog;

	UPROPERTY( meta = ( BindWidget ) )
		class UProgressBar* HealthBar;
	UPROPERTY( meta = ( BindWidget ) )
		class UTextBlock* CurrentHealthLabel;
	UPROPERTY( meta = ( BindWidget ) )
		class UTextBlock* MaxHealthLabel;

};

HealthBar.cpp

#include "HealthBar.h"
#include <Components/ProgressBar.h>
#include <Components/TextBlock.h>

void UHealthBar::NativeTick( const FGeometry& MyGeometry, float InDeltaTime )
{
	Super::NativeTick( MyGeometry, InDeltaTime );

	if ( !OwnerDog.IsValid() )
		return;

	HealthBar->SetPercent( OwnerDog->GetHealth() / OwnerDog->GetMaxHealth() );

	FNumberFormattingOptions Opts;
	Opts.SetMaximumFractionalDigits( 0 );
	CurrentHealthLabel->SetText( FText::AsNumber( OwnerDog->GetHealth(), &Opts ) );
	MaxHealthLabel->SetText( FText::AsNumber( OwnerDog->GetMaxHealth(), &Opts ) );
}