Implemented HealthAttributeSet and VerbMessage

This commit is contained in:
LongLy 2025-01-15 10:00:06 -07:00
parent b9b7e5344b
commit a43fc18456
4 changed files with 388 additions and 0 deletions

View File

@ -0,0 +1,243 @@
// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action.
#include "AbilitySystem/Attributes/OLSHealthAttributeSet.h"
#include "GameplayEffectExtension.h"
#include "AbilitySystem/OLSAbilitySystemComponent.h"
#include "GameFramework/GameplayMessageSubsystem.h"
#include "Messages/OLSVerbMessage.h"
#include "Net/UnrealNetwork.h"
UE_DEFINE_GAMEPLAY_TAG(TAG_Gameplay_Damage, "Gameplay.Damage");
UE_DEFINE_GAMEPLAY_TAG(TAG_Gameplay_DamageImmunity, "Gameplay.DamageImmunity");
UE_DEFINE_GAMEPLAY_TAG(TAG_Gameplay_DamageSelfDestruct, "Gameplay.Damage.SelfDestruct");
UE_DEFINE_GAMEPLAY_TAG(TAG_Gameplay_FellOutOfWorld, "Gameplay.Damage.FellOutOfWorld");
UE_DEFINE_GAMEPLAY_TAG(TAG_OLS_Damage_Message, "Lyra.Damage.Message");
UOLSHealthAttributeSet::UOLSHealthAttributeSet()
: Health(100.0f)
, MaxHealth(100.0f)
{
bIsOutOfHealth = false;
MaxHealthBeforeAttributeChange = 0.0f;
HealthBeforeAttributeChange = 0.0f;
}
void UOLSHealthAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UOLSHealthAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UOLSHealthAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
}
void UOLSHealthAttributeSet::OnRep_Health(const FGameplayAttributeData& oldValue)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UOLSHealthAttributeSet, Health, oldValue);
// Call the change callback, but without an instigator
// This could be changed to an explicit RPC in the future
// These events on the client should not be changing attributes
const float currentHealth = GetHealth();
const float estimatedMagnitude = currentHealth - oldValue.GetCurrentValue();
OnHealthChanged.Broadcast(
nullptr,
nullptr,
nullptr,
estimatedMagnitude,
oldValue.GetCurrentValue(),
currentHealth);
const bool isCurrentHealthEqualOrBelowZero = (currentHealth <= 0.0f);
if (!bIsOutOfHealth && isCurrentHealthEqualOrBelowZero)
{
OnOutOfHealth.Broadcast(nullptr, nullptr, nullptr, estimatedMagnitude, oldValue.GetCurrentValue(), currentHealth);
}
bIsOutOfHealth = isCurrentHealthEqualOrBelowZero;
}
void UOLSHealthAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldValue)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UOLSHealthAttributeSet, MaxHealth, OldValue);
// Call the change callback, but without an instigator
// This could be changed to an explicit RPC in the future
OnMaxHealthChanged.Broadcast(
nullptr,
nullptr,
nullptr,
GetMaxHealth() - OldValue.GetCurrentValue(),
OldValue.GetCurrentValue(), GetMaxHealth());
}
bool UOLSHealthAttributeSet::PreGameplayEffectExecute(FGameplayEffectModCallbackData& data)
{
if (!Super::PreGameplayEffectExecute(data))
{
return false;
}
// Handle modifying incoming normal damage
if (data.EvaluatedData.Attribute == GetDamageAttribute())
{
if (data.EvaluatedData.Magnitude > 0.0f)
{
const bool isDamageFromSelfDestruct = data.EffectSpec.GetDynamicAssetTags().HasTagExact(TAG_Gameplay_DamageSelfDestruct);
if (data.Target.HasMatchingGameplayTag(TAG_Gameplay_DamageImmunity) && !isDamageFromSelfDestruct)
{
// Do not take away any health.
data.EvaluatedData.Magnitude = 0.0f;
return false;
}
#if !UE_BUILD_SHIPPING
// Check GodMode cheat, unlimited health is checked below
// if (data.Target.HasMatchingGameplayTag(LyraGameplayTags::Cheat_GodMode) && !isDamageFromSelfDestruct)
// {
// // Do not take away any health.
// data.EvaluatedData.Magnitude = 0.0f;
// return false;
// }
#endif // #if !UE_BUILD_SHIPPING
}
}
// Save the current health
HealthBeforeAttributeChange = GetHealth();
MaxHealthBeforeAttributeChange = GetMaxHealth();
return true;
}
void UOLSHealthAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
const bool isDamageFromSelfDestruct = Data.EffectSpec.GetDynamicAssetTags().HasTagExact(TAG_Gameplay_DamageSelfDestruct);
float minimumHealth = 0.0f;
#if !UE_BUILD_SHIPPING
// Godmode and unlimited health stop death unless it's a self destruct
// if (!bIsDamageFromSelfDestruct &&
// (Data.Target.HasMatchingGameplayTag(LyraGameplayTags::Cheat_GodMode) || Data.Target.HasMatchingGameplayTag(LyraGameplayTags::Cheat_UnlimitedHealth) ))
// {
// MinimumHealth = 1.0f;
// }
#endif // #if !UE_BUILD_SHIPPING
const FGameplayEffectContextHandle& effectContext = Data.EffectSpec.GetEffectContext();
AActor* instigator = effectContext.GetOriginalInstigator();
AActor* causer = effectContext.GetEffectCauser();
if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
// Send a standardized verb message that other systems can observe
if (Data.EvaluatedData.Magnitude > 0.0f)
{
FOLSVerbMessage message;
message.Verb = TAG_OLS_Damage_Message;
message.Instigator = Data.EffectSpec.GetEffectContext().GetEffectCauser();
message.InstigatorTags = *Data.EffectSpec.CapturedSourceTags.GetAggregatedTags();
message.Target = GetOwningActor();
message.TargetTags = *Data.EffectSpec.CapturedTargetTags.GetAggregatedTags();
//@TODO: Fill out context tags, and any non-ability-system source/instigator tags
//@TODO: Determine if it's an opposing team kill, self-own, team kill, etc...
message.Magnitude = Data.EvaluatedData.Magnitude;
UGameplayMessageSubsystem& messageSystem = UGameplayMessageSubsystem::Get(GetWorld());
messageSystem.BroadcastMessage(message.Verb, message);
}
// Convert into -Health and then clamp
SetHealth(FMath::Clamp(GetHealth() - GetDamage(), minimumHealth, GetMaxHealth()));
SetDamage(0.0f);
}
else if (Data.EvaluatedData.Attribute == GetHealingAttribute())
{
// Convert into +Health and then clamo
SetHealth(FMath::Clamp(GetHealth() + GetHealing(), minimumHealth, GetMaxHealth()));
SetHealing(0.0f);
}
else if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// Clamp and fall into out of health handling below
SetHealth(FMath::Clamp(GetHealth(), minimumHealth, GetMaxHealth()));
}
else if (Data.EvaluatedData.Attribute == GetMaxHealthAttribute())
{
// TODO clamp current health?
// Notify on any requested max health changes
OnMaxHealthChanged.Broadcast(instigator, causer, &Data.EffectSpec, Data.EvaluatedData.Magnitude, MaxHealthBeforeAttributeChange, GetMaxHealth());
}
// If health has actually changed activate callbacks
if (GetHealth() != HealthBeforeAttributeChange)
{
OnHealthChanged.Broadcast(instigator, causer, &Data.EffectSpec, Data.EvaluatedData.Magnitude, HealthBeforeAttributeChange, GetHealth());
}
if ((GetHealth() <= 0.0f) && !bIsOutOfHealth)
{
OnOutOfHealth.Broadcast(instigator, causer, &Data.EffectSpec, Data.EvaluatedData.Magnitude, HealthBeforeAttributeChange, GetHealth());
}
// Check health again in case an event above changed it.
bIsOutOfHealth = (GetHealth() <= 0.0f);
}
void UOLSHealthAttributeSet::PreAttributeBaseChange(const FGameplayAttribute& attribute, float& newValue) const
{
Super::PreAttributeBaseChange(attribute, newValue);
ClampAttribute(attribute, newValue);
}
void UOLSHealthAttributeSet::PreAttributeChange(const FGameplayAttribute& attribute, float& newValue)
{
Super::PreAttributeChange(attribute, newValue);
ClampAttribute(attribute, newValue);
}
void UOLSHealthAttributeSet::PostAttributeChange(const FGameplayAttribute& attribute, float oldValue, float newValue)
{
Super::PostAttributeChange(attribute, oldValue, newValue);
if (attribute == GetMaxHealthAttribute())
{
// Make sure current health is not greater than the new max health.
if (GetHealth() > newValue)
{
UOLSAbilitySystemComponent* asc = GetOLSAbilitySystemComponent();
check(asc);
asc->ApplyModToAttribute(GetHealthAttribute(), EGameplayModOp::Override, newValue);
}
}
if (bIsOutOfHealth && (GetHealth() > 0.0f))
{
bIsOutOfHealth = false;
}
}
void UOLSHealthAttributeSet::ClampAttribute(const FGameplayAttribute& attribute, float& outNewValue) const
{
if (attribute == GetHealthAttribute())
{
// Do not allow health to go negative or above max health.
outNewValue = FMath::Clamp(outNewValue, 0.0f, GetMaxHealth());
}
else if (attribute == GetMaxHealthAttribute())
{
// Do not allow max health to drop below 1.
outNewValue = FMath::Max(outNewValue, 1.0f);
}
}

View File

@ -0,0 +1,11 @@
// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action.
#include "Messages/OLSVerbMessage.h"
FString FOLSVerbMessage::ToString() const
{
FString HumanReadableMessage;
FOLSVerbMessage::StaticStruct()->ExportText(/*out*/ HumanReadableMessage, this, /*Defaults=*/ nullptr, /*OwnerObject=*/ nullptr, PPF_None, /*ExportRootScope=*/ nullptr);
return HumanReadableMessage;
}

View File

@ -0,0 +1,96 @@
// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action.
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "NativeGameplayTags.h"
#include "OLSAttributeSetBase.h"
#include "OLSHealthAttributeSet.generated.h"
OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Gameplay_Damage);
OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Gameplay_DamageImmunity);
OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Gameplay_DamageSelfDestruct);
OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Gameplay_FellOutOfWorld);
OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_OLS_Damage_Message);
/**
* UOLSHealthAttributeSet
*
* Class that defines attributes that are necessary for taking damage.
* Attribute examples include: health, shields, and resistances.
*/
UCLASS(BlueprintType)
class OLS_API UOLSHealthAttributeSet : public UOLSAttributeSetBase
{
GENERATED_BODY()
public:
UOLSHealthAttributeSet();
//~ Begin UObject interface.
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
//~ End UObject interface.
ATTRIBUTE_ACCESSORS(ThisClass, Health);
ATTRIBUTE_ACCESSORS(ThisClass, MaxHealth);
ATTRIBUTE_ACCESSORS(ThisClass, Healing);
ATTRIBUTE_ACCESSORS(ThisClass, Damage);
// Delegate when health changes due to damage/healing, some information may be missing on the client
mutable FOLSAttributeEventNativeDelegate OnHealthChanged;
// Delegate when max health changes
mutable FOLSAttributeEventNativeDelegate OnMaxHealthChanged;
// Delegate to broadcast when the health attribute reaches zero
mutable FOLSAttributeEventNativeDelegate OnOutOfHealth;
protected:
UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& oldValue);
UFUNCTION()
void OnRep_MaxHealth(const FGameplayAttributeData& OldValue);
virtual bool PreGameplayEffectExecute(FGameplayEffectModCallbackData& data) override;
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
virtual void PreAttributeBaseChange(const FGameplayAttribute& attribute, float& newValue) const override;
virtual void PreAttributeChange(const FGameplayAttribute& attribute, float& newValue) override;
virtual void PostAttributeChange(const FGameplayAttribute& attribute, float oldValue, float newValue) override;
void ClampAttribute(const FGameplayAttribute& attribute, float& outNewValue) const;
private:
// The current health attribute. The health will be capped by the max health attribute. Health is hidden from modifiers so only executions can modify it.
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "OLS|Health", Meta = (HideFromModifiers, AllowPrivateAccess = true))
FGameplayAttributeData Health;
// The current max health attribute. Max health is an attribute since gameplay effects can modify it.
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "OLS|Health", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData MaxHealth;
// Used to track when the health reaches 0.
uint8 bIsOutOfHealth : 1 = false;
// Store the health before any changes
float MaxHealthBeforeAttributeChange = 0.f;
float HealthBeforeAttributeChange = 0.f
;
// -------------------------------------------------------------------
// Meta Attribute (please keep attributes that aren't 'stateful' below
// -------------------------------------------------------------------
// Incoming healing. This is mapped directly to +Health
UPROPERTY(BlueprintReadOnly, Category="Lyra|Health", Meta=(AllowPrivateAccess=true))
FGameplayAttributeData Healing;
// Incoming damage. This is mapped directly to -Health
UPROPERTY(BlueprintReadOnly, Category="Lyra|Health", Meta=(HideFromModifiers, AllowPrivateAccess=true))
FGameplayAttributeData Damage;
};

View File

@ -0,0 +1,38 @@
// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "OLSVerbMessage.generated.h"
// Represents a generic message of the form Instigator Verb Target (in Context, with Magnitude)
USTRUCT(BlueprintType)
struct FOLSVerbMessage
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, Category=Gameplay)
FGameplayTag Verb = FGameplayTag::EmptyTag;
UPROPERTY(BlueprintReadWrite, Category=Gameplay)
TObjectPtr<class UObject> Instigator = nullptr;
UPROPERTY(BlueprintReadWrite, Category=Gameplay)
TObjectPtr<class UObject> Target = nullptr;
UPROPERTY(BlueprintReadWrite, Category=Gameplay)
FGameplayTagContainer InstigatorTags;
UPROPERTY(BlueprintReadWrite, Category=Gameplay)
FGameplayTagContainer TargetTags;
UPROPERTY(BlueprintReadWrite, Category=Gameplay)
FGameplayTagContainer ContextTags;
UPROPERTY(BlueprintReadWrite, Category=Gameplay)
double Magnitude = 1.0;
// Returns a debug string representation of this message
OLS_API FString ToString() const;
};