Gameplay Ability System Course Project - Development Blog

Hello again everyone! Welcome to another entry in this on-going blog/development thread for my GAS Course Project. For more context of this project, please see the very first post in this thread, and as usual, you can find my socials here:

Twitch : Twitch
Discord : UE5 - GAS Course Project - JevinsCherries
Tiktok : Devin Sherry (@jevinscherriesgamedev) | TikTok
Youtube : Youtube
Twitter : https://twitter.com/jevincherries
Instagram : Jevins Cherries Gamedev (@jevinscherriesgamedev) • Instagram photos and videos

Today we are going to talk about View Models and how we can use them to update UI elements for both player and NPC characters. I will showcase how I use a single view model to update and replicate the Health and Max Health attributes of separate UI for these characters, and how I used a custom Health Component to achieve this.


Classes to Research:

MVVMViewModelBase

Additional Reading:

https://dev.epicgames.com/documentation/en-us/unreal-engine/umg-viewmodel?application_version=5.1


What is a View Model?

A view model is a means of communication between two separate systems, one of which being your UI. In the context of this GAS Course Project, it is a means of communication between gameplay and UI systems. In most cases, if you wanted to bind gameplay data to your UI, you could so in a few different ways. Here are two examples and reasons as to why they weren’t the best. Then we will discuss how using a View Model is an improvement on this paradigm.


Property Binding

Built into UMG are property bindings that you can override to provide the compatible data into the property. For example, when using a Progress Bar, you can override the property Percent with a function. Below is an example of how we can have the Progress Bar update based on variables on character variables of Current Health and Max Health:

The problem with this method is that these property binding functions are updated On Tick and can be proven to be inefficient for many types of games. You could, for example, store a reference to the Owning Player Pawn on construction of the UI, and use that reference to access these variables. Don’t get me wrong, in some cases, this method is valid and shouldn’t be discouraged, especially on solo projects. However, there are better means to handle updating your UI, such as Event Driven methods.


Event Driven

An improvement can be made to only update the UI when the data relevant to said UI is updated. This can be done through custom events, either in the UI itself or inside of the actor that owns a reference to the UI. Sticking with the same example of a simple Health Bar UI, here are two different ways to handle updating the UI when the respective health variables are updated.

The first method is to create a custom event inside of the UI that the owner can invoke whenever their Health variables are updated:


The second method, using the Gameplay Ability System plugin framework, can listen for attribute changes for Current Health and Max Health using the owning actor as the Target Actor (assuming that the owning actor has an Ability System Component). These tasks will execute whenever the associated attribute is changed, and returns both the Old and New values; and using these we can update the Health Bar UI progress bar.

By doing so, we only update the UI when it needs to be updated. However, there is still a problem; there is a direct connection between the UI and its owning actor. In the second example, if a designer were to want to change how health is updated, they would need to update the widget class directly. Vice versa, if a UI designer needed to change how the health variables are passed into the UI they would need to update the logic inside of the character. Lastly, depending on how your project is setup, different actors may need to display the same data differently; for example, as text rather than just a float value. With this being the case, different classes would need to implement methods to pass their data correctly to the UI in order for it to be displayed as intended. This is where View Models shine.

Let’s dive deeper by creating our Health View Model class.


Creating a View Model

Note: Make sure to follow the setup tutorial in the Additional Reading section in order to setup/enable the plugin for use in your project.

I will provide examples on how to create a View Model, both in C++ and in Blueprints; but for the GAS Course Project, we will be using the C++ class. It is also recommended to not create a monolithic View Model that contains all data you would need for various UI, and to only have View Models that contain related data to each other. An exemption can be a view model that contains all attributes of a character to be used in displaying a stats UI for your game. The example we will be doing is only for a characters’ Health; both player and NPC.

Blueprint:

First, create a new Blueprint that derives from MVVMBaseViewModel:

Next, create whichever variables you would require for your View Model; in this case, CurrentHealth and MaxHealth float variables to represent our players’ Health:

The key with creating variables for your View Model is to enable them as Field Notify by clicking the small bell icon next to the variable name. Field Notifies allows you to map functions that can be updated/respond to the broadcasting changes of these variables.

Now let’s create four functions we need for our UI:

float GetCurrentHealth()
float GetMaxHealth()
float GetHealthPercentage()
FText GetHealthAsDisplay()

Make sure to make these functions both Const and Blueprint Pure functions so that they can be used as a Field Notify, as shown below:

Now that we have functions available as Field Notifies, we can assign them to both our CurrentHealth and MaxHealth variables.

CurrentHealth should have the following Field Notifies:

GetCurrentHealth()
GetHealthAsDisplay()
GetHealthPercentage()

MaxHealth should have the following Field Notifies:

GetMaxHealth()
GetHealthAsDisplay()
GetHealthPercentage()

These functions will be used to bind the data from the View Model into the UI and they will be updated whenever CurrentHealth and MaxHealth are updated. Now I will show you the same kind of setup, but in C++; which we are using for the GAS Course Project:

C++

GASC_UVM_Health.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "MVVMViewModelBase.h"
#include "GASC_UVM_Health.generated.h"

/**
 * 
 */
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;
	
};

GASC_UVM_Health.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Game/HUD/ViewModels/Health/GASC_UVM_Health.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)
{
	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)
	{
		return CurrentHealth / MaxHealth;
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Max Health == 0.0!"));
		return 0.0f;
	}
}

FText UGASC_UVM_Health::GetHealthAsDisplay() const
{
	FString HealthString = FString::SanitizeFloat(CurrentHealth, 0) + "/" + FString::SanitizeFloat(MaxHealth, 0);
	return FText::AsCultureInvariant(HealthString);
}

With our Health View Model created, we can now create the necessary bindings that link the View Model and the UI elements. We will cover both implementations of Health for both our Player and our NPC; the cool thing is that both can use the same View Model and simply update its data differently. We will cover how we instantiate and update our View Models via a custom Health Component later in this blog post.

For the Player Health UI, I use both a Text Block and a Progress Bar to display the health data.

Note: You may need to navigate to Window->ViewBindings and Window->Viewmodels in order to show the required windows to setup the View Model and its bindings within the UMG editor.

First, press the +Viewmodel button to add a our new View Model class. Make sure to select the class you made, whether it was in C++ or in Blueprint:

Select the top-level class to then update the View Model parameters:

View Model Name: This is simply the variable name representation of the View Model if used within the UI explicitly, such as when used with Creation Types Create Instance or Manual. This doesn’t occur when using Global Viewmodel Collection; not sure about Property Path as I have not used this option for my project.

Notify Field Value Class: This is the View Model class being used.

Creation Type: This refers to how we want to initialize the View Model. For our player, we will be using Global View Model, but later, when we setup our NPC Health UI, we will be using Manual.

Create Instance: The widget automatically creates its own instance of the Viewmodel.

Manual: The widget initializes with the Viewmodel as null, and you need to manually create an instance and assign it.

Global Viewmodel Collection: Refers to a globally-available Viewmodel that can be used by any widget in your project. Requires a Global Viewmodel Identifier.

Property Path: At initialization, execute a function to find the Viewmodel. The Viewmodel Property Path uses member names separated by periods. For example: GetPlayerController.Vehicle.ViewModel. Property paths are always relative to the widget.

When using the Global Viewmodel Collection type, you then need to add a Global Viewmodel Identifier; this is an FName that is very important to notate as it is used to connect the instanced view model during the creation process that will happen later on! I use the identifier VM_Player_Health:

Now we can use the Field Notify functions to connect the view model updates to property bindings of our UI elements. Again, we are using both a Progress Bar and a Text Block. Let’s start with the Progress Bar. Under the View Bindings window, press the + Add Widget button to add a new notify. Navigate the window to find the Progress Bar UI element and then select the Percent variable to setup the binding:

Next, we can select the next field to select which function data we should be using to bind into the Progress Bar percent variable. Navigate to find the GetHealthPercentage() function from the view model class:

The final property binding will look like the following:

Lastly, we can then setup the property binding to update the Text Block text variable using the GetHealthAsDisplay() function from our view model. The final setup will look like this:

The Health Bar UI for the NPC works almost identically to the Health UI as the player, however, the Creation Type used is Manual instead of Global View Model Collection. The reason that I found is that each NPC must have its own view model manually created rather than sharing a single Global View Model is that each enemy has its own health. While developing this, I found that when NPC’s use a Global View Model Collection, when dealing damage to one NPC, the health UI would update for all NPCs in the world. By giving each NPC its own manually constructed view model, each will maintain its own Health and UI correctly!




In order to connect the gameplay logic with the Health View Model, let’s create an explicit actor component to instantiate the view model and monitor our attribute changes for CurrentHealth and MaxHealth. Let’s start with the C++ implementation:

GASC_HealthComponent.h

// 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();

	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;
};

GASC_HealthComponent.cpp

// 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()
{
	InitializeViewModel();
	Server_InitializeHealthAttributes();

	Super::BeginPlay();
}

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::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::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::HealthViewModelInstantiated_Implementation(UGASC_UVM_Health* InstantiatedViewModel)
{
}

The most important thing we are doing here is in GASC_HealthComponent::InitializeViewModel() where we construct the view model object and add it to our Global View Model Collection. Here is what this would look like in Blueprints:

Although the NPC character does not use a Global Viewmodel Collection Creation Type, our player does, and so we make sure we register it here. We then use a delegate to pass this Viewmodel back into the health component to use in Blueprints, but also in a separate delegate that the base NPC class will use when constructing its Health Bar:

Remember that the Context Name must be the same name given under the Global Viewmodel Identifier parameter of the Viewmodel inside of the UI! (VM_Player_Health)

We then use replicated variables for CurrentHealth and MaxHealth to replicate and update our View Model by invoking HealthViewModel->SetCurrentHealth and HealthViewModel->SetMaxHealth. Remember that our Field Notifies will ensure that the GetHealthPercentage() and GetHealthAsDisplay() functions are called and our UI property bindings will reflect the correct data!

Let’s add the Health Component to our base character class:

GASCourseCharacter.h

	/**
	 * @brief The CharacterHealthComponent variable represents the component responsible for handling the health functionality of the character.
	 *
	 * This variable is decorated with UPROPERTY to ensure replication and provide read-only access. It is also marked as EditAnywhere and BlueprintReadOnly, allowing it to be edited in the editor and accessed from blueprints. It falls under the StatusEffects category.
	 * The meta flag AllowPrivateAccess is set to true, allowing private access to this component.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = HealthComponent, meta = (AllowPrivateAccess = "true"))
	class UGASC_HealthComponent* CharacterHealthComponent;

GASCourseCharacter.cpp

CharacterHealthComponent = CreateDefaultSubobject<UGASC_HealthComponent>(TEXT(“CharacterHealthComponent”));

We then make sure inside of both our Player and base NPC character Blueprints we override the Character Health Component with the Blueprint class:

Also make sure to update the public variables with the relevant data for the Context Class, Context Name, and Health Attribute Set:

What we need to do now is to create a Blueprint that inherits from this Health Component class so that we can then listen for Attribute Changes and update our View Model!

Note: I tried for so long to make this Health Component entirely C++ but could not for the life of me get things to replicate correctly on all instances, for all characters (both Player and NPC). I had to resort to a hybrid approach of C++ and Blueprints. If anyone can suggest how to make this functional purely in C++, please comment within this thread :slight_smile:

The first thing we do is make a call to the Event Health View Model Instantiated which returns to us the Instantiated view model we create from UGASC_HealthComponent::InitializeViewModel(). We store this reference to a replicated variable called RepHealthViewModel and then use a Reliable, Run On Server, RPC event called Monitor Health Attributes.

We use a Run On Server RPC to listen for attribute changes using the task WaitForAttributeChanged on the server using the GetOwner() of the component as the Target Actor. We do this because the Server is responsible for updating these attributes and replicating them down to the clients.


We not only update the CurrentHealth and MaxHealth replicated variables, but also make calls to both OwningClient and Multicast Reliable events that also update our replicated RepHealthViewModel. We do this depending on whether the owning actor, casted to our GASCourseCharacter, is LocallyControlled or not; in cases for NPCs in our world, we need to NetMultiCast the data so that all clients & server get the data.

Here is the final results of updating our Health Attributes and view models!

ViewModels_Example

We are now able to update different types of UI with the same data through our Health View Model, and any updates or changes required can happen in the view model itself.


References:

Thank you for taking the time to read this post, and I hope you were able to take something away from it. Please let me know if I got any information wrong, or explained something incorrectly! Also add any thoughts or questions or code review feedback so we can all learn together :slight_smile:


Next Blog Post Topic:

*General Combat Targeting

1 Like