I am running into an issue recently (not sure when it started exactly) where my player health view model and UI no longer properly update in development packaged builds.
A few notes:
- In packaged builds, the player health UI correctly initializes with a value of 100.0/100.0 in the display text and a full health bar.
- The health bar, however, does not
- From what I have debugged already, it seems as if the following lines are not called from my view model void
UGASC_UVM_Health::SetCurrentHealth(const float& NewCurrentHealth)
when applying damage:
UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercentage);
UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthAsDisplay);
- These broadcasts, however, seem to work fine on initialization of the health component and the view model; meaning that in packaged builds, the health bar and the health display text show correctly.
- I am using a Global View Model Collection.
Here is my health view model class:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "MVVMViewModelBase.h"
#include "GASC_UVM_Health.generated.h"
/**
* UGASC_UVM_Health is a ViewModel class extending UMVVMViewModelBase, designed to manage the health
* properties of an actor, including current health, maximum health, and health display information.
* It provides methods to get and set health values, calculate the health percentage, and format health
* information as text for display purposes.
*/
UCLASS()
class GASCOURSE_API UGASC_UVM_Health : public UMVVMViewModelBase
{
GENERATED_BODY()
public:
UGASC_UVM_Health();
UFUNCTION(BlueprintPure, FieldNotify)
float GetCurrentHealth() const;
UFUNCTION(BlueprintCallable)
void SetCurrentHealth(const float& NewCurrentHealth);
UFUNCTION(BlueprintPure, FieldNotify)
float GetMaxHealth() const;
UFUNCTION(BlueprintCallable)
void SetMaxHealth(const float& NewMaxHealth);
UFUNCTION(BlueprintPure, FieldNotify)
float GetHealthPercentage() const;
UFUNCTION(BlueprintPure, FieldNotify)
FText GetHealthAsDisplay() const;
private:
UPROPERTY(BlueprintReadOnly, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
float CurrentHealth = 0.0f;
UPROPERTY(BlueprintReadOnly, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
float MaxHealth = 0.0f;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Game/HUD/ViewModels/Health/GASC_UVM_Health.h"
#include "Kismet/KismetMathLibrary.h"
UGASC_UVM_Health::UGASC_UVM_Health()
{
}
float UGASC_UVM_Health::GetCurrentHealth() const
{
return CurrentHealth;
}
float UGASC_UVM_Health::GetMaxHealth() const
{
return MaxHealth;
}
void UGASC_UVM_Health::SetCurrentHealth(const float& NewCurrentHealth)
{
UE_LOG(LogTemp, Warning, TEXT("Current Health: %f | New Current Health: %f"), CurrentHealth, NewCurrentHealth);
if(UE_MVVM_SET_PROPERTY_VALUE(CurrentHealth, NewCurrentHealth))
{
UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercentage);
UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthAsDisplay);
}
}
void UGASC_UVM_Health::SetMaxHealth(const float& NewMaxHealth)
{
if(UE_MVVM_SET_PROPERTY_VALUE(MaxHealth, NewMaxHealth))
{
UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercentage);
UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthAsDisplay);
}
}
float UGASC_UVM_Health::GetHealthPercentage() const
{
if(MaxHealth != 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("Current Health: %f | Max Health: %f"), CurrentHealth, MaxHealth);
return CurrentHealth / MaxHealth;
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Max Health == 0.0!"));
return 0.0f;
}
}
FText UGASC_UVM_Health::GetHealthAsDisplay() const
{
uint32 CurrentHealthAsInt = UKismetMathLibrary::FFloor(CurrentHealth);
FString CurrentHealthString = FString::FromInt(CurrentHealthAsInt);
uint32 MaxHealthAsInt = UKismetMathLibrary::FFloor(MaxHealth);
FString MaxHealthString = FString::FromInt(MaxHealthAsInt);
FString HealthString = CurrentHealthString + "/" + MaxHealthString;
return FText::AsCultureInvariant(HealthString);
}
Here is my health component class:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Components/ActorComponent.h"
#include "MVVMViewModelBase.h"
#include "Game/HUD/ViewModels/Health/GASC_UVM_Health.h"
#include "Game/GameplayAbilitySystem/AttributeSets/GASCourseHealthAttributeSet.h"
#include "GASC_HealthComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthViewModelInstantiated, UGASC_UVM_Health*, HealthViewModel);
/**
* This class represents a health component for an actor in the game.
* It provides functionality for managing the current and maximum health values of the actor.
* The health component also supports replication to ensure consistent gameplay across the network.
*/
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent), Blueprintable )
class GASCOURSE_API UGASC_HealthComponent : public UActorComponent
{
GENERATED_BODY()
public:
/**
* Default constructor for the UGASC_HealthComponent class.
* Sets up the component to be initialized when the game starts and to be ticked every frame.
* It also enables replication for the component.
*/
UGASC_HealthComponent();
protected:
virtual void BeginPlay() override;
/**
* GetLifetimeReplicatedProps is a method of the UGASC_HealthComponent class.
* It is a const method that overrides the GetLifetimeReplicatedProps method of the UActorComponent class.
* This method is responsible for defining the properties that will be replicated over the network.
* It adds the CurrentHealth and MaxHealth properties to the OutLifetimeProps array using the DOREPLIFETIME macro.
* The replicated properties will be automatically synchronized between the server and clients.
*
* @param OutLifetimeProps - The array of lifetime replicated properties to be populated.
*/
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/**
* Initializes the view model for the UGASC_HealthComponent.
*
* The view model is responsible for managing the health-related properties and functions of the UGASC_HealthComponent.
* This method creates a UGASC_UVM_Health instance and adds it to the global view model collection.
* It then sets the HealthViewModel property of the UGASC_HealthComponent to the created instance.
*
* Prerequisites:
* - The UGASC_HealthComponent must be attached to an actor.
* - The owning game instance must have a valid UMVVMGameSubsystem instance.
*
* @see UGASC_HealthComponent
* @see UMVVMGameSubsystem
* @see UMVVMViewModelCollectionObject
* @see UGASC_UVM_Health
* @see FMVVMViewModelContext
* @see CharacterHealthViewModelContextClass
* @see CharacterHealthContextName
* @see HealthViewModel
*/
UFUNCTION()
void InitializeViewModel();
/**
* @brief CurrentHealth is a float variable that represents the current health of the actor.
* It is replicated using the "OnRep_CurrentHealth" function.
*/
UPROPERTY(BlueprintReadWrite, ReplicatedUsing="OnRep_CurrentHealth")
float CurrentHealth = 0.0f;
/**
* @brief The maximum health of the actor.
*
* This variable represents the maximum health value that an actor can have.
* It is used in conjunction with the current health variable to determine the health status of the actor.
* The value of this variable should be set according to the specific requirements of the game or application.
*
* @details
* - The variable is of type `float`.
* - It is marked with the `ReplicatedUsing` attribute, indicating that changes to its value will be replicated to all clients.
* - The default value of the variable is `0.0f`.
*
* @note
* - Modifying the value of this variable directly may have unintended side effects.
* It is recommended to use appropriate functions or methods to update the health value.
* - This variable can be accessed and modified during runtime, as needed.
* - The variable should be synchronized across all instances of the actor in a networked environment to ensure consistent gameplay.
* The `OnRep_MaxHealth` function will be called when the value of this variable is updated on the server.
* Therefore, any logic related to synchronizing the value on clients should be implemented in that function.
*
* @see OnRep_MaxHealth
*
* @warning This variable should not be initialized or modified directly in external code.
* Use appropriate functions or methods within the associated class to manage the health value.
*/
UPROPERTY(BlueprintReadWrite, ReplicatedUsing="OnRep_MaxHealth")
float MaxHealth = 0.0f;
/**
* Callback function invoked when the replicated property CurrentHealth is replicated to clients.
* This function is automatically called by the engine when the replicated property changes.
* It updates the current health in the health view model, if available.
*
* Usage:
* - Override this function in subclasses of UGASC_HealthComponent to provide custom logic when the current health changes.
* - Within the implementation of the overridden function, call the base implementation first, and then add any additional code specific to the subclass.
* - Example usage:
* ```cpp
* void UMyHealthComponent::OnRep_CurrentHealth()
* {
* Super::OnRep_CurrentHealth();
*
* // Custom logic here
* }
* ```
* - Note that the base implementation of this function already sets the current health in the health view model if available, so you may not need to modify this behavior in most cases.
*/
UFUNCTION(BlueprintCallable)
virtual void OnRep_CurrentHealth();
/**
* Called when the MaxHealth property is replicated from the server to the clients.
* This method is automatically called by the Unreal Engine's replication system.
* It updates the Max Health value in the HealthViewModel if it exists.
*
* @remarks
* This method assumes that the HealthViewModel has been properly initialized and assigned.
* If the HealthViewModel is null, no action will be taken.
*
* @see UGASC_HealthViewModel
*/
UFUNCTION(BlueprintCallable)
virtual void OnRep_MaxHealth();
/**
* Initializes the health attributes for the server.
* This method is called on the server when the health component is being initialized.
* It retrieves the health attribute values from the owning character's ability system component and sets the local CurrentHealth and MaxHealth variables accordingly.
* It also triggers the OnRep_CurrentHealth and OnRep_MaxHealth methods to ensure proper replication.
*
* @note This method should be called on the server only.
*/
UFUNCTION(Reliable, Server)
void Server_InitializeHealthAttributes();
UFUNCTION(Reliable, Client)
void Client_InitializeHealthAttributes();
UPROPERTY(BlueprintAssignable, BlueprintCallable)
FOnHealthViewModelInstantiated OnHealthViewModelInstantiated;
UFUNCTION(BlueprintNativeEvent)
void HealthViewModelInstantiated(UGASC_UVM_Health* InstantiatedViewModel);
public:
/**
* CharacterHealthViewModelContextClass is a property that holds the subclass of UMVVMViewModelBase
* used to represent the character's health in the health view model.
*
* This property is editable in the editor and can be accessed by blueprint scripts.
*
* @see UGASC_HealthComponent::RegisterHealthComponent
*
* @category View Models
* @subcategory Health
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Health Component|View Model")
TSubclassOf<UMVVMViewModelBase> CharacterHealthViewModelContextClass;
/**
* CharacterHealthContextName is a variable that holds the name of the context used to represent the character's health in the health view model.
*
* This variable is editable in the editor and can be accessed by blueprint scripts.
*
* @see UGASC_HealthComponent::RegisterToHealthViewModel_Client
* @see UGASC_HealthComponent::RegisterToHealthViewModel
*
* @category View Models
* @subcategory Health
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Health Component|View Model")
FName CharacterHealthContextName;
/**
* The HealthAttributeSet variable is a subclass of UGASCourseHealthAttributeSet.
* This variable is exposed to the editor and can be accessed by blueprint scripts.
* It represents the attribute set for the health component.
*
* @see UGASC_HealthComponent
* @see UGASC_HealthComponent::OnHealthViewModelRegistered_Implementation
* @see UGASC_HealthComponent::MonitorHealthChanges_Server
*
* @category Attribute
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Health Component|Gameplay AttributeSet")
TSubclassOf<UGASCourseHealthAttributeSet> HealthAttributeSet;
/**
* @brief The HealthViewModel variable represents the health view model for an actor.
*
* This variable is of type UGASC_UVM_Health and is marked as BlueprintReadOnly, which means it can only be read from Blueprint.
*
* The HealthViewModel is responsible for managing the current and maximum health values of the actor.
* It provides functionality for updating and retrieving the health values.
*
* Example usage:
* @code
* if (HealthViewModel != nullptr) {
* float currentHealth = HealthViewModel->GetCurrentHealth();
* float maxHealth = HealthViewModel->GetMaxHealth();
* // Perform necessary operations with currentHealth and maxHealth
* }
* @endcode
*
* @see UGASC_UVM_Health
*/
UPROPERTY(BlueprintReadOnly)
UGASC_UVM_Health* HealthViewModel;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Game/Character/Components/Health/GASC_HealthComponent.h"
#include "MVVMGameSubsystem.h"
#include "MVVMSubsystem.h"
#include "Abilities/Tasks/AbilityTask_WaitAttributeChange.h"
#include "Game/GameplayAbilitySystem/AttributeSets/GASCourseHealthAttributeSet.h"
#include "Game/HUD/ViewModels/Health/GASC_UVM_Health.h"
#include "GASCourse/GASCourseCharacter.h"
#include "Net/UnrealNetwork.h"
// Sets default values for this component's properties
UGASC_HealthComponent::UGASC_HealthComponent()
{
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
// off to improve performance if you don't need them.
PrimaryComponentTick.bCanEverTick = false;
SetIsReplicatedByDefault(true);
}
void UGASC_HealthComponent::BeginPlay()
{
Super::BeginPlay();
InitializeViewModel();
Server_InitializeHealthAttributes();
Client_InitializeHealthAttributes();
}
void UGASC_HealthComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UGASC_HealthComponent, CurrentHealth);
DOREPLIFETIME(UGASC_HealthComponent, MaxHealth);
}
void UGASC_HealthComponent::OnRep_CurrentHealth()
{
if(HealthViewModel)
{
HealthViewModel->SetCurrentHealth(CurrentHealth);
}
}
void UGASC_HealthComponent::OnRep_MaxHealth()
{
if(HealthViewModel)
{
HealthViewModel->SetMaxHealth(MaxHealth);
}
}
void UGASC_HealthComponent::Server_InitializeHealthAttributes_Implementation()
{
if(AGASCourseCharacter* OwningCharacter = Cast<AGASCourseCharacter>(GetOwner()))
{
if(UAbilitySystemComponent* OwningASC = Cast<UAbilitySystemComponent>(OwningCharacter->GetAbilitySystemComponent()))
{
if(const UGASCourseHealthAttributeSet* HealthAttributes = Cast<UGASCourseHealthAttributeSet>(OwningASC->GetAttributeSet(HealthAttributeSet)))
{
CurrentHealth = HealthAttributes->GetCurrentHealth();
MaxHealth = HealthAttributes->GetMaxHealth();
OnRep_CurrentHealth();
OnRep_MaxHealth();
}
}
}
}
void UGASC_HealthComponent::Client_InitializeHealthAttributes_Implementation()
{
if(AGASCourseCharacter* OwningCharacter = Cast<AGASCourseCharacter>(GetOwner()))
{
if(UAbilitySystemComponent* OwningASC = Cast<UAbilitySystemComponent>(OwningCharacter->GetAbilitySystemComponent()))
{
if(const UGASCourseHealthAttributeSet* HealthAttributes = Cast<UGASCourseHealthAttributeSet>(OwningASC->GetAttributeSet(HealthAttributeSet)))
{
CurrentHealth = HealthAttributes->GetCurrentHealth();
MaxHealth = HealthAttributes->GetMaxHealth();
OnRep_CurrentHealth();
OnRep_MaxHealth();
}
}
}
}
void UGASC_HealthComponent::InitializeViewModel()
{
UMVVMGameSubsystem* ViewModelGameSubsystem = GetOwner()->GetGameInstance()->GetSubsystem<UMVVMGameSubsystem>();
check(ViewModelGameSubsystem);
UMVVMViewModelCollectionObject* GlobalViewModelCollection = ViewModelGameSubsystem->GetViewModelCollection();
check(GlobalViewModelCollection);
UGASC_UVM_Health* CharacterHealthViewModel = NewObject<UGASC_UVM_Health>();
FMVVMViewModelContext CharacterHealthViewModelContext;
CharacterHealthViewModelContext.ContextClass = CharacterHealthViewModelContextClass;
CharacterHealthViewModelContext.ContextName = CharacterHealthContextName;
if(CharacterHealthViewModelContext.IsValid())
{
GlobalViewModelCollection->AddViewModelInstance(CharacterHealthViewModelContext, CharacterHealthViewModel);
HealthViewModel = CharacterHealthViewModel;
OnHealthViewModelInstantiated.Broadcast(HealthViewModel);
HealthViewModelInstantiated(HealthViewModel);
}
}
void UGASC_HealthComponent::HealthViewModelInstantiated_Implementation(UGASC_UVM_Health* InstantiatedViewModel)
{
if (GetOwner()->HasAuthority())
{
Server_InitializeHealthAttributes();
}
Client_InitializeHealthAttributes();
}
Video Examples:
In-Editor/PIE
Development Packaged Build
Here is what the widget view model binding looks like:
Here is what the health component does in Blueprints; listening to the health attribute changes using GAS:
Here is what the debug input looks like that applies damage:
Any help would be appreciated; I am truly stuck on this problem Thanks in advance!