Healthbars

Published by

on

Ahh UI stuff, not going to lie I don’t really enjoy it, it’s a lot of wiring up and glue code, but it’s what makes your game actually look good and have a good UX so you got to do it. I usually default to doing whatever benui and his wonderful resources tell me to do, and this is post is no different!

Where we were at

If I recall correctly, I had the majority of the damage calculations and logic working in the server but not for the client. I also had some pretty game breaking bugs in my logic (like never removing the apply damage entity after a character was hit, meaning they’d get hit every tick).

So let’s focus on the changes for the client and what I had to do to get the damage from the server, through flecs, to the UE code and finally to a health bar widget.

The challenges

There’s a few things we have to do to get this working:

  • Receive the network actors health information over the network
  • Update the actor’s “Attributes” UE component
  • Create an actor WidgetComponent
  • Make sure the WidgetComponent always faces the camera
  • Create a custom UserWidget with our health/shield bars
  • Bind the UserWidget C++ code with the Blueprint UMG
  • Update the widget only when health actually changes

While this may seem straight forward, I got caught up on the last two points. Without further ado, let’s look at some code and UMG stuff!

Health information

Inside of our UE GameMode we register our flecs.dev systems which handle updates coming in from the network. In this case we just add in two more components:

  • Attributes – A character’s Health, Shield, Stamina, and Mana.
  • TraitAttributes – A character’s Intelligence, Wisdom, Dexterity, Strength and Constitution.

A custom UE5 component called an UPMOAttributesComponent is parented to all network (and the player) characters. This class also creates a custom Delegate which is essential for broadcasting updates to other classes such as our UserWidget.

DECLARE_MULTICAST_DELEGATE(FOnAttributesUpdated);
// ... class definition ... //
public:
	void UPMOAttributesComponent::Update(units::Attributes& UpdatedAttributes, units::TraitAttributes& UpdatedTraits)
	{
		// check before we overwrite
		bool bHasUpdated = (Attributes != UpdatedAttributes || Traits != UpdatedTraits);
	
		Attributes = UpdatedAttributes;
		Traits = UpdatedTraits;
	
		if (bHasUpdated)
		{
			AttributesUpdatedDelegate.Broadcast();
		}
	}
	
	FOnAttributesUpdated& GetAttributesUpdatedDelegate() { return AttributesUpdatedDelegate; };
	
private:
	units::Attributes Attributes;
	units::TraitAttributes Traits;
	flecs::entity FlecsOwner;

	FOnAttributesUpdated AttributesUpdatedDelegate;
}

Making the WidgetComponent face the player

A WidgetComponent is a UE component we can add to our network actors (or our player). You use this component as a container to hold widgets. This makes it so we can design our widgets in UMG then have them attached to each actor.

I opt’d to make a new class called UWFloatingWidgetComponent that will be used just for adding these floating widgets above in game actors. I wanted to do this so I could bake in having the WidgetComponent always face the player, no matter what widget was contained inside.

Every tick this component calls FacePlayer which updates the rotation of the widget component (and the widget housed inside of it):

void UUWFloatingWidgetComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
    FacePlayer();
}

void UUWFloatingWidgetComponent::FacePlayer()
{
    auto FirstPlayer = GetWorld()->GetFirstPlayerController();
    if (!FirstPlayer || !FirstPlayer->PlayerCameraManager)
    {
        return;
    }

    auto WidgetLocation = GetComponentLocation();
    auto TargetLocation = FirstPlayer->PlayerCameraManager->GetTargetLocation();
    auto NewRotation = UKismetMathLibrary::FindLookAtRotation(WidgetLocation, TargetLocation);
    SetWorldRotation(NewRotation);
}

Our custom health bar

Here’s the health bar in UMG, pretty amazing huh?
Pasted image 20241101190210.png
You’ll note that in the top right corner this blueprint UMG inherits from a custom C++ class called UWFloatingHealthBar. You may also note that the selected HealthBar has the “Is Variable” checked, this is necessary so we can update it’s value directly from C++ whenever our delegate event is called.

The naive approach would be to simply update the healthbar on every tick. This is what you’ll find in a lot of tutorials. But if you think about having 100 players on screen, most of which are not having the health bar change, you’ll quickly see what that would be inefficient.

To update these values from C++, you need to use a custom UPROPERTY called BindWidget, here’s our FloatingHealthBar class:

class PMOCLIENT_API UUW_FloatingHealthBar : public UUserWidget, public IIFloatingWidget
{
	GENERATED_BODY()

public:
	virtual bool Initialize() override;

	virtual void SetComponent(class UUWFloatingWidgetComponent* Component) override;

	void OnUpdateAttributes();
	
	void SetMaxHealth(float MaxHealthValue);

	void SetMaxEnergy(float MaxEnergyValue);

	void SetMaxShield(float MaxShieldValue);

	void UpdateHealth(float NewValue);

	void UpdateShield(float NewValue);

	void UpdateEnergy(float NewValue);

protected:

	UPROPERTY(meta=(BindWidget))
	UProgressBar* HealthBar;
	float MaxHealthValue;
	float HealthValue;

	UPROPERTY(meta=(BindWidget))
	UProgressBar* ShieldBar;
	float MaxShieldValue;
	float ShieldValue;
private:
	TObjectPtr<UUWFloatingWidgetComponent> Owner;
	TObjectPtr<UPMOAttributesComponent> Attributes;
	FNumberFormattingOptions NumberFormatOptions;
}

By putting the UPROPERTY above our ProgressBar’s with the SAME name as what we declared “Is Variable” we now can link the C++ class’s progressbar with our UMG blueprint class variables.

There’s actually a lot of complexity to wire these things up, mainly because our UserWidget has no idea it’s contained in a WidgetComponent, or which Actor it’s apart of! For that, we need to inject the details, and to inject the details… we need a UInterface.

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UIFloatingWidget : public UInterface
{
	GENERATED_BODY()
};

class PMOCLIENT_API IIFloatingWidget
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void SetComponent(class UUWFloatingWidgetComponent* Component) = 0;
};

What this IIFloatingWidget allows us to do is, inside of our WidgetComponent, check if any contained widgets implement the UIFloatingWidget interface. If they do, we can call SetComponent on it.

This is a nice abstraction because now I can throw in any custom widget type and the component will be able to set a reference to itself. So now if I want to add say a Damage over time widget that shows up above a players head, I can just implement this SetComponent method and inherit from the IIFloatingWidget and everything will just work.

Here’s the UUWFloatingWidgetComponent doing just that:

void UUWFloatingWidgetComponent::BeginPlay()
{
    InitWidget();
    auto FloatingWidget = Cast<IIFloatingWidget>(GetWidget());
    if (FloatingWidget)
    {
        FloatingWidget->SetComponent(this);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("FloatingWidget not set, GetWidget() returned null or not a FloatingWidget, can't call SetComponent"));
    }
    Super::BeginPlay();
}

Using UE’s Cast method, we check if the widget it contains has a SetComponent method, if it does, great! We can call it and inject ourselves. Once we do that, the UserWidget can access the underlying actor owner. Here’s what our UW_FloatingHealthBar does when SetComponent is called:

void UUW_FloatingHealthBar::SetComponent(class UUWFloatingWidgetComponent* Component)
{
    Owner = Component;
    if (Owner)
    {
        Attributes = Owner->GetOwner()->GetComponentByClass<UPMOAttributesComponent>();
        Attributes->GetAttributesUpdatedDelegate().AddUObject(this, &ThisClass::OnUpdateAttributes);
        UE_LOG(LogTemp, Error, TEXT("UUW_FloatingHealthBar::SetComponent added delegate for OnUpdate"));
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("UUW_FloatingHealthBar::SetComponent failed to init delegate"));
    }
    SetMaxHealth(units::MaxHealth);
    SetMaxShield(units::MaxShield);
}

SetComponent does two things:

  • Get’s the PMOAttributesComponent to get a reference to the OnAttributesUpdatedDelegate and binds our UserWidget’s OnUpdateAttributes method to it
  • Initializes the progress bars max values (so we can calculate the percentages)

We can now have our widget get updated whenever the values change.

I wish I could say this went smoothly but it did not, I ran into quite a few problems (refactor’d some things…) but for the life of me I could not get the progress bars to update.

For some reason my original design (before I used the UInterface) I would create my custom HealthBar class but the progress bars were always null. After bashing my head for a few hours I asked in the Unreal Source discord, and got some lovely help from ben ui himself as well as another user!

I wanted to share Ben’s list (with his permission!) of trouble shooting steps because they are a great collection of steps to take:

From Ben:

Stuff I would check in decreasing order of sanity (apologies you’ve probably tried all of these already but just rubber-ducking here):

  1. When are you checking for existence of the progress bar? If it’s too early it might not yet be initialized
  2. How are you instantiating your Blueprint widget? CreateWidget? NewObject? Something in BP-land?
  3. When you spawn the widget, can you see it?
  4. Make sure whatever is creating the widget instance is pointing to the BP subclass and not the base C++ class. I’ve done that before. I like to make my base C++ classes UCLASS(Abstract) so I can’t accidentally pick them instead of the BP subclass.
  5. Make a NativeConstruct() function in C++, set a breakpoint, make sure it’s getting hit where you expect
  6. Try with other types of widget, does it work with UImage, but not UProgressBar?
  7. Make another BP subclass of the class you’re expecting, see if it is similarly cursed.
  8. Make a new C++ class that’s the bare minimum, see if that works.
  9. Threaten the PC with violence.
  10. Give up and live in the woods.

Not going to lie, I almost made it to step 10, and made it to step 9 many times. I think my problem was point 4, but I refactored everything to use an interface and ensured InitWidget() was called from the BeginPlay of my WidgetComponent, I think those two things are what finally made it start working!

Anyways, I’m now working on finishing up the die/respawn logic. I’ve also started on adding a blocking mechanic (with stamina depleting) so I can start fighting my kid in my game!