Integriere Gameplay Ability System (GAS)

Ersetze HealthComponent und StaminaComponent durch GAS-basierte AttributeSets (`UHealthAttributeSet`, `UStaminaAttributeSet`) und füge `UAbilitySystemComponent` hinzu.

Implementiere neue GameplayEffects (`UGE_HealthDamage`, `UGE_HealthHealing`, `UGE_HealthDebuff`, `UGE_StaminaFatiguePenalty`) und eine Modifikator-Berechnung (`UMMC_MaxStaminaFatiguePenalty`).

Erweitere `AKingshearthLegacyCharacter` um Methoden zur Attributmanipulation (`ApplyDamage`, `SpendStamina`, etc.) und Stamina-Regeneration.

Aktualisiere UI-Widgets (`HealthBarWidget`, `StaminaBarWidget`) zur Nutzung der neuen AttributeSets. Entferne Legacy-Code für Health- und Stamina-Komponenten.

Füge neue Assets (Niagara-Systeme, Materialien, Texturen) hinzu und passe Projektkonfiguration an (z. B. `GameplayAbilities`-Modul, `r.Substrate`).
This commit is contained in:
2026-09-04 22:54:46 +02:00
parent 495424a2b2
commit 64f4a59b0b
79 changed files with 877 additions and 431 deletions
@@ -0,0 +1,96 @@
#include "HealthAttributeSet.h"
#include "GameplayEffectExtension.h"
UHealthAttributeSet::UHealthAttributeSet()
{
InitHealth(100.0f);
InitMaxHealth(100.0f);
}
void UHealthAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
}
else if (Attribute == GetMaxHealthAttribute())
{
NewValue = FMath::Max(NewValue, 0.0f);
}
}
void UHealthAttributeSet::PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const
{
Super::PreAttributeBaseChange(Attribute, NewValue);
// Instant GameplayEffects (and any future ApplyModToAttribute caller) write BaseValue through this hook,
// not PreAttributeChange - without clamping here a large delta can push BaseValue out of range while
// PreAttributeChange still clamps the displayed CurrentValue, silently absorbing later deltas. See the
// matching comment on UStaminaAttributeSet::PreAttributeBaseChange for how this actually manifested.
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
}
else if (Attribute == GetMaxHealthAttribute())
{
NewValue = FMath::Max(NewValue, 0.0f);
}
}
void UHealthAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// Only converts the Damage/Healing meta-attributes into a Health change - this hook fires for GE
// "executes" (Instant effects, periodic ticks) only. GE_HealthDebuff is Infinite and non-periodic,
// so applying/updating it does NOT fire this - see PostAttributeChange for the hook that does.
const FGameplayAttribute& ModifiedAttribute = Data.EvaluatedData.Attribute;
if (ModifiedAttribute == GetDamageAttribute())
{
const float DamageDone = GetDamage();
SetDamage(0.0f);
SetHealth(GetHealth() - DamageDone);
}
else if (ModifiedAttribute == GetHealingAttribute())
{
const float HealingDone = GetHealing();
SetHealing(0.0f);
SetHealth(GetHealth() + HealingDone);
}
}
void UHealthAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
Super::PostAttributeChange(Attribute, OldValue, NewValue);
// Fires for every CurrentValue change regardless of source (GE execute, or - critically - an Infinite
// GE like GE_HealthDebuff being applied/removed/re-evaluated, which never triggers PostGameplayEffectExecute).
if (Attribute == GetHealthAttribute())
{
if (!FMath::IsNearlyEqual(OldValue, NewValue))
{
OnHealthChanged.Broadcast(NewValue);
}
if (OldValue > 0.0f && NewValue <= 0.0f)
{
OnDeath.Broadcast();
}
}
else if (Attribute == GetMaxHealthAttribute())
{
if (!FMath::IsNearlyEqual(OldValue, NewValue))
{
ClampHealth();
OnHealthDebuffChanged.Broadcast(GetHealthDebuff());
}
}
}
void UHealthAttributeSet::ClampHealth()
{
SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));
}
@@ -0,0 +1,79 @@
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "HealthAttributeSet.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthDebuffChanged, float, NewHealthDebuff);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
#define HEALTHATTRIBUTE_ACCESSORS(PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(UHealthAttributeSet, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
/**
* Health + MaxHealth, replacing UHealthComponent. HealthDebuff no longer exists as its own
* attribute - it's GE_HealthDebuff applying a Multiply modifier to MaxHealth, so the "locked"
* fraction is just (1 - MaxHealth.CurrentValue / MaxHealth.BaseValue). See GetHealthDebuff()
* on AKingshearthLegacyCharacter.
*/
UCLASS()
class KINGSHEARTHLEGACY_API UHealthAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UHealthAttributeSet();
UPROPERTY(BlueprintReadOnly, Category = "Health")
FGameplayAttributeData Health;
HEALTHATTRIBUTE_ACCESSORS(Health)
/** Nominal cap: BaseValue is the undebuffed max, CurrentValue is what GE_HealthDebuff (and similar) leave after their modifiers. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Health")
FGameplayAttributeData MaxHealth;
HEALTHATTRIBUTE_ACCESSORS(MaxHealth)
/** Unclamped meta attribute: incoming damage lands here via GE_HealthDamage; PostGameplayEffectExecute converts it into a Health decrease and resets it to 0. Never read directly. */
UPROPERTY(BlueprintReadOnly, Category = "Health")
FGameplayAttributeData Damage;
HEALTHATTRIBUTE_ACCESSORS(Damage)
/** Unclamped meta attribute: incoming healing lands here via GE_HealthHealing; PostGameplayEffectExecute converts it into a Health increase and resets it to 0. Never read directly. */
UPROPERTY(BlueprintReadOnly, Category = "Health")
FGameplayAttributeData Healing;
HEALTHATTRIBUTE_ACCESSORS(Healing)
/** Nominal cap, unaffected by debuffs. Feeds the health bar's total width. */
float GetNominalMaxHealth() const { return MaxHealth.GetBaseValue(); }
/** 0-1 fraction of GetNominalMaxHealth() currently locked/unusable (curses, status effects, ...). */
float GetHealthDebuff() const
{
const float NominalMaxHealth = FMath::Max(GetNominalMaxHealth(), KINDA_SMALL_NUMBER);
return FMath::Clamp(1.0f - (GetMaxHealth() / NominalMaxHealth), 0.0f, 1.0f);
}
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnHealthChanged OnHealthChanged;
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnHealthDebuffChanged OnHealthDebuffChanged;
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnDeath OnDeath;
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const override;
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
private:
void ClampHealth();
};
#undef HEALTHATTRIBUTE_ACCESSORS
@@ -0,0 +1,86 @@
#include "StaminaAttributeSet.h"
UStaminaAttributeSet::UStaminaAttributeSet()
{
InitStamina(100.0f);
InitMaxStamina(100.0f);
InitFatigue(0.0f);
}
void UStaminaAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
if (Attribute == GetStaminaAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxStamina());
}
else if (Attribute == GetMaxStaminaAttribute())
{
NewValue = FMath::Max(NewValue, 0.0f);
}
else if (Attribute == GetFatigueAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, 1.0f);
}
}
void UStaminaAttributeSet::PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const
{
Super::PreAttributeBaseChange(Attribute, NewValue);
// ApplyModToAttribute (SpendStamina/RestoreStamina/regen) writes BaseValue through this hook, not
// PreAttributeChange - without clamping here, a large delta (e.g. RestoreStamina's debug "fill to max")
// pushes BaseValue past MaxStamina. PreAttributeChange still clamps the displayed CurrentValue, but the
// bloated BaseValue then absorbs every subsequent spend/regen delta before CurrentValue moves again.
if (Attribute == GetStaminaAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxStamina());
}
else if (Attribute == GetMaxStaminaAttribute())
{
NewValue = FMath::Max(NewValue, 0.0f);
}
else if (Attribute == GetFatigueAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, 1.0f);
}
}
void UStaminaAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
Super::PostAttributeChange(Attribute, OldValue, NewValue);
if (Attribute == GetStaminaAttribute())
{
if (!FMath::IsNearlyEqual(OldValue, NewValue))
{
OnStaminaChanged.Broadcast(NewValue);
}
if (OldValue > 0.0f && NewValue <= 0.0f)
{
OnExhausted.Broadcast();
}
}
else if (Attribute == GetMaxStaminaAttribute())
{
if (!FMath::IsNearlyEqual(OldValue, NewValue))
{
// MaxStamina shrank/grew (typically GE_StaminaFatiguePenalty re-evaluating) - pull Stamina back in range.
ClampStamina();
}
}
else if (Attribute == GetFatigueAttribute())
{
if (!FMath::IsNearlyEqual(OldValue, NewValue))
{
OnFatigueChanged.Broadcast(NewValue);
}
}
}
void UStaminaAttributeSet::ClampStamina()
{
SetStamina(FMath::Clamp(GetStamina(), 0.0f, GetMaxStamina()));
}
@@ -0,0 +1,74 @@
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "StaminaAttributeSet.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStaminaChanged, float, NewStamina);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFatigueChanged, float, NewFatigue);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnExhausted);
#define STAMINAATTRIBUTE_ACCESSORS(PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(UStaminaAttributeSet, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
/**
* Stamina + MaxStamina + Fatigue, replacing UStaminaComponent. Unlike Health, all three attributes
* are mutated directly (via AbilitySystemComponent::ApplyModToAttribute from the character's tick,
* see SpendStamina/TickStaminaRegen) rather than through discrete GameplayEffects - there's no
* damage-type/resistance concept for stamina spend to warrant Health's meta-attribute indirection.
* The one thing that IS a real GameplayEffect is GE_StaminaFatiguePenalty: an always-on Infinite
* effect (applied once in BeginPlay) that uses MMC_MaxStaminaFatiguePenalty to keep MaxStamina's
* CurrentValue equal to BaseValue * (1 - Fatigue), recalculated automatically by GAS's own
* aggregator whenever Fatigue changes, replacing the old hand-written GetEffectiveMaxStamina().
*
* Broadcasts live in PostAttributeChange (fires for ANY mutation path, not just GameplayEffect
* execution) specifically so the Fatigue-driven MaxStamina recalculation broadcasts correctly too.
*/
UCLASS()
class KINGSHEARTHLEGACY_API UStaminaAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UStaminaAttributeSet();
UPROPERTY(BlueprintReadOnly, Category = "Stamina")
FGameplayAttributeData Stamina;
STAMINAATTRIBUTE_ACCESSORS(Stamina)
/** Nominal cap: BaseValue is the undebuffed max, CurrentValue is what GE_StaminaFatiguePenalty leaves after Fatigue. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
FGameplayAttributeData MaxStamina;
STAMINAATTRIBUTE_ACCESSORS(MaxStamina)
/** 0-1 fraction of MaxStamina currently locked away due to over-extension. */
UPROPERTY(BlueprintReadOnly, Category = "Fatigue")
FGameplayAttributeData Fatigue;
STAMINAATTRIBUTE_ACCESSORS(Fatigue)
/** Nominal cap, unaffected by Fatigue. Feeds the stamina bar's total width. */
float GetNominalMaxStamina() const { return MaxStamina.GetBaseValue(); }
UPROPERTY(BlueprintAssignable, Category = "Stamina")
FOnStaminaChanged OnStaminaChanged;
UPROPERTY(BlueprintAssignable, Category = "Fatigue")
FOnFatigueChanged OnFatigueChanged;
/** Broadcast when Stamina is driven from a positive value to empty. */
UPROPERTY(BlueprintAssignable, Category = "Stamina")
FOnExhausted OnExhausted;
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const override;
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;
private:
void ClampStamina();
};
#undef STAMINAATTRIBUTE_ACCESSORS
@@ -0,0 +1,18 @@
#include "GE_HealthDamage.h"
#include "HealthAttributeSet.h"
#include "KingshearthLegacyGameplayTags.h"
UGE_HealthDamage::UGE_HealthDamage()
{
DurationPolicy = EGameplayEffectDurationType::Instant;
FSetByCallerFloat SetByCaller;
SetByCaller.DataTag = TAG_Data_Health_Damage.GetTag();
FGameplayModifierInfo Modifier;
Modifier.Attribute = UHealthAttributeSet::GetDamageAttribute();
Modifier.ModifierOp = EGameplayModOp::Additive;
Modifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller);
Modifiers.Add(Modifier);
}
@@ -0,0 +1,15 @@
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffect.h"
#include "GE_HealthDamage.generated.h"
/** Instant effect: adds a caller-specified amount (SetByCaller TAG_Data_Health_Damage) to HealthAttributeSet::Damage. */
UCLASS()
class KINGSHEARTHLEGACY_API UGE_HealthDamage : public UGameplayEffect
{
GENERATED_BODY()
public:
UGE_HealthDamage();
};
@@ -0,0 +1,18 @@
#include "GE_HealthDebuff.h"
#include "HealthAttributeSet.h"
#include "KingshearthLegacyGameplayTags.h"
UGE_HealthDebuff::UGE_HealthDebuff()
{
DurationPolicy = EGameplayEffectDurationType::Infinite;
FSetByCallerFloat SetByCaller;
SetByCaller.DataTag = TAG_Data_HealthDebuff_Multiplier.GetTag();
FGameplayModifierInfo Modifier;
Modifier.Attribute = UHealthAttributeSet::GetMaxHealthAttribute();
Modifier.ModifierOp = EGameplayModOp::MultiplyAdditive;
Modifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller);
Modifiers.Add(Modifier);
}
@@ -0,0 +1,20 @@
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffect.h"
#include "GE_HealthDebuff.generated.h"
/**
* Infinite effect: multiplies MaxHealth's CurrentValue by a caller-specified fraction
* (SetByCaller TAG_Data_HealthDebuff_Multiplier, e.g. 0.7 to lock away 30%). Stands in for
* a future curse/status-effect system - AKingshearthLegacyCharacter::SetHealthDebuff applies/
* removes one instance of this, tracked by its FActiveGameplayEffectHandle.
*/
UCLASS()
class KINGSHEARTHLEGACY_API UGE_HealthDebuff : public UGameplayEffect
{
GENERATED_BODY()
public:
UGE_HealthDebuff();
};
@@ -0,0 +1,18 @@
#include "GE_HealthHealing.h"
#include "HealthAttributeSet.h"
#include "KingshearthLegacyGameplayTags.h"
UGE_HealthHealing::UGE_HealthHealing()
{
DurationPolicy = EGameplayEffectDurationType::Instant;
FSetByCallerFloat SetByCaller;
SetByCaller.DataTag = TAG_Data_Health_Healing.GetTag();
FGameplayModifierInfo Modifier;
Modifier.Attribute = UHealthAttributeSet::GetHealingAttribute();
Modifier.ModifierOp = EGameplayModOp::Additive;
Modifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller);
Modifiers.Add(Modifier);
}
@@ -0,0 +1,15 @@
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffect.h"
#include "GE_HealthHealing.generated.h"
/** Instant effect: adds a caller-specified amount (SetByCaller TAG_Data_Health_Healing) to HealthAttributeSet::Healing. */
UCLASS()
class KINGSHEARTHLEGACY_API UGE_HealthHealing : public UGameplayEffect
{
GENERATED_BODY()
public:
UGE_HealthHealing();
};
@@ -0,0 +1,18 @@
#include "GE_StaminaFatiguePenalty.h"
#include "StaminaAttributeSet.h"
#include "MMC_MaxStaminaFatiguePenalty.h"
UGE_StaminaFatiguePenalty::UGE_StaminaFatiguePenalty()
{
DurationPolicy = EGameplayEffectDurationType::Infinite;
FCustomCalculationBasedFloat CustomCalculation;
CustomCalculation.CalculationClassMagnitude = UMMC_MaxStaminaFatiguePenalty::StaticClass();
FGameplayModifierInfo Modifier;
Modifier.Attribute = UStaminaAttributeSet::GetMaxStaminaAttribute();
Modifier.ModifierOp = EGameplayModOp::MultiplyAdditive;
Modifier.ModifierMagnitude = FGameplayEffectModifierMagnitude(CustomCalculation);
Modifiers.Add(Modifier);
}
@@ -0,0 +1,19 @@
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffect.h"
#include "GE_StaminaFatiguePenalty.generated.h"
/**
* Always-on Infinite effect applied once in AKingshearthLegacyCharacter::BeginPlay: multiplies
* MaxStamina's CurrentValue by MMC_MaxStaminaFatiguePenalty's (1 - Fatigue), so MaxStamina tracks
* Fatigue automatically via GAS's aggregator - replaces the old hand-written GetEffectiveMaxStamina().
*/
UCLASS()
class KINGSHEARTHLEGACY_API UGE_StaminaFatiguePenalty : public UGameplayEffect
{
GENERATED_BODY()
public:
UGE_StaminaFatiguePenalty();
};
@@ -0,0 +1,20 @@
#include "MMC_MaxStaminaFatiguePenalty.h"
#include "StaminaAttributeSet.h"
UMMC_MaxStaminaFatiguePenalty::UMMC_MaxStaminaFatiguePenalty()
{
FatigueCaptureDef = FGameplayEffectAttributeCaptureDefinition(
UStaminaAttributeSet::GetFatigueAttribute(),
EGameplayEffectAttributeCaptureSource::Target,
false);
RelevantAttributesToCapture.Add(FatigueCaptureDef);
}
float UMMC_MaxStaminaFatiguePenalty::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
float Fatigue = 0.0f;
GetCapturedAttributeMagnitude(FatigueCaptureDef, Spec, FAggregatorEvaluateParameters(), Fatigue);
return 1.0f - FMath::Clamp(Fatigue, 0.0f, 1.0f);
}
@@ -0,0 +1,20 @@
#pragma once
#include "CoreMinimal.h"
#include "GameplayModMagnitudeCalculation.h"
#include "MMC_MaxStaminaFatiguePenalty.generated.h"
/** Captures Fatigue (live, non-snapshot) and returns (1 - Fatigue) as the Multiply(Additive) magnitude for GE_StaminaFatiguePenalty's MaxStamina modifier. */
UCLASS()
class KINGSHEARTHLEGACY_API UMMC_MaxStaminaFatiguePenalty : public UGameplayModMagnitudeCalculation
{
GENERATED_BODY()
public:
UMMC_MaxStaminaFatiguePenalty();
virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
private:
FGameplayEffectAttributeCaptureDefinition FatigueCaptureDef;
};
@@ -1,89 +0,0 @@
#include "HealthComponent.h"
UHealthComponent::UHealthComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UHealthComponent::BeginPlay()
{
Super::BeginPlay();
CurrentHealth = GetEffectiveMaxHealth();
}
float UHealthComponent::GetHealth() const
{
return CurrentHealth;
}
float UHealthComponent::GetMaxHealth() const
{
return MaxHealth;
}
float UHealthComponent::GetHealthDebuff() const
{
return HealthDebuff;
}
float UHealthComponent::GetEffectiveMaxHealth() const
{
return MaxHealth * (1.0f - HealthDebuff);
}
void UHealthComponent::SetMaxHealth(float NewMaxHealth)
{
MaxHealth = FMath::Max(NewMaxHealth, 0.0f);
CurrentHealth = FMath::Clamp(CurrentHealth, 0.0f, GetEffectiveMaxHealth());
}
void UHealthComponent::SetHealth(float NewHealth)
{
const float PreviousHealth = CurrentHealth;
CurrentHealth = FMath::Clamp(NewHealth, 0.0f, GetEffectiveMaxHealth());
OnHealthChanged.Broadcast(CurrentHealth);
// Only trigger death when crossing from alive to dead.
if (PreviousHealth > 0.0f && CurrentHealth <= 0.0f)
{
OnDeath.Broadcast();
}
}
void UHealthComponent::AddHealth(float Amount)
{
if (Amount <= 0.0f)
{
return;
}
SetHealth(CurrentHealth + Amount);
}
void UHealthComponent::RemoveHealth(float Amount)
{
if (Amount <= 0.0f)
{
return;
}
SetHealth(CurrentHealth - Amount);
}
void UHealthComponent::SetHealthDebuff(float NewHealthDebuff)
{
HealthDebuff = FMath::Clamp(NewHealthDebuff, 0.0f, 1.0f);
const float ClampedHealth = FMath::Clamp(CurrentHealth, 0.0f, GetEffectiveMaxHealth());
if (!FMath::IsNearlyEqual(ClampedHealth, CurrentHealth))
{
CurrentHealth = ClampedHealth;
OnHealthChanged.Broadcast(CurrentHealth);
}
OnHealthDebuffChanged.Broadcast(HealthDebuff);
}
@@ -1,73 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HealthComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthDebuffChanged, float, NewHealthDebuff);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class KINGSHEARTHLEGACY_API UHealthComponent : public UActorComponent
{
GENERATED_BODY()
public:
UHealthComponent();
protected:
virtual void BeginPlay() override;
public:
// Health
UFUNCTION(BlueprintPure, Category = "Health")
float GetHealth() const;
/** Nominal cap, unaffected by HealthDebuff. Feeds the health bar's total width. */
UFUNCTION(BlueprintPure, Category = "Health")
float GetMaxHealth() const;
/** 0-1 fraction of MaxHealth currently locked/unusable (curses, status effects, ...). */
UFUNCTION(BlueprintPure, Category = "Health")
float GetHealthDebuff() const;
UFUNCTION(BlueprintCallable, Category = "Health")
void SetMaxHealth(float NewMaxHealth);
UFUNCTION(BlueprintCallable, Category = "Health")
void SetHealth(float NewHealth);
UFUNCTION(BlueprintCallable, Category = "Health")
void AddHealth(float Amount);
UFUNCTION(BlueprintCallable, Category = "Health")
void RemoveHealth(float Amount);
/** Sets how much of MaxHealth is currently locked away (0-1). Not driven by anything yet - a future debuff/curse/status-effect system calls this. */
UFUNCTION(BlueprintCallable, Category = "Health")
void SetHealthDebuff(float NewHealthDebuff);
protected:
/** MaxHealth minus whatever HealthDebuff currently locks away - what Health is actually clamped against. */
float GetEffectiveMaxHealth() const;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Health")
float MaxHealth = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Health")
float CurrentHealth = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Health")
float HealthDebuff = 0.0f;
public:
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnHealthChanged OnHealthChanged;
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnHealthDebuffChanged OnHealthDebuffChanged;
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnDeath OnDeath;
};
@@ -18,13 +18,18 @@ public class KingshearthLegacy : ModuleRules
"StateTreeModule",
"GameplayStateTreeModule",
"UMG",
"Slate"
"Slate",
"GameplayAbilities",
"GameplayTags",
"GameplayTasks"
});
PrivateDependencyModuleNames.AddRange(new string[] { });
PublicIncludePaths.AddRange(new string[] {
"KingshearthLegacy",
"KingshearthLegacy/AttributeSets",
"KingshearthLegacy/GameplayEffects",
"KingshearthLegacy/Variant_Platforming",
"KingshearthLegacy/Variant_Platforming/Animation",
"KingshearthLegacy/Variant_Combat",
@@ -11,7 +11,14 @@
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "KingshearthLegacy.h"
#include "StaminaComponent.h"
#include "AbilitySystemComponent.h"
#include "HealthAttributeSet.h"
#include "StaminaAttributeSet.h"
#include "GE_HealthDamage.h"
#include "GE_HealthHealing.h"
#include "GE_HealthDebuff.h"
#include "GE_StaminaFatiguePenalty.h"
#include "KingshearthLegacyGameplayTags.h"
AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
{
@@ -47,8 +54,16 @@ AKingshearthLegacyCharacter::AKingshearthLegacyCharacter()
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
// Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character)
// Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character)
// are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++)
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
HealthAttributeSet = CreateDefaultSubobject<UHealthAttributeSet>(TEXT("HealthAttributeSet"));
AbilitySystemComponent->AddAttributeSetSubobject(HealthAttributeSet.Get());
StaminaAttributeSet = CreateDefaultSubobject<UStaminaAttributeSet>(TEXT("StaminaAttributeSet"));
AbilitySystemComponent->AddAttributeSetSubobject(StaminaAttributeSet.Get());
}
void AKingshearthLegacyCharacter::BeginPlay()
@@ -56,14 +71,172 @@ void AKingshearthLegacyCharacter::BeginPlay()
Super::BeginPlay();
BaseWalkSpeed = GetCharacterMovement()->MaxWalkSpeed;
StaminaComponent = FindComponentByClass<UStaminaComponent>();
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
// Always-on: keeps MaxStamina tracking Fatigue via MMC_MaxStaminaFatiguePenalty for the character's lifetime.
const FGameplayEffectSpecHandle FatiguePenaltySpec = AbilitySystemComponent->MakeOutgoingSpec(UGE_StaminaFatiguePenalty::StaticClass(), 1.0f, AbilitySystemComponent->MakeEffectContext());
if (FatiguePenaltySpec.IsValid())
{
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*FatiguePenaltySpec.Data.Get());
}
}
}
UAbilitySystemComponent* AKingshearthLegacyCharacter::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
float AKingshearthLegacyCharacter::GetHealth() const
{
return HealthAttributeSet ? HealthAttributeSet->GetHealth() : 0.0f;
}
float AKingshearthLegacyCharacter::GetMaxHealth() const
{
return HealthAttributeSet ? HealthAttributeSet->GetNominalMaxHealth() : 0.0f;
}
float AKingshearthLegacyCharacter::GetHealthDebuff() const
{
return HealthAttributeSet ? HealthAttributeSet->GetHealthDebuff() : 0.0f;
}
void AKingshearthLegacyCharacter::ApplyDamage(float Amount)
{
if (Amount <= 0.0f || !AbilitySystemComponent)
{
return;
}
const FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(UGE_HealthDamage::StaticClass(), 1.0f, AbilitySystemComponent->MakeEffectContext());
if (SpecHandle.IsValid())
{
SpecHandle.Data->SetSetByCallerMagnitude(TAG_Data_Health_Damage.GetTag(), Amount);
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
void AKingshearthLegacyCharacter::ApplyHealing(float Amount)
{
if (Amount <= 0.0f || !AbilitySystemComponent)
{
return;
}
const FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(UGE_HealthHealing::StaticClass(), 1.0f, AbilitySystemComponent->MakeEffectContext());
if (SpecHandle.IsValid())
{
SpecHandle.Data->SetSetByCallerMagnitude(TAG_Data_Health_Healing.GetTag(), Amount);
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
void AKingshearthLegacyCharacter::SetHealthDebuff(float NewHealthDebuff)
{
if (!AbilitySystemComponent)
{
return;
}
if (HealthDebuffEffectHandle.IsValid())
{
AbilitySystemComponent->RemoveActiveGameplayEffect(HealthDebuffEffectHandle);
HealthDebuffEffectHandle.Invalidate();
}
const float ClampedDebuff = FMath::Clamp(NewHealthDebuff, 0.0f, 1.0f);
if (ClampedDebuff <= 0.0f)
{
return;
}
const FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(UGE_HealthDebuff::StaticClass(), 1.0f, AbilitySystemComponent->MakeEffectContext());
if (SpecHandle.IsValid())
{
SpecHandle.Data->SetSetByCallerMagnitude(TAG_Data_HealthDebuff_Multiplier.GetTag(), 1.0f - ClampedDebuff);
HealthDebuffEffectHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
float AKingshearthLegacyCharacter::GetStamina() const
{
return StaminaAttributeSet ? StaminaAttributeSet->GetStamina() : 0.0f;
}
float AKingshearthLegacyCharacter::GetMaxStamina() const
{
return StaminaAttributeSet ? StaminaAttributeSet->GetNominalMaxStamina() : 0.0f;
}
float AKingshearthLegacyCharacter::GetFatigue() const
{
return StaminaAttributeSet ? StaminaAttributeSet->GetFatigue() : 0.0f;
}
void AKingshearthLegacyCharacter::SpendStamina(float Amount)
{
if (Amount <= 0.0f || !AbilitySystemComponent || !StaminaAttributeSet)
{
return;
}
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetStaminaAttribute(), EGameplayModOp::AddBase, -Amount);
TimeSinceLastStaminaSpend = 0.0f;
}
void AKingshearthLegacyCharacter::RestoreStamina(float Amount)
{
if (Amount <= 0.0f || !AbilitySystemComponent || !StaminaAttributeSet)
{
return;
}
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetStaminaAttribute(), EGameplayModOp::AddBase, Amount);
}
void AKingshearthLegacyCharacter::TickStaminaRegen(float DeltaSeconds)
{
if (!AbilitySystemComponent || !StaminaAttributeSet)
{
return;
}
TimeSinceLastStaminaSpend += DeltaSeconds;
const float TimeSinceRegenStarted = TimeSinceLastStaminaSpend - StaminaRegenDelay;
if (TimeSinceRegenStarted > 0.0f && StaminaAttributeSet->GetStamina() < StaminaAttributeSet->GetMaxStamina())
{
const float RampedRegenRate = StaminaRegenRampTime > 0.0f
? FMath::Min(TimeSinceRegenStarted / StaminaRegenRampTime, 1.0f) * StaminaRegenRate
: StaminaRegenRate;
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetStaminaAttribute(), EGameplayModOp::AddBase, RampedRegenRate * DeltaSeconds);
// Fatigue only builds while Stamina is actively regaining - never while draining, never while idle.
// It never recovers on its own; only an explicit SetFatigue call (e.g. a debug key or a future rest mechanic) reduces it.
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetFatigueAttribute(), EGameplayModOp::AddBase, FatigueGainRateWhileRegaining * DeltaSeconds);
}
}
void AKingshearthLegacyCharacter::SetFatigue(float NewFatigue)
{
if (!AbilitySystemComponent || !StaminaAttributeSet)
{
return;
}
AbilitySystemComponent->ApplyModToAttribute(UStaminaAttributeSet::GetFatigueAttribute(), EGameplayModOp::Override, FMath::Clamp(NewFatigue, 0.0f, 1.0f));
}
void AKingshearthLegacyCharacter::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (!StaminaComponent)
if (!StaminaAttributeSet)
{
return;
}
@@ -71,15 +244,17 @@ void AKingshearthLegacyCharacter::Tick(float DeltaSeconds)
const bool bIsMoving = GetVelocity().SizeSquared() > FMath::Square(SprintMovementThreshold);
const bool bIsGrounded = GetCharacterMovement()->IsMovingOnGround();
if (bWantsToSprint && bIsMoving && bIsGrounded && StaminaComponent->GetStamina() > 0.0f)
if (bWantsToSprint && bIsMoving && bIsGrounded && StaminaAttributeSet->GetStamina() > 0.0f)
{
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed * SprintSpeedMultiplier;
StaminaComponent->RemoveStamina(SprintStaminaDrainRate * DeltaSeconds);
SpendStamina(SprintStaminaDrainRate * DeltaSeconds);
}
else
{
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
}
TickStaminaRegen(DeltaSeconds);
}
void AKingshearthLegacyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
@@ -174,9 +349,9 @@ void AKingshearthLegacyCharacter::Jump()
Super::Jump();
if (bCouldJump && StaminaComponent)
if (bCouldJump)
{
StaminaComponent->RemoveStamina(JumpStaminaCost);
SpendStamina(JumpStaminaCost);
}
}
@@ -5,12 +5,16 @@
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Logging/LogMacros.h"
#include "AbilitySystemInterface.h"
#include "ActiveGameplayEffectHandle.h"
#include "KingshearthLegacyCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputAction;
class UStaminaComponent;
class UAbilitySystemComponent;
class UHealthAttributeSet;
class UStaminaAttributeSet;
struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
@@ -20,7 +24,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
* Implements a controllable orbiting camera
*/
UCLASS(abstract)
class AKingshearthLegacyCharacter : public ACharacter
class AKingshearthLegacyCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
@@ -70,6 +74,22 @@ protected:
UPROPERTY(EditAnywhere, Category="Stamina")
float JumpStaminaCost = 15.0f;
/** Regen rate once fully ramped up, in stamina/sec. */
UPROPERTY(EditAnywhere, Category="Stamina")
float StaminaRegenRate = 20.0f;
/** Seconds after the last stamina spend before regen starts at all. */
UPROPERTY(EditAnywhere, Category="Stamina")
float StaminaRegenDelay = 1.0f;
/** Seconds for the regen rate to ramp from 0 up to StaminaRegenRate once regen starts, instead of kicking in at full speed immediately. */
UPROPERTY(EditAnywhere, Category="Stamina")
float StaminaRegenRampTime = 1.5f;
/** Fatigue gained per second while Stamina is actively regaining (ramping back up). Never recovers on its own otherwise - only an explicit debug/design call reduces it. */
UPROPERTY(EditAnywhere, Category="Fatigue")
float FatigueGainRateWhileRegaining = 0.1f;
public:
/** Constructor */
@@ -100,16 +120,89 @@ protected:
/** Called when the sprint input is released */
void StopSprint();
/** Advances the regen delay/ramp curve and applies Stamina/Fatigue regen for this frame. */
void TickStaminaRegen(float DeltaSeconds);
/** Cached from the CharacterMovementComponent's MaxWalkSpeed in BeginPlay, restored whenever not sprinting. */
float BaseWalkSpeed = 0.0f;
bool bWantsToSprint = false;
UPROPERTY()
TObjectPtr<UStaminaComponent> StaminaComponent;
/** Seconds since Stamina was last reduced by a spend. Resets the regen delay/ramp whenever SpendStamina runs. */
float TimeSinceLastStaminaSpend = 0.0f;
/** Drives all attribute sets (Health, Stamina, and future Nutrition/Blood). Self-owned: this character is both owner and avatar. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Abilities", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Abilities", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UHealthAttributeSet> HealthAttributeSet;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Abilities", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UStaminaAttributeSet> StaminaAttributeSet;
/** Currently-applied GE_HealthDebuff instance, if any - reapplied/removed wholesale by SetHealthDebuff so it behaves like the old "set to X" API instead of stacking. */
FActiveGameplayEffectHandle HealthDebuffEffectHandle;
public:
//~ Begin IAbilitySystemInterface
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
//~ End IAbilitySystemInterface
/** Exposed for Blueprint to bind OnHealthChanged/OnHealthDebuffChanged/OnDeath - there's no "component bound event" for a plain UObject subobject, so bind via this getter's return pin instead (drag off it, search "Bind Event to..."). */
UFUNCTION(BlueprintPure, Category = "Health")
FORCEINLINE UHealthAttributeSet* GetHealthAttributeSet() const { return HealthAttributeSet; }
/** Exposed for Blueprint to bind OnStaminaChanged/OnFatigueChanged/OnExhausted - see GetHealthAttributeSet(). */
UFUNCTION(BlueprintPure, Category = "Stamina")
FORCEINLINE UStaminaAttributeSet* GetStaminaAttributeSet() const { return StaminaAttributeSet; }
// Health
UFUNCTION(BlueprintPure, Category = "Health")
float GetHealth() const;
/** Nominal cap, unaffected by HealthDebuff. Feeds the health bar's total width. */
UFUNCTION(BlueprintPure, Category = "Health")
float GetMaxHealth() const;
/** 0-1 fraction of MaxHealth currently locked/unusable (curses, status effects, ...). */
UFUNCTION(BlueprintPure, Category = "Health")
float GetHealthDebuff() const;
UFUNCTION(BlueprintCallable, Category = "Health")
void ApplyDamage(float Amount);
UFUNCTION(BlueprintCallable, Category = "Health")
void ApplyHealing(float Amount);
/** Sets how much of MaxHealth is currently locked away (0-1) by applying/refreshing/removing a single GE_HealthDebuff instance. */
UFUNCTION(BlueprintCallable, Category = "Health")
void SetHealthDebuff(float NewHealthDebuff);
// Stamina
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetStamina() const;
/** Nominal cap, unaffected by Fatigue. Feeds the stamina bar's total width. */
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetMaxStamina() const;
UFUNCTION(BlueprintPure, Category = "Fatigue")
float GetFatigue() const;
/** Deducts Amount from Stamina and resets the regen delay/ramp. No-op for Amount <= 0. */
UFUNCTION(BlueprintCallable, Category = "Stamina")
void SpendStamina(float Amount);
/** Adds Amount to Stamina without affecting the regen delay/ramp (e.g. a stamina potion). No-op for Amount <= 0. */
UFUNCTION(BlueprintCallable, Category = "Stamina")
void RestoreStamina(float Amount);
/** Sets Fatigue directly (0-1). Fatigue only ever builds on its own; this is the only way to reduce it (e.g. a debug key or a future rest mechanic). */
UFUNCTION(BlueprintCallable, Category = "Fatigue")
void SetFatigue(float NewFatigue);
/** Handles move inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoMove(float Right, float Forward);
@@ -0,0 +1,5 @@
#include "KingshearthLegacyGameplayTags.h"
UE_DEFINE_GAMEPLAY_TAG(TAG_Data_Health_Damage, "Data.Health.Damage");
UE_DEFINE_GAMEPLAY_TAG(TAG_Data_Health_Healing, "Data.Health.Healing");
UE_DEFINE_GAMEPLAY_TAG(TAG_Data_HealthDebuff_Multiplier, "Data.HealthDebuff.Multiplier");
@@ -0,0 +1,8 @@
#pragma once
#include "NativeGameplayTags.h"
// SetByCaller data tags - the Name a GameplayEffect spec's magnitude is set against at runtime.
KINGSHEARTHLEGACY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Data_Health_Damage)
KINGSHEARTHLEGACY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Data_Health_Healing)
KINGSHEARTHLEGACY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Data_HealthDebuff_Multiplier)
@@ -9,8 +9,7 @@
#include "KingshearthLegacy.h"
#include "Widgets/Input/SVirtualJoystick.h"
#include "UI/PlayerHUDWidget.h"
#include "HealthComponent.h"
#include "StaminaComponent.h"
#include "KingshearthLegacyCharacter.h"
void AKingshearthLegacyPlayerController::BeginPlay()
{
@@ -88,7 +87,11 @@ void AKingshearthLegacyPlayerController::OnPossess(APawn* InPawn)
if (PlayerHUD)
{
PlayerHUD->InitializeHUD(InPawn->FindComponentByClass<UHealthComponent>(), InPawn->FindComponentByClass<UStaminaComponent>());
const AKingshearthLegacyCharacter* PossessedCharacter = Cast<AKingshearthLegacyCharacter>(InPawn);
UHealthAttributeSet* PossessedHealthAttributeSet = PossessedCharacter ? PossessedCharacter->GetHealthAttributeSet() : nullptr;
UStaminaAttributeSet* PossessedStaminaAttributeSet = PossessedCharacter ? PossessedCharacter->GetStaminaAttributeSet() : nullptr;
PlayerHUD->InitializeHUD(PossessedHealthAttributeSet, PossessedStaminaAttributeSet);
}
}
@@ -55,7 +55,7 @@ protected:
/** Input mapping context setup */
virtual void SetupInputComponent() override;
/** Spawns/initializes the player HUD once a pawn (and its HealthComponent) is available */
/** Spawns/initializes the player HUD once a pawn (and its HealthAttributeSet) is available */
virtual void OnPossess(APawn* InPawn) override;
/** Returns true if the player should use UMG touch controls */
@@ -1,115 +0,0 @@
#include "StaminaComponent.h"
UStaminaComponent::UStaminaComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UStaminaComponent::BeginPlay()
{
Super::BeginPlay();
CurrentStamina = MaxStamina;
}
void UStaminaComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
TimeSinceLastStaminaSpend += DeltaTime;
const float TimeSinceRegenStarted = TimeSinceLastStaminaSpend - StaminaRegenDelay;
if (TimeSinceRegenStarted > 0.0f && CurrentStamina < GetEffectiveMaxStamina())
{
const float RampedRegenRate = StaminaRegenRampTime > 0.0f
? FMath::Min(TimeSinceRegenStarted / StaminaRegenRampTime, 1.0f) * StaminaRegenRate
: StaminaRegenRate;
SetStamina(CurrentStamina + RampedRegenRate * DeltaTime);
// Fatigue only builds while Stamina is actively regaining - never while draining, never
// while idle. It never recovers on its own; only an explicit SetFatigue call does.
SetFatigue(Fatigue + FatigueGainRateWhileRegaining * DeltaTime);
}
}
float UStaminaComponent::GetStamina() const
{
return CurrentStamina;
}
float UStaminaComponent::GetMaxStamina() const
{
return MaxStamina;
}
float UStaminaComponent::GetFatigue() const
{
return Fatigue;
}
float UStaminaComponent::GetEffectiveMaxStamina() const
{
return MaxStamina * (1.0f - Fatigue);
}
void UStaminaComponent::SetMaxStamina(float NewMaxStamina)
{
MaxStamina = FMath::Max(NewMaxStamina, 0.0f);
CurrentStamina = FMath::Clamp(CurrentStamina, 0.0f, GetEffectiveMaxStamina());
}
void UStaminaComponent::SetStamina(float NewStamina)
{
const float PreviousStamina = CurrentStamina;
CurrentStamina = FMath::Clamp(NewStamina, 0.0f, GetEffectiveMaxStamina());
if (CurrentStamina < PreviousStamina)
{
TimeSinceLastStaminaSpend = 0.0f;
}
OnStaminaChanged.Broadcast(CurrentStamina);
// Only trigger the exhaustion signal when crossing from not-empty to empty.
if (PreviousStamina > 0.0f && CurrentStamina <= 0.0f)
{
OnExhausted.Broadcast();
}
}
void UStaminaComponent::AddStamina(float Amount)
{
if (Amount <= 0.0f)
{
return;
}
SetStamina(CurrentStamina + Amount);
}
void UStaminaComponent::RemoveStamina(float Amount)
{
if (Amount <= 0.0f)
{
return;
}
SetStamina(CurrentStamina - Amount);
}
void UStaminaComponent::SetFatigue(float NewFatigue)
{
Fatigue = FMath::Clamp(NewFatigue, 0.0f, 1.0f);
const float ClampedStamina = FMath::Clamp(CurrentStamina, 0.0f, GetEffectiveMaxStamina());
if (!FMath::IsNearlyEqual(ClampedStamina, CurrentStamina))
{
CurrentStamina = ClampedStamina;
OnStaminaChanged.Broadcast(CurrentStamina);
}
OnFatigueChanged.Broadcast(Fatigue);
}
@@ -1,93 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "StaminaComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStaminaChanged, float, NewStamina);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFatigueChanged, float, NewFatigue);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnExhausted);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class KINGSHEARTHLEGACY_API UStaminaComponent : public UActorComponent
{
GENERATED_BODY()
public:
UStaminaComponent();
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
public:
// Stamina
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetStamina() const;
/** Nominal cap, unaffected by Fatigue. Feeds the stamina bar's total width. */
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetMaxStamina() const;
/** 0-1 fraction of MaxStamina currently locked/unusable due to over-extension. */
UFUNCTION(BlueprintPure, Category = "Stamina")
float GetFatigue() const;
UFUNCTION(BlueprintCallable, Category = "Stamina")
void SetMaxStamina(float NewMaxStamina);
UFUNCTION(BlueprintCallable, Category = "Stamina")
void SetStamina(float NewStamina);
UFUNCTION(BlueprintCallable, Category = "Stamina")
void AddStamina(float Amount);
UFUNCTION(BlueprintCallable, Category = "Stamina")
void RemoveStamina(float Amount);
UFUNCTION(BlueprintCallable, Category = "Fatigue")
void SetFatigue(float NewFatigue);
protected:
/** MaxStamina minus whatever Fatigue currently locks away - what Stamina is actually clamped against. */
float GetEffectiveMaxStamina() const;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
float MaxStamina = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stamina")
float CurrentStamina = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Fatigue")
float Fatigue = 0.0f;
/** Regen rate once fully ramped up, in stamina/sec. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
float StaminaRegenRate = 20.0f;
/** Seconds after the last stamina spend before regen starts at all. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
float StaminaRegenDelay = 1.0f;
/** Seconds for the regen rate to ramp from 0 up to StaminaRegenRate once regen starts, instead of kicking in at full speed immediately. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stamina")
float StaminaRegenRampTime = 1.5f;
/** Seconds since Stamina was last reduced by a spend (RemoveStamina/SetStamina decrease). Resets the regen delay/ramp whenever stamina is spent. */
float TimeSinceLastStaminaSpend = 0.0f;
/** Fatigue gained per second while Stamina is actively regaining (ramping back up). Never recovers on its own otherwise - only an explicit SetFatigue call (e.g. a debug key) reduces it. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fatigue")
float FatigueGainRateWhileRegaining = 0.1f;
public:
UPROPERTY(BlueprintAssignable, Category = "Stamina")
FOnStaminaChanged OnStaminaChanged;
UPROPERTY(BlueprintAssignable, Category = "Fatigue")
FOnFatigueChanged OnFatigueChanged;
/** Broadcast when Stamina is driven from a positive value to empty. */
UPROPERTY(BlueprintAssignable, Category = "Stamina")
FOnExhausted OnExhausted;
};
+15 -15
View File
@@ -1,28 +1,28 @@
#include "HealthBarWidget.h"
#include "HealthComponent.h"
#include "HealthAttributeSet.h"
void UHealthBarWidget::InitializeHealthBar(UHealthComponent* InHealthComponent)
void UHealthBarWidget::InitializeHealthBar(UHealthAttributeSet* InHealthAttributeSet)
{
if (!InHealthComponent)
if (!InHealthAttributeSet)
{
return;
}
if (HealthComponent)
if (HealthAttributeSet)
{
HealthComponent->OnHealthChanged.RemoveDynamic(this, &UHealthBarWidget::HandleHealthChanged);
HealthComponent->OnHealthDebuffChanged.RemoveDynamic(this, &UHealthBarWidget::HandleHealthDebuffChanged);
HealthComponent->OnDeath.RemoveDynamic(this, &UHealthBarWidget::HandleDeath);
HealthAttributeSet->OnHealthChanged.RemoveDynamic(this, &UHealthBarWidget::HandleHealthChanged);
HealthAttributeSet->OnHealthDebuffChanged.RemoveDynamic(this, &UHealthBarWidget::HandleHealthDebuffChanged);
HealthAttributeSet->OnDeath.RemoveDynamic(this, &UHealthBarWidget::HandleDeath);
}
HealthComponent = InHealthComponent;
HealthAttributeSet = InHealthAttributeSet;
HealthComponent->OnHealthChanged.AddDynamic(this, &UHealthBarWidget::HandleHealthChanged);
HealthComponent->OnHealthDebuffChanged.AddDynamic(this, &UHealthBarWidget::HandleHealthDebuffChanged);
HealthComponent->OnDeath.AddDynamic(this, &UHealthBarWidget::HandleDeath);
HealthAttributeSet->OnHealthChanged.AddDynamic(this, &UHealthBarWidget::HandleHealthChanged);
HealthAttributeSet->OnHealthDebuffChanged.AddDynamic(this, &UHealthBarWidget::HandleHealthDebuffChanged);
HealthAttributeSet->OnDeath.AddDynamic(this, &UHealthBarWidget::HandleDeath);
SetStatValue(HealthComponent->GetHealth(), HealthComponent->GetMaxHealth());
SetMaxHealthDebuff(HealthComponent->GetHealthDebuff());
SetStatValue(HealthAttributeSet->GetHealth(), HealthAttributeSet->GetNominalMaxHealth());
SetMaxHealthDebuff(HealthAttributeSet->GetHealthDebuff());
}
void UHealthBarWidget::SetMaxHealthDebuff(float InDebuffPercent)
@@ -32,9 +32,9 @@ void UHealthBarWidget::SetMaxHealthDebuff(float InDebuffPercent)
void UHealthBarWidget::HandleHealthChanged(float NewHealth)
{
if (HealthComponent)
if (HealthAttributeSet)
{
SetStatValue(NewHealth, HealthComponent->GetMaxHealth());
SetStatValue(NewHealth, HealthAttributeSet->GetNominalMaxHealth());
}
}
@@ -4,11 +4,11 @@
#include "UI/StatBarWidget.h"
#include "HealthBarWidget.generated.h"
class UHealthComponent;
class UHealthAttributeSet;
/**
* Stat bar bound to a live UHealthComponent. Call InitializeHealthBar once the
* component is known (typically from the owning PlayerController on possession).
* Stat bar bound to a live UHealthAttributeSet. Call InitializeHealthBar once the
* attribute set is known (typically from the owning PlayerController on possession).
*/
UCLASS(abstract)
class UHealthBarWidget : public UStatBarWidget
@@ -17,7 +17,7 @@ class UHealthBarWidget : public UStatBarWidget
public:
UFUNCTION(BlueprintCallable, Category = "Health")
void InitializeHealthBar(UHealthComponent* InHealthComponent);
void InitializeHealthBar(UHealthAttributeSet* InHealthAttributeSet);
/** Hook for a future debuff/status-effect system: 0-1 fraction of max health currently locked/unusable. */
UFUNCTION(BlueprintCallable, Category = "Health")
@@ -38,5 +38,5 @@ protected:
void OnHealthDepleted();
UPROPERTY(BlueprintReadOnly, Category = "Health")
TObjectPtr<UHealthComponent> HealthComponent;
TObjectPtr<UHealthAttributeSet> HealthAttributeSet;
};
@@ -2,15 +2,15 @@
#include "UI/HealthBarWidget.h"
#include "UI/StaminaBarWidget.h"
void UPlayerHUDWidget::InitializeHUD(UHealthComponent* InHealthComponent, UStaminaComponent* InStaminaComponent)
void UPlayerHUDWidget::InitializeHUD(UHealthAttributeSet* InHealthAttributeSet, UStaminaAttributeSet* InStaminaAttributeSet)
{
if (HealthBar)
{
HealthBar->InitializeHealthBar(InHealthComponent);
HealthBar->InitializeHealthBar(InHealthAttributeSet);
}
if (StaminaBar)
{
StaminaBar->InitializeStaminaBar(InStaminaComponent);
StaminaBar->InitializeStaminaBar(InStaminaAttributeSet);
}
}
@@ -6,8 +6,8 @@
class UHealthBarWidget;
class UStaminaBarWidget;
class UHealthComponent;
class UStaminaComponent;
class UHealthAttributeSet;
class UStaminaAttributeSet;
/**
* Top-level player HUD. Owns a health bar and a stamina bar and forwards
@@ -21,7 +21,7 @@ class UPlayerHUDWidget : public UUserWidget
public:
UFUNCTION(BlueprintCallable, Category = "HUD")
void InitializeHUD(UHealthComponent* InHealthComponent, UStaminaComponent* InStaminaComponent);
void InitializeHUD(UHealthAttributeSet* InHealthAttributeSet, UStaminaAttributeSet* InStaminaAttributeSet);
protected:
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional = true))
@@ -1,33 +1,33 @@
#include "StaminaBarWidget.h"
#include "StaminaComponent.h"
#include "StaminaAttributeSet.h"
void UStaminaBarWidget::InitializeStaminaBar(UStaminaComponent* InStaminaComponent)
void UStaminaBarWidget::InitializeStaminaBar(UStaminaAttributeSet* InStaminaAttributeSet)
{
if (!InStaminaComponent)
if (!InStaminaAttributeSet)
{
return;
}
if (StaminaComponent)
if (StaminaAttributeSet)
{
StaminaComponent->OnStaminaChanged.RemoveDynamic(this, &UStaminaBarWidget::HandleStaminaChanged);
StaminaComponent->OnFatigueChanged.RemoveDynamic(this, &UStaminaBarWidget::HandleFatigueChanged);
StaminaAttributeSet->OnStaminaChanged.RemoveDynamic(this, &UStaminaBarWidget::HandleStaminaChanged);
StaminaAttributeSet->OnFatigueChanged.RemoveDynamic(this, &UStaminaBarWidget::HandleFatigueChanged);
}
StaminaComponent = InStaminaComponent;
StaminaAttributeSet = InStaminaAttributeSet;
StaminaComponent->OnStaminaChanged.AddDynamic(this, &UStaminaBarWidget::HandleStaminaChanged);
StaminaComponent->OnFatigueChanged.AddDynamic(this, &UStaminaBarWidget::HandleFatigueChanged);
StaminaAttributeSet->OnStaminaChanged.AddDynamic(this, &UStaminaBarWidget::HandleStaminaChanged);
StaminaAttributeSet->OnFatigueChanged.AddDynamic(this, &UStaminaBarWidget::HandleFatigueChanged);
SetStatValue(StaminaComponent->GetStamina(), StaminaComponent->GetMaxStamina());
SetFatigue(StaminaComponent->GetFatigue());
SetStatValue(StaminaAttributeSet->GetStamina(), StaminaAttributeSet->GetNominalMaxStamina());
SetFatigue(StaminaAttributeSet->GetFatigue());
}
void UStaminaBarWidget::HandleStaminaChanged(float NewStamina)
{
if (StaminaComponent)
if (StaminaAttributeSet)
{
SetStatValue(NewStamina, StaminaComponent->GetMaxStamina());
SetStatValue(NewStamina, StaminaAttributeSet->GetNominalMaxStamina());
}
}
@@ -4,11 +4,11 @@
#include "UI/StatBarWidget.h"
#include "StaminaBarWidget.generated.h"
class UStaminaComponent;
class UStaminaAttributeSet;
/**
* Stat bar bound to a live UStaminaComponent. Call InitializeStaminaBar once the
* component is known (typically from the owning PlayerController on possession),
* Stat bar bound to a live UStaminaAttributeSet. Call InitializeStaminaBar once the
* attribute set is known (typically from the owning PlayerController on possession),
* mirroring UHealthBarWidget::InitializeHealthBar.
*/
UCLASS(abstract)
@@ -18,7 +18,7 @@ class UStaminaBarWidget : public UStatBarWidget
public:
UFUNCTION(BlueprintCallable, Category = "Stamina")
void InitializeStaminaBar(UStaminaComponent* InStaminaComponent);
void InitializeStaminaBar(UStaminaAttributeSet* InStaminaAttributeSet);
UFUNCTION(BlueprintCallable, Category = "Fatigue")
void SetFatigue(float NewFatigue);
@@ -38,7 +38,7 @@ protected:
void OnFatigueChanged(float NewFatigue);
UPROPERTY(BlueprintReadOnly, Category = "Stamina")
TObjectPtr<UStaminaComponent> StaminaComponent;
TObjectPtr<UStaminaAttributeSet> StaminaAttributeSet;
/** 0-1 exhaustion amount. Drives the bar's penalty segment via SetPenaltyPercent. */
UPROPERTY(BlueprintReadOnly, Category = "Fatigue")