How should I implement HP Bar for ai and player?

Hi everyone. I’m implementing HP Bar for ai and player right now. At first, I only made player’s Hp Bar visible.


I made the widget pop on top of my head as shown in the picture above.
image
The player made it appear on the screen as shown in the picture above.

Now ai and player are inheriting the base class. Base has set up the deligate. Let me show you the code.

HpWidget code.

void UOLHpBarWidget::NativeConstruct()
{
	Super::NativeConstruct();

	HpBarProgress = Cast<UProgressBar>(GetWidgetFromName(TEXT("HpBar")));
	ensure(HpBarProgress);

	APawn* Player = GetOwningPlayerPawn();
	ensure(Player);

	IOLHpInterface* CharacterInterface = Cast<IOLHpInterface>(Player);
	ensure(CharacterInterface);

	CharacterInterface->SettingHpBar(this);
}

void UOLHpBarWidget::UpdateHpBar(float CurrentHp)
{
	UE_LOG(LogTemp, Error, TEXT("Update Hpabar?"));

	if (HpBarProgress)
	{
		HpBarProgress->SetPercent(CurrentHp); 
		UE_LOG(LogTemp, Error, TEXT("Update Hp"));
	}
}

Base code inherited by ai and Player.

// .h
protected:
	virtual void SettingHpBar(class UOLHpBarWidget* HpWidget) override;	
	virtual void ChangeHp() override;
	class UOLHpBarWidget* HpBarWidget;

// .cpp
void AOLCharacterBase::SettingHpBar(UOLHpBarWidget* HpWidget)
{
	HpBarWidget = HpWidget;
}

void AOLCharacterBase::ChangeHp()
{
}

Player Code

// .h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnHpChangeDelegate, float /*CurrenHp*/);
public:
	FOnHpChangeDelegate OnHpChange;

// HP
protected:
	virtual void SettingHpBar(class UOLHpBarWidget* HpWidget) override; // A function for setting up a deligate.
	virtual void ChangeHp() override;	// Function to reflect hp in widget

// .cpp
void AOLCharacterPlayer::SettingHpBar(UOLHpBarWidget* HpWidget)
{
	//UOLHpBarWidget* HpBarWidget = Cast<UOLHpBarWidget>(HpBar);	
	OnHpChange.AddUObject(HpBarWidget, &UOLHpBarWidget::UpdateHpBar);
}

void AOLCharacterPlayer::ChangeHp()
{
	OnHpChange.Broadcast(Hp / MaxHp);
	UE_LOG(LogTemp, Warning, TEXT("Player Change Hp"));
}

ai Code

// .h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnHpChangeDelegate, float /*CurrenHp*/);
public:
	FOnHpChangeDelegate OnHpChange;

protected:
	virtual void SettingHpBar(class UOLHpBarWidget* HpWidget) override; // A function for setting up a deligate.
	virtual void ChangeHp() override;	// Function to reflect hp in widget

// .cpp
void AOLNPCBasic::SettingHpBar(UOLHpBarWidget* HpWidget)
{
	//UOLHpBarWidget* HpBarWidget = Cast<UOLHpBarWidget>(HpBar);
	OnHpChange.AddUObject(HpBarWidget, &UOLHpBarWidget::UpdateHpBar);
}

void AOLNPCBasic::ChangeHp()
{
	OnHpChange.Broadcast(Hp / MaxHp);	
	UE_LOG(LogTemp, Warning, TEXT("NPC Change Hp"));
}

The code is too long. Sorry, but ChangeHp functions are called when being attacked or recovering from physical strength. Logs are visible for both ai and player. When ChangeHp is called, the UpdateHpBar function is called. But the log doesn’t show up at this time. I think there’s a problem with widget settings, but I don’t know how to fix it…
For reference, the reason why ai and player have the same function and the same content was that when changeHp was written in Base, ai referred to the player’s widget as it was, and when the player was attacked, the widget in ai moved the same way as the player.

This is getting too long. Thank you for reading. I really appreciate your help.
(The language may not be natural because of the use of a translator.)

I could be wrong, but I don’t think GetOwningPlayerPawn() works inside a widget inside a widget component. There’s no need to do this anyhow. Just use WidgetComponent->GetUserWidgetObject() and cast it to your widget class. You can do this on BeginPlay. At least, that’s what I’m doing and it works fine.

Then you have an IOLHpInterface. Is that an interface that can be used in Blueprints? If so, you’re not supposed to call methods directly unless your interface has this UINTERFACE tag:

meta = (CannotImplementInterfaceInBlueprint)

Otherwise, you need to use something like this:

TScriptInterface<IOLHpInterface> CharacterInterface(ParentActor);
CharacterInterface->Execute_SettingHpBar(CharacterInterface->_getUObject(), this);

OR

IOLHpInterface::Execute_SettingHpBar(ParentActor, this);

As for the logs, it wasn’t clear from your description what is working and what isn’t. You said this:

But immediately after, you say this:

Which is it? And what logs work and what logs don’t work?

Anyhow, from what I’m seeing, if GetOwningPlayerPawn() does work, it’ll always return the same value so you’re just setting the widget on the player actor. The NPC’s widget pointer is always null. I would be very surprised if the NPC’s HpBarWidget member is not null.

Thank you so much for your response!

It’s not an interface that can be used in Blueprint!

I changed the code as I understood it. For now, Deligate has set up player and ai separately.

player code

// HP
protected:
	virtual void SettingHpBar(class UOLHpBarWidget* HpWidget) override;	
	virtual void DrinkHpPotion(float HpAmount) override;	
	virtual void ChangeHp() override;	

void AOLCharacterPlayer::SettingHpBar(UOLHpBarWidget* HpBar)
{
	if (IsPlayerControlled())
	{
		UOLHpBarWidget* HpBarWidget = Cast<UOLHpBarWidget>(HpBar);	// 초기화
		OnHpChange.AddUObject(HpBarWidget, &UOLHpBarWidget::UpdateHpBar);
		UE_LOG(LogTemp, Warning, TEXT("Player Setting HpBar"));
	}
}

void AOLCharacterPlayer::DrinkHpPotion(float HpAmount)
{
	Hp = FMath::Clamp(Hp + HpAmount, 0.0f, MaxHp);	
	ChangeHp();	
}

void AOLCharacterPlayer::ChangeHp()
{
	OnHpChange.Broadcast(Hp / MaxHp);	
	UE_LOG(LogTemp, Warning, TEXT("Player Change Hp"));
}

First of all, the important thing in the code above is that there are a total of 4 npc in the map, UE_LOG (LogTemp, Warning, TEXT (“Player Setting HpBar”) including player; that log appears 5 times. I don’t know why. And the important thing is that when a player is attacked, ai seems to share that widget as well. There are fewer widgets together.

ai code

AOLNPCBasic::AOLNPCBasic()
{
	// HP Bar
	HpBar = CreateDefaultSubobject< UWidgetComponent>(TEXT("HpBar"));
	HpBar->SetupAttachment(GetMesh());	// 메시에 부착
	HpBar->SetRelativeLocation(FVector(0.0f, 0.0f, 202.0f));	// 머리 위에 위치하게(부착된 메시를 기준으로 움직임)
	// 월드공간 기준으로 배치될지(3D UI), 스크린 좌표 기준으로 배치될지(2D UI) 결정
	// Screen은 절대로 화면 밖으로 짤리지 않는다
	HpBar->SetWidgetSpace(EWidgetSpace::Screen);
	HpBar->SetDrawSize(FVector2D(150.0f, 20.0f));	// 위젯 크기
	HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision);	// 충돌 없음

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

	// HP Bar
	UUserWidget* Widget = HpBar->GetUserWidgetObject();
	UOLHpBarWidget* HpWidget = Cast<UOLHpBarWidget>(Widget);
	OnHpChange.AddUObject(HpWidget, &UOLHpBarWidget::UpdateHpBar);

}

void AOLNPCBasic::ChangeHp()
{
	OnHpChange.Broadcast(Hp / MaxHp);	// 체력이 바뀌면 신호 보내기, 해당 캐릭터
	UE_LOG(LogTemp, Warning, TEXT("NPC Change Hp"));
}

Base code


float AOLCharacterBase::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	Hp = FMath::Clamp(Hp - DamageAmount, 0.0f, MaxHp);	// 체력  업데이트

	if (AOLCharacterPlayer* Player = Cast<AOLCharacterPlayer>(HpUpdateTarget))
	{
		IOLUpdateHpInterface* HpInterface = Cast<IOLUpdateHpInterface>(Player);
		HpInterface->ChangeHp();
	}
	else if (AOLNPCBasic* NPC = Cast<AOLNPCBasic>(HpUpdateTarget))
	{
		IOLUpdateHpInterface* HpInterface = Cast<IOLUpdateHpInterface>(NPC);
		HpInterface->ChangeHp();
	}
	

	if (Hp <= 0.0f && !IsDead)
	{
		DeadCheck();
	}

	return DamageAmount;
}

The reason I wrote the if statement in the code above is because if a player is attacked, the widget in the ai keeps moving the same way as the player’s widget. But it didn’t work. I think I’m doing something wrong. There are so many things I don’t know.. (sorry)

If there’s a change in fitness, I’ve made it to log in withget. If a player is attacked, only one player has a change in fitness, so with with without, you’ll only get one log. But a total of 5 logs. Including ai’s.
And importantly, if the ai is actually attacked, the widgets in all the ai will change…

For your AOLCharacterPlayer, you should really get rid of the SettingBar function. Also remove the call from the widget.

Your widget should initialize like this:

void UOLHpBarWidget::NativeConstruct()
{
	Super::NativeConstruct();

	HpBarProgress = Cast<UProgressBar>(GetWidgetFromName(TEXT("HpBar")));
	ensure(HpBarProgress);
}

Your AOLNPCBasic constructor should call the base class constructor.

AOLNPCBasic::AOLNPCBasic()
  : Super()
{
...
}

I like how you’re setting up the widget in the NPC class and how you set up the delegate in BeginPlay(). You should do the same thing with your player character. This way, you don’t need the SettingBar function at all and you can safely remove it as mentioned above.

I’m not sure I understand what the issue is, but the fact that you said the log is being called 5 times is likely because you’re creating 5 widgets and this code:

APawn* Player = GetOwningPlayerPawn();

always returns the player character even if it’s on an NPC. So it will call SettingHpBar() 5 times on your player character. This is why you see the same log 5 times. Now, shouldn’t IsPlayerControlled() return false for the NPCs? You would think so. So I’m not sure what’s going on there. Did you correctly set the AIController on your NPCs?

Anyhow, try removing the SettingHpBar function and set up your character widget the same way you did with your NPC with the BeginPlay function. After that, your widgets should start working a lot better.

BTW, you don’t need an interface to call ChangeHp(). It’s virtual. You can just call it directly in the TakeDamage function.

this->ChangeHp();

Not sure what HpUpdateTarget is or how it was initialized, but you don’t need it for calling ChangeHp();

Just like the NPC, the player also set up a character widget. It works well.
But the problem is that I made the hp widget look in the viewport (player). But the hp widget in the viewport doesn’t reduce the stamina. Only the widget above the player character’s head works. Why is it like this..?

Sorry, I don’t understand.

Oh, so you have TWO widgets for the player’s HP? The one over the head of the player character is just HP. The one in the viewport is HP + Stamina. Is that correct?

So the stamina bar isn’t updating in the viewport widget?

That one only has HP. Does the viewport widget update at all? Does the viewport’s widget’s hp bar update?

Or did you remove the hp widget over the player character and replace it with the viewport one that also shows stamina?

So far, we’ve only talked about NPC and player character widgets. I didn’t understand you had a viewport widget as well. Where is that set up? For this to work, your player character needs to know about the viewport widget and update it, or the widget needs to know about the player and register its own delegate.

I get the feeling you replaced the old player widget with the viewport one and the stamina bar won’t update. Show me the code where you update the stamina bar.

Thank you for replying to me. Let me explain the situation again for now. I don’t think I was able to explain it in detail. I’m sorry.
If you look at what I wrote at the top, the HP Bar shown as a viewport in the second picture (yellow is a skill, so you don’t have to worry…:)) exists.
Originally, I wasn’t going to create an HP Bar over the head of the character. The player only sees Hp through the viewport, and the npc was my purpose to see the Hp Bar over the headof the npc.

The Viewport Hp widget will not update. The Viewport Hp widget will update the first time you start the game, but the Viewport Hp widget will not update when you restart the game.
I temporarily created two Deligates in the player for now. One is the overhead HP widget, and one is the Viewport widget.

I’ll show you the code.

void UOLHpBarWidget::NativeConstruct()
{
	Super::NativeConstruct();

	HpBarProgress = Cast<UProgressBar>(GetWidgetFromName(TEXT("HpBar")));
	ensure(HpBarProgress);

	APawn* Player = GetOwningPlayerPawn();
	ensure(Player);

	IOLHpInterface* CharacterInterface = Cast<IOLHpInterface>(Player);
	ensure(CharacterInterface);

	UE_LOG(LogTemp, Display, TEXT("GetOwning Player Pawn"));
	CharacterInterface->SettingHpBar(this);
}

I thought I’d need a SettingHpBar, so I saved it again.. :anxious_face_with_sweat:

DECLARE_DELEGATE_OneParam(FOnUpdateHpChangeDelegate, float /*CurrenHp*/);
DECLARE_DELEGATE_OneParam(FOnHpWidgetChangeDelegate, float /*CurrenHp*/);

	// Delegate
public:
	FOnUpdateHpChangeDelegate OnUpdateHpChange;
	FOnHpWidgetChangeDelegate OnHpWidgetChange;

// HP
protected:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = UI, Meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UWidgetComponent> HpBar;

	virtual void SettingHpBar(class UOLHpBarWidget* HpWidget) override; // Interface function for Viewport Deligates
	virtual void DrinkHpPotion(float HpAmount) override;	

public:
	virtual void ChangeHp() override;	

// .cpp
void AOLCharacterPlayer::SettingHpBar(UOLHpBarWidget* HpWidget)
{
	UOLHpBarWidget* HpBarWidget = Cast<UOLHpBarWidget>(HpWidget);	
	OnHpWidgetChange.BindUObject(HpBarWidget, &UOLHpBarWidget::UpdateHpBar);	

	UE_LOG(LogTemp, Warning, TEXT("Seiitng HP Bar Player"));

}

void AOLCharacterPlayer::DrinkHpPotion(float HpAmount)
{
	Hp = FMath::Clamp(Hp + HpAmount, 0.0f, MaxHp);	
	ChangeHp();
}

void AOLCharacterPlayer::ChangeHp()
{
	if (OnUpdateHpChange.IsBound())
	{
		if (OnHpWidgetChange.IsBound())
		{
			OnHpWidgetChange.Execute(Hp / MaxHp);// viewport widget
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("No OnHpWidgetChange"));

		}
		OnUpdateHpChange.Execute(Hp / MaxHp);
		

		UE_LOG(LogTemp, Warning, TEXT("Player Change Hp"));
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("No Player Bound"));

	}
} 

The above code FnHpWidgetChangeDelegate, or OnHpWidgetChange, is about the Viewport widget.

float AOLCharacterBase::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	Hp = FMath::Clamp(Hp - DamageAmount, 0.0f, MaxHp);	

	IOLUpdateHpInterface* HpInterface = Cast<IOLUpdateHpInterface>(this);
	HpInterface->ChangeHp();

...
}

The npc and the player have a common attack. The base dealt with the damage and through the interface, for example, if the npc was attacked, I thought the changeHp on the npc would be called and I wrote the code (maybe it’s wrong)

I wrote a reply, but I’m writing another reply because I’m afraid there’s something I left out.

No it doesn’t work. HP Bar will update when you first start the game, but Hp Bar won’t update when you restart the game.

I didn’t remove the Hp widget above the player’s head. The Viewport widget was originally there. From the very beginning!
I had a Viewport HP widget before, but I made an additional HP widget above the player’s head.

To sum things up, the current map has an HP widget over the player’s head, a Viewport HP widget, and an HP widget over the npc head.

In the beginning
Do you remember setting up the deligate through this function, GetOwingPlayerPawn()? When I first worked on the viewport widget, I updated the viewport Hp widget by setting the deligate through that function.

Are you using the Reset Level node or the Load Level node with the same level name? Either one will have the same effect. This does not reset your widgets in the viewport. So the setup code in your viewport widget won’t run again. This is why I wanted you to put your setup code in your player character actor. This is why I said to get rid of SettingHpBar. That function keeps causing you issues.

In your player character’s BeginPlay event, do this:

  TArray<UUserWidget*> HpWidgets;
  bool TopLevelOnly = false;
  UWidgetBlueprintLibrary::GetAllWidgetsOfClass(this->GetWorld(), HpWidgets, UOLHpBarWidget::StaticClass(), TopLevelOnly);
  check(!HpWidgets.IsEmpty());
  UOLHpBarWidget* HpBarWidget = Cast<UOLHpBarWidget>(HpWidgets[0]);
  OnHpWidgetChange.BindUObject(HpBarWidget, &UOLHpBarWidget::UpdateHpBar);

You’ll need this include at the top of your file somewhere.

#include "Blueprint/WidgetBlueprintLibrary.h"

And get rid of SettingHpBar.

Also, how are you adding the viewport widget? Make sure you’re not adding it twice. Or if you are, make sure the original one is removed.

I loaded the same level using the RestartLevel function. There was something I didn’t say, but before I started the game, the start screen appears. I pressed the start button on the start screen to get the viewport hp bar.

void AOLCharacterPlayer::BeginPlay()
{
	Super::BeginPlay();
...

	// HP Bar
	UUserWidget* Widget = HpBar->GetUserWidgetObject();
	UOLHpBarWidget* HpBarWidget = Cast<UOLHpBarWidget>(Widget);
	OnUpdateHpChange.BindUObject(HpBarWidget, &UOLHpBarWidget::UpdateHpBar);

	TArray<UUserWidget*> HpWidgets;	
	bool TopLevelOnly = false;	
	UWidgetBlueprintLibrary::GetAllWidgetsOfClass(Cast<AOLCharacterPlayer>(this)->GetWorld(), HpWidgets, UOLHpBarWidget::StaticClass(), TopLevelOnly);	
	check(!HpWidgets.IsEmpty());
	UOLHpBarWidget* HpWidget = Cast<UOLHpBarWidget>(HpWidgets[0]);
	OnHpWidgetChange.BindUObject(HpWidget, &UOLHpBarWidget::UpdateHpBar);
	UE_LOG(LogTemp, Warning, TEXT("HpWidgets: %d"), HpWidgets.Num());

...
}

I made the change like the code above. I got rid of settingHpBar altogether, and I wrote the code on beginPlay. But there is a problem.
The viewport hp bar does not update even after the game starts button is pressed and rebooted. npcpBar will update.


What I mean is, if you look at the picture above, do you see the hp of other hp bars besides the player character hp? That’s the hp bar of npc. When a player is attacked, some npc of hpBar is connected and blood is reduced together.

And when I printed out the HpWidgets log, a total of 6 were taken. There are 4 npc numbers in the world, 1 player, and 1 viewport hp bar. If you add it to the total, there are 6, but I think all of these are in the array.

Now I’m going to show you the code that floats the Viewport HP Bar.

void AOLPlayerController::GameMainMenu()
{
	if (MainMenuScreenClass && OLInstance->bMainMenuActive)
	{
		MainMenuScreen = CreateWidget(this, MainMenuScreenClass);

		if (MainMenuScreen)
		{
			MainMenuScreen->AddToViewport();
			OLInstance->bMainMenuActive = false;	
		}
	}
	else if (!OLInstance->bMainMenuActive)
	{
		GameStart();
	}
}

void AOLPlayerController::GameStartClicked()
{
	MainMenuScreen->RemoveFromParent();

	GameStart();
}

void AOLPlayerController::GameStart()
{
	...

	if (HUDClass)
	{
		HUD = CreateWidget(this, HUDClass);

		if (HUD)
		{
			HUD->AddToViewport();
		}
	}
}

To explain this code, first you start the game, the main menu screen appears. And when you press Start, you’ll see HUD (floating the HpBar Viewport widget) on the screen through GameStart.
And when the game is over, I erase the HUD through RemoveFromParent.
And when you start again, you start GameStart and activate HUD again.

What’s the problem… maybe I don’t understand very well. Thanks for answering though!

What I think is going on is that you have the same base class and you’re just grabbing all the widgets when you just want the viewport one. That’s why they’re all in the array. We need to figure out what the most derived class is of the viewport widget.

If you have a derived C++ class for your viewport widget, just use that in the call to GetAllWidgetsOfClass. BTW, you don’t need to cast ‘this’ in the same call.

OTOH, if you’re using the same C++ class for both widgets, then you’ll need to do what I indicate below. Note that you’ll need a DIFFERENT Blueprint for the viewport widget. If you’re using the exact same widget blueprint for both the overhead widget and the viewport widget, just duplicate it and replace your viewport widget with the duplicated one. I think you can also derive from it if you think you will modify your widget in the future. You can then right click on your widget in the UI editor and there’s an option to replace the widget if you have it selected in your content folder. You MUST have two different classes somewhere.

In your player chararcter’s header file, add something like this:

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TSubclassOf<UUserWidget> ViewportWidgetClass;

Shut down the editor and compile it. Back in the editor, go to your character blueprint and set this value to the viewport blueprint.

Then change the GetAllWidgetsOfClass line to this:

UWidgetBlueprintLibrary::GetAllWidgetsOfClass(Cast<AOLCharacterPlayer>(this)->GetWorld(), HpWidgets, ViewportWidgetClass, TopLevelOnly);

edit:

Another solution is that your player controller seems to be creating the HUD. So you could have your HUD have a function that returns the viewport HP widget. Then instead of GetAllWidgetsOfClass, you would just grab it from the player controller.

edit2:

Yet another solution is to add a variable to your widget and set it to something unique for the viewport one in one of your HUD’s construct events (NOT the HP widget’s construct). Then you can iterate over the widgets you get from GetAllWidgetsOfClass() until you find the one with the correct variable value.

So you have 4 possible solutions.

  1. Do you have a separate derived C++ class for your viewport HP widget? If so, just use that class in GetAllWidgetsOfClass()
  2. Use a Class property in your player character and set your viewport’s HP BP to it. If your viewport HP widget BP is the same as the overhead HP widget, you must duplicate it or derive from it and replace the viewport HP widget.
  3. Add a function to your HUD to return the viewport HP widget.
  4. Add a C++ variable to your widget and set it to a different value in your HUD’s construct event. Then scan for that value after you call GetAllWidgetsOfClass().

Any of these solutions will work. It’s up to you which one you like best. There’s probably other ways to do it. For example, a problem with #3 is that you must be familiar with how to call BP functions from C++.

Thanks to AlienRenders, we solved it.
Thank you for all the different solutions. I chose number two. I chose it because it was the simplest solution for me. Why didn’t I think about doing this before?

If it wasn’t for AlienRenders, I might not have solved it… I learned a lot and solved it thanks to you. Also, thank you so much for letting me know other ways! I’ve solved it with method 2 now, but I think it would be a good idea to try the rest.

Thank you so much. Thank you so much for your long answer. I hope you are always happy.

1 Like

Glad it worked! Option 2 is likely the best one as you can change it anytime. The other contender is #3. I wouldn’t bother with the other ones. I mentioned as many options as possible because I’m not familiar with what your entire setup and unsure what you would find easiest.

If you say my last response worked, kindly set it as solution if you can. Thanks.

1 Like