// Copyright Epic Games, Inc. All Rights Reserved. #include "Character/LyraHealthComponent.h" #include "LyraLogChannels.h" #include "System/LyraAssetManager.h" #include "System/LyraGameData.h" #include "LyraGameplayTags.h" #include "Net/UnrealNetwork.h" #include "GameplayEffect.h" #include "GameplayEffectExtension.h" #include "GameplayPrediction.h" #include "Abilities/GameplayAbilityTypes.h" #include "AbilitySystem/LyraAbilitySystemComponent.h" #include "AbilitySystem/Attributes/LyraHealthSet.h" #include "Messages/LyraVerbMessage.h" #include "Messages/LyraVerbMessageHelpers.h" #include "NativeGameplayTags.h" #include "Components/GameFrameworkComponentManager.h" #include "GameFramework/GameplayMessageSubsystem.h" #include "GameFramework/PlayerState.h" #include "Engine/World.h" UE_DEFINE_GAMEPLAY_TAG_STATIC(TAG_Lyra_Elimination_Message, "Lyra.Elimination.Message"); ULyraHealthComponent::ULyraHealthComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryComponentTick.bStartWithTickEnabled = false; PrimaryComponentTick.bCanEverTick = false; SetIsReplicatedByDefault(true); AbilitySystemComponent = nullptr; HealthSet = nullptr; DeathState = ELyraDeathState::NotDead; } void ULyraHealthComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(ULyraHealthComponent, DeathState); } void ULyraHealthComponent::OnUnregister() { UninitializeFromAbilitySystem(); Super::OnUnregister(); } void ULyraHealthComponent::InitializeWithAbilitySystem(ULyraAbilitySystemComponent* InASC) { AActor* Owner = GetOwner(); check(Owner); if (AbilitySystemComponent) { UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Health component for owner [%s] has already been initialized with an ability system."), *GetNameSafe(Owner)); return; } AbilitySystemComponent = InASC; if (!AbilitySystemComponent) { UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Cannot initialize health component for owner [%s] with NULL ability system."), *GetNameSafe(Owner)); return; } HealthSet = AbilitySystemComponent->GetSet(); if (!HealthSet) { UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Cannot initialize health component for owner [%s] with NULL health set on the ability system."), *GetNameSafe(Owner)); return; } // Register to listen for attribute changes. AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(ULyraHealthSet::GetHealthAttribute()).AddUObject(this, &ThisClass::HandleHealthChanged); AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(ULyraHealthSet::GetMaxHealthAttribute()).AddUObject(this, &ThisClass::HandleMaxHealthChanged); HealthSet->OnOutOfHealth.AddUObject(this, &ThisClass::HandleOutOfHealth); // TEMP: Reset attributes to default values. Eventually this will be driven by a spread sheet. AbilitySystemComponent->SetNumericAttributeBase(ULyraHealthSet::GetHealthAttribute(), HealthSet->GetMaxHealth()); ClearGameplayTags(); OnHealthChanged.Broadcast(this, HealthSet->GetHealth(), HealthSet->GetHealth(), nullptr); OnMaxHealthChanged.Broadcast(this, HealthSet->GetHealth(), HealthSet->GetHealth(), nullptr); //UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(GetOwner(), UGameFrameworkComponentManager::NAME_HealthComponentReady); } void ULyraHealthComponent::UninitializeFromAbilitySystem() { ClearGameplayTags(); if (HealthSet) { HealthSet->OnOutOfHealth.RemoveAll(this); } HealthSet = nullptr; AbilitySystemComponent = nullptr; } void ULyraHealthComponent::ClearGameplayTags() { if (AbilitySystemComponent) { const FLyraGameplayTags& GameplayTags = FLyraGameplayTags::Get(); AbilitySystemComponent->SetLooseGameplayTagCount(GameplayTags.Status_Death_Dying, 0); AbilitySystemComponent->SetLooseGameplayTagCount(GameplayTags.Status_Death_Dead, 0); } } float ULyraHealthComponent::GetHealth() const { return (HealthSet ? HealthSet->GetHealth() : 0.0f); } float ULyraHealthComponent::GetMaxHealth() const { return (HealthSet ? HealthSet->GetMaxHealth() : 0.0f); } float ULyraHealthComponent::GetHealthNormalized() const { if (HealthSet) { const float Health = HealthSet->GetHealth(); const float MaxHealth = HealthSet->GetMaxHealth(); return ((MaxHealth > 0.0f) ? (Health / MaxHealth) : 0.0f); } return 0.0f; } static AActor* GetInstigatorFromAttrChangeData(const FOnAttributeChangeData& ChangeData) { if (ChangeData.GEModData != nullptr) { const FGameplayEffectContextHandle& EffectContext = ChangeData.GEModData->EffectSpec.GetEffectContext(); return EffectContext.GetOriginalInstigator(); } return nullptr; } void ULyraHealthComponent::HandleHealthChanged(const FOnAttributeChangeData& ChangeData) { OnHealthChanged.Broadcast(this, ChangeData.OldValue, ChangeData.NewValue, GetInstigatorFromAttrChangeData(ChangeData)); } void ULyraHealthComponent::HandleMaxHealthChanged(const FOnAttributeChangeData& ChangeData) { OnMaxHealthChanged.Broadcast(this, ChangeData.OldValue, ChangeData.NewValue, GetInstigatorFromAttrChangeData(ChangeData)); } void ULyraHealthComponent::HandleOutOfHealth(AActor* DamageInstigator, AActor* DamageCauser, const FGameplayEffectSpec& DamageEffectSpec, float DamageMagnitude) { #if WITH_SERVER_CODE if (AbilitySystemComponent) { // Send the "GameplayEvent.Death" gameplay event through the owner's ability system. This can be used to trigger a death gameplay ability. { FGameplayEventData Payload; Payload.EventTag = FLyraGameplayTags::Get().GameplayEvent_Death; Payload.Instigator = DamageInstigator; Payload.Target = AbilitySystemComponent->GetAvatarActor(); Payload.OptionalObject = DamageEffectSpec.Def; Payload.ContextHandle = DamageEffectSpec.GetEffectContext(); Payload.InstigatorTags = *DamageEffectSpec.CapturedSourceTags.GetAggregatedTags(); Payload.TargetTags = *DamageEffectSpec.CapturedTargetTags.GetAggregatedTags(); Payload.EventMagnitude = DamageMagnitude; FScopedPredictionWindow NewScopedWindow(AbilitySystemComponent, true); AbilitySystemComponent->HandleGameplayEvent(Payload.EventTag, &Payload); } // Send a standardized verb message that other systems can observe { FLyraVerbMessage Message; Message.Verb = TAG_Lyra_Elimination_Message; Message.Instigator = DamageInstigator; Message.InstigatorTags = *DamageEffectSpec.CapturedSourceTags.GetAggregatedTags(); Message.Target = ULyraVerbMessageHelpers::GetPlayerStateFromObject(AbilitySystemComponent->GetAvatarActor()); Message.TargetTags = *DamageEffectSpec.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... UGameplayMessageSubsystem& MessageSystem = UGameplayMessageSubsystem::Get(GetWorld()); MessageSystem.BroadcastMessage(Message.Verb, Message); } //@TODO: assist messages (could compute from damage dealt elsewhere)? } #endif // #if WITH_SERVER_CODE } void ULyraHealthComponent::OnRep_DeathState(ELyraDeathState OldDeathState) { const ELyraDeathState NewDeathState = DeathState; // Revert the death state for now since we rely on StartDeath and FinishDeath to change it. DeathState = OldDeathState; if (OldDeathState > NewDeathState) { // The server is trying to set us back but we've already predicted past the server state. UE_LOG(LogLyra, Warning, TEXT("LyraHealthComponent: Predicted past server death state [%d] -> [%d] for owner [%s]."), (uint8)OldDeathState, (uint8)NewDeathState, *GetNameSafe(GetOwner())); return; } if (OldDeathState == ELyraDeathState::NotDead) { if (NewDeathState == ELyraDeathState::DeathStarted) { StartDeath(); } else if (NewDeathState == ELyraDeathState::DeathFinished) { StartDeath(); FinishDeath(); } else { UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Invalid death transition [%d] -> [%d] for owner [%s]."), (uint8)OldDeathState, (uint8)NewDeathState, *GetNameSafe(GetOwner())); } } else if (OldDeathState == ELyraDeathState::DeathStarted) { if (NewDeathState == ELyraDeathState::DeathFinished) { FinishDeath(); } else { UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Invalid death transition [%d] -> [%d] for owner [%s]."), (uint8)OldDeathState, (uint8)NewDeathState, *GetNameSafe(GetOwner())); } } ensureMsgf((DeathState == NewDeathState), TEXT("LyraHealthComponent: Death transition failed [%d] -> [%d] for owner [%s]."), (uint8)OldDeathState, (uint8)NewDeathState, *GetNameSafe(GetOwner())); } void ULyraHealthComponent::StartDeath() { if (DeathState != ELyraDeathState::NotDead) { return; } DeathState = ELyraDeathState::DeathStarted; if (AbilitySystemComponent) { AbilitySystemComponent->SetLooseGameplayTagCount(FLyraGameplayTags::Get().Status_Death_Dying, 1); } AActor* Owner = GetOwner(); check(Owner); OnDeathStarted.Broadcast(Owner); Owner->ForceNetUpdate(); } void ULyraHealthComponent::FinishDeath() { if (DeathState != ELyraDeathState::DeathStarted) { return; } DeathState = ELyraDeathState::DeathFinished; if (AbilitySystemComponent) { AbilitySystemComponent->SetLooseGameplayTagCount(FLyraGameplayTags::Get().Status_Death_Dead, 1); } AActor* Owner = GetOwner(); check(Owner); OnDeathFinished.Broadcast(Owner); Owner->ForceNetUpdate(); } void ULyraHealthComponent::DamageSelfDestruct(bool bFellOutOfWorld) { if ((DeathState == ELyraDeathState::NotDead) && AbilitySystemComponent) { const TSubclassOf DamageGE = ULyraAssetManager::GetSubclass(ULyraGameData::Get().DamageGameplayEffect_SetByCaller); if (!DamageGE) { UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: DamageSelfDestruct failed for owner [%s]. Unable to find gameplay effect [%s]."), *GetNameSafe(GetOwner()), *ULyraGameData::Get().DamageGameplayEffect_SetByCaller.GetAssetName()); return; } FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(DamageGE, 1.0f, AbilitySystemComponent->MakeEffectContext()); FGameplayEffectSpec* Spec = SpecHandle.Data.Get(); if (!Spec) { UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: DamageSelfDestruct failed for owner [%s]. Unable to make outgoing spec for [%s]."), *GetNameSafe(GetOwner()), *GetNameSafe(DamageGE)); return; } Spec->AddDynamicAssetTag(TAG_Gameplay_DamageSelfDestruct); if (bFellOutOfWorld) { Spec->AddDynamicAssetTag(TAG_Gameplay_FellOutOfWorld); } const float DamageAmount = GetMaxHealth(); Spec->SetSetByCallerMagnitude(FLyraGameplayTags::Get().SetByCaller_Damage, DamageAmount); AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*Spec); } }