Section 5: Stats
In this section, we learned how to add a stats system to our characters for storing health, strength and stamina.
Section Intro - Stats
There are no notes for this lecture.
Creating Maps
In this lecture, we learned how to create a map for storing a collection of data. Maps are similar to arrays except they allow you to customize the index. The generic requires two types, which is the data type for the index and the type for the value.
UPROPERTY(EditAnywhere)
TMap<FString, float> Stats;To use a map, we can access an item just like we would any other value from an array. The main exception is that the index must match the type used in the first type for the generic. Let's say we had a value stored with the index of "Health". We could access it like so:
UE_LOG(LogTemp, Warning, TEXT("Health: %f"), Stats["Health"]);Defining Enums
In this lecture, we learned how to define an enum for Unreal. To do so, we must mark our enums with the UENUM(BlueprintType) macro.
UENUM(BlueprintType)
enum EStat
{
None UMETA(DisplyName = "None Selected"),
Health UMETA(DisplayName = "Health"),
MaxHealth UMETA(DisplayName = "Max Health"),
Strength UMETA(DisplayName = "Strength"),
Stamina UMETA(DisplayName = "Stamina"),
MaxStamina UMETA(DisplayName = "Max Stamina")
};If you plan on allowing the enum to be used within the Unreal editor, you can add the UMETA() macro with the DisplayName setting to display a human-readable name.
If you want to use an enum in a map, you must use the TEnumAsBye type since enums are really just integers.
TMap<TEnumAsByte<EStat>, float> Stats;If you want to access a stat, you can do the following:
Stats[EStat::Health];Instantiating Components
In this lecture, we decided to refactor our codebase by instantiating our components with C++ through the AMainCharacter class. First, we have to define variables for storing pointers to the components.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
class UStatsComponent* StatsComp;In this example, we're forward declaring the types and adding the UPROPERTY macro. To be able to edit the component's settings, we must add the EditAnywhere specifier. To be able to use the component from our blueprint, we must add the BlueprintReadWrite.
Next, we can instantiate the component from the class's constructor function. This step is important because we want the component to be accessible from our editor. So, if we want a value to be available during edit and gameplay mode, it's best to set the variable from the constructor function.
AMainCharacter::AMainCharacter()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
StatsComp = CreateDefaultSubobject<UStatsComponent>(TEXT("StatsComponent"));
}To create a component, we can use the CreateDefaultSubobject() function. This function accepts the component to create as a generic. A custom name can be given as an argument to the function with the TEXT() macro.
Reducing Character Health
In this lecture, we updated the boss to reduce their health when taking damage from the player. To do this, we defined a function in the stats component called ReduceHealth() that looks something like this:
void UStatsComponent::ReduceHealth(float Amount)
{
if (Stats[EStat::Health] <= 0) { return; }
Stats[EStat::Health] -= Amount;
Stats[EStat::Health] = UKismetMathLibrary::FClamp(
Stats[EStat::Health],
0,
Stats[EStat::MaxHealth]
);
}There are three things we're doing.
Checking if the character's health is already at or below 0. If so, we prevent the rest of the function from running.
Reduce the character's health stat by the incoming amount.
Clamp the health to prevent it from going below 0 or above the maximum health.
The FClamp() function has three arguments, which are the value to clamp, the minimum threshold, and the maximum threshold.
Reducing Stamina
In this lecture, we took the time to define an event from our combat component and handle that event from the stats component. Communicating through events is great because both components don't need to know about each other from their code directly. For the combat component, we added the event with the following:
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(
FOnAttackPerformedSignature,
UCombatComponent, OnAttackPerformedDelegate,
float, Amount
);Then, in our stats component, we had a function to reduce stamina:
void UStatsComponent::ReduceStamina(float Amount)
{
Stats[EStat::Stamina] -= Amount;
Stats[EStat::Stamina] = UKismetMathLibrary::FClamp(
Stats[EStat::Stamina],
0,
Stats[EStat::MaxStamina]
);
}To connect the two, the third-person blueprint acts as a middleman.

Checking for Stamina
In this lecture, we added a way to check for stamina to prevent the player from endlessly spamming attacks. To do so, we added a function to our interface called HasEnoughStamina(). It'll check if the cost passed in exceeds the current stamina.
virtual bool HasEnoughStamina(float Cost) { return true; }We override this function on our character's class like so:
bool AMainCharacter::HasEnoughStamina(float Cost)
{
return StatsComp->Stats[EStat::Stamina] >= Cost;
}
Lastly, we used this function from out combat component. First, we check if the owner implemented this interface. If they did, we casted the character to the interface and then called the function with the stamina cost for an attack.
if (CharacterRef->Implements<UMainPlayer>())
{
IMainPlayer* IPlayerRef{ Cast<IMainPlayer>(CharacterRef) };
if (IPlayerRef && !IPlayerRef->HasEnoughStamina(StaminaCost)) { return; }
}In the end, if there's not enough stamina, the function is returned to prevent the player from attacking.
Adding Sprinting
In this lecture, we worked on adding a sprinting feature to our game. To accomplish this, we outsourced the logic to the UPlayerActionsComponent class. In this class, we grabbed the character movement component and then updated it MaxWalkSpeed like so:
MovementComp->MaxWalkSpeed = SprintSpeed;Other than that, we didn't do anything particularly new.
Draining Stamina
In this lecture, we defined an event for when the player sprints so that we can drain the stamina.
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(
FOnSprintSignature,
UPlayerActionsComponent, OnSprintDelegate,
float, Cost
);During this process, we discovered that the player can drain their stamina while standing still and can still sprint even when the stamina was drained. To fix these issues, we compared their velocity with a vector zero using the Equals() function available on all vectors.
if (MovementComp->Velocity.Equals(FVector::ZeroVector, 1)) { return; }This function accepts the vector to compare to and a tolerance. The tolerance will make sure that the two vectors are close enough to return true. Attempting to compare two vectors exactly may not always yield the best results since accuracy is hard to pull off.
As for the second problem, we simply called the Walk() function when the player does not have enough stamina.
if (!IPlayerRef->HasEnoughStamina(SprintCost))
{
Walk();
return;
}Stamina Regeneration
In this lecture, we used the UKismetMathLibrary::FInterpTo_Constant function to regenerate stamina. This function is designed to take one value and slowly go up to another value at a constant rate.
Stats[EStat::Stamina] = UKismetMathLibrary::FInterpTo_Constant(
Stats[EStat::Stamina],
Stats[EStat::MaxStamina],
GetWorld()->DeltaTimeSeconds,
StaminaRegenRate
);It has four arguments:
The current value.
The target value.
The delta time.
The rate to go from the current value to the target value.
We connected this function to the Tick event in our blueprint. So, it gets called on every frame to update the stamina.
Resources
FInterpTo_Constant() Function - https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Kismet/UKismetMathLibrary/FInterpTo_Constant
Adding a Regeneration Delay
In this lecture, we learned how to delay stamina regeneration with the UKismetSystemLibrary::RetriggerableDelay function. This function has three arguments, which is the world context, duration, and information on the function. For the duration, we outsourced it to a variable on class that can be modified through the Unreal reditor.
FLatentActionInfo FunctionInfo {
0,
100,
TEXT("EnableRegen"),
this
};
UKismetSystemLibrary::RetriggerableDelay(
GetWorld(),
StaminaDelayTime,
FunctionInfo
);For the third argument, we're using the FLatentActionInfo type, which allows us to provide info on the function to call. There are four arguments.
The linkage tells it where to resume the function.
A unique ID for the function.
The function name to call.
The class where the function can be found.
When defining the function to call, make sure it has the UFUNCTION() macro. Otherwise, the timer may not call it.
UFUNCTION()
void EnableRegen();Resource
Last updated