Enemy Hit React
데미지를 입을 시 이에 반응하는 애니메이션을 재생하려 함.
GameplayAbility, GameplayEffect를 통해 피격 상태를 구현함.
피격 시 반응할 액터들에 대하여 (AuraCharacterBase 상속 클래스) GA_HitReact라는 이름의 GameplayAbility를 부여하도록 함.
(UAuraAbilitySystemLibrary에 GiveStartupAbilities() 함수를 추가하여, 이를 AuraEnemyCharacter의 BeginPlay() 등에서 호출하여 CharacterClassInfo DataAsset에 미리 설정해 둔 공통 GameplayAbility들을 부여하는 방식임.)
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CharacterClassInfo.generated.h"
class UGameplayEffect;
class UGameplayAbility;
UENUM(BlueprintType)
enum class ECharacterClass : uint8
{
Elementalist,
Warrior,
Ranger
};
USTRUCT(BlueprintType)
struct FCharacterClassDefaultInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
TSubclassOf<UGameplayEffect> PrimaryAttributes;
};
UCLASS()
class AURA_API UCharacterClassInfo : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category = "Character Class Defaults")
TMap<ECharacterClass, FCharacterClassDefaultInfo> CharacterClassInformation;
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TSubclassOf<UGameplayEffect> SecondaryAttributes;
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TSubclassOf<UGameplayEffect> VitalAttributes;
// '피격 시' 등의 공용 GameplayAbility를 담기 위한 TArray
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TArray<TSubclassOf<UGameplayAbility>> CommonAbilities;
FCharacterClassDefaultInfo GetClassDefaultInfo(ECharacterClass CharacterClass);
};
void UAuraAbilitySystemLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* AbilitySystemComponent)
{
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if (AuraGameMode == nullptr) return;
// AuraGameMode에 존재하는 CommonAbilities에서 공용 GameplayAbility를 캐릭터에 부여
UCharacterClassInfo* CharacterClassInfo = AuraGameMode->CharacterClassInfo;
for (auto AbilityClass : CharacterClassInfo->CommonAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
AbilitySystemComponent->GiveAbility(AbilitySpec);
}
}
GiveStartupAbilities()를 AuraEnemyCharacter의 BeginPlay() 등에서 호출하여,
액터가 준비될 시 공용 동작을 위한 GameplayAbility를 보유하도록 함.
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
FEffectProperties EffectProperties;
SetEffectProperties(Data, EffectProperties);
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
const bool bFatal = NewHealth <= 0.f;
if (!bFatal)
{
// 피격 반응용 태그와 반응하도록 TryActivateAbilitiesByTag() 호출
FGameplayTagContainer GameplayTagContainer;
GameplayTagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
EffectProperties.TargetASC->TryActivateAbilitiesByTag(GameplayTagContainer);
}
// Something to do when getting Fatal Damage
}
}
}
데미지 적용 시, 데미지가 적용되는 액터 (TargetActor)의 AbilitySystemComponent에서 TryActivateAbilitiesByTag()를 통해 GA_HitReact가 작동하도록 함.


class AURA_API ICombatInterface
{
GENERATED_BODY()
public:
virtual int32 GetPlayerLevel();
virtual FVector GetCombatSocketLocation();
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void UpdateFacingTarget(const FVector& FacingTargetLocation);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
UAnimMontage* GetHitReactAnimMontage();
};
캐릭터마다 피격 반응 애니메이션이 당연히 다르기 때문에, 피격 반응 AnimMontage를 얻어오기 위한 함수를 CombatInterface에 선언하고, 이를 AuraCharacterBase에서 구현하고, 각 캐릭터 블루프린트에서 피격 반응용 AnimMontage를 설정해주면 됨.
GA_HitReact는 AnimMontage를 재생하고, GE_HitReact를 적용함.
AnimMontage가 종료되거나 어떠한 이유로든 방해받으면 GE_HitReact를 제거하고 GA_HitReact를 종료시킴.

GE_HitReact는 피격 반응 태그를 대상 액터에게 적용함.
AuraEnemyCharacter 등 필요한 곳에 AbilitySystemComponent의 RegisterGameplayTagEvent()를 이용하여 태그가 변경될 때 적용할 함수를 선언하여 등록시키고, 이를 통해 이동을 일시적으로 불가능하게 하는 등의 효과를 낼 수 있음.
void AAuraEnemyCharacter::BeginPlay()
{
Super::BeginPlay();
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
InitAbilityActorInfo();
UAuraAbilitySystemLibrary::GiveStartupAbilities(this, AbilitySystemComponent);
/* HealthBar Widget Bindings Start */
if (UAuraUserWidget* HealthBar = Cast<UAuraUserWidget>(HealthBarWidget->GetUserWidgetObject()))
{
HealthBar->SetWidgetController(this);
}
if (const UAuraAttributeSet* AuraAS = Cast<UAuraAttributeSet>(AttributeSet))
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnHealthChanged.Broadcast(Data.NewValue);
});
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAS->GetMaxHealthAttribute()).AddLambda(
[this](const FOnAttributeChangeData& Data)
{
OnMaxHealthChanged.Broadcast(Data.NewValue);
});
AbilitySystemComponent->RegisterGameplayTagEvent(FAuraGameplayTags::Get().Effects_HitReact, EGameplayTagEventType::NewOrRemoved)
.AddUObject(this, &AAuraEnemyCharacter::HitReactTagChanged);
OnHealthChanged.Broadcast(AuraAS->GetHealth());
OnMaxHealthChanged.Broadcast(AuraAS->GetMaxHealth());
}
/* HealthBar Widget Bindings End */
}
void AAuraEnemyCharacter::HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
// HitReact 태그가 1개 이상인 경우, CharacterMovement의 WalkSpeed를 0으로 함으로서 일시적으로 이동 불가 상태로 만듬.
bHitReacting = NewCount > 0;
GetCharacterMovement()->MaxWalkSpeed = bHitReacting ? 0.f : BaseWalkSpeed;
}
캐릭터 사망 시, Ragdoll 처리를 위해 물리 처리를 켜고, 무기를 떨어뜨리게 한 다음, Dissolve 효과를 적용하여 시체가 사라지게 만드는 연출을 적용한다.
CombatInterface에 사망 시 호출될 함수 Die()를 선언하고 이를 구현한다.
// AuraCharacterBase.h
virtual void Die() override;
캐릭터 부모 클래스인 AuraCharacterBase()에서 Die()를 호출하되, 사망 시의 세부 로직은 Multicast 함수를 별도로 선언하여 해당 함수에 구현한다.
(그냥 Die()에서 처리하면 서버 측에서만 처리될 것이기 때문)
void AAuraCharacterBase::Die()
{
WeaponSkeletalMesh->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));
MulticastHandleDeath();
}
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
WeaponSkeletalMesh->SetSimulatePhysics(true);
WeaponSkeletalMesh->SetEnableGravity(true);
WeaponSkeletalMesh->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Dissolve();
}
void AAuraCharacterBase::Dissolve()
{
if (IsValid(DissolveMaterialInstance))
{
UMaterialInstanceDynamic* DynamicMaterialInstance = UMaterialInstanceDynamic::Create(DissolveMaterialInstance, this);
for (int i = 0; i < GetMesh()->GetNumMaterials(); i++)
{
GetMesh()->SetMaterial(i, DynamicMaterialInstance);
}
StartDissolveTimeline(DynamicMaterialInstance);
}
if (IsValid(WeaponDissolveMaterialInstance))
{
UMaterialInstanceDynamic* DynamicMaterialInstance = UMaterialInstanceDynamic::Create(WeaponDissolveMaterialInstance, this);
for (int i = 0; i < WeaponSkeletalMesh->GetNumMaterials(); i++)
{
WeaponSkeletalMesh->SetMaterial(i, DynamicMaterialInstance);
}
StartWeaponDissolveTimeline(DynamicMaterialInstance);
}
}
StartDissolveTimeline() 및 StartWeaponDissolveTimeline()은 타임라인 컴포넌트를 재생하여 Dynamic Material Instance의 수치를 부드럽게 조절하기 위한 것으로, 타임라인 자체는 블루프린트에서 생성하여 사용한다.

입힌 데미지 양이 출력되는 기능 적용 (WidgetComponent 사용)
#pragma once
#include "CoreMinimal.h"
#include "Components/WidgetComponent.h"
#include "DamageTextComponent.generated.h"
UCLASS()
class AURA_API UDamageTextComponent : public UWidgetComponent
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void SetDamageText(float Damage);
};
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// ...
if (LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
const bool bFatal = NewHealth <= 0.f;
if (bFatal)
{
ICombatInterface* CombatInterface = Cast<ICombatInterface>(EffectProperties.TargetAvatarActor);
if (CombatInterface)
{
CombatInterface->Die();
}
}
else
{
FGameplayTagContainer GameplayTagContainer;
GameplayTagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
EffectProperties.TargetASC->TryActivateAbilitiesByTag(GameplayTagContainer);
}
ShowFloatingText(EffectProperties, LocalIncomingDamage);
}
}
}
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& EffectProperties, float Damage)
{
if (EffectProperties.SourceCharacter != EffectProperties.TargetCharacter)
{
if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(EffectProperties.SourceCharacter, 0)))
{
AuraPlayerController->ShowDamageNumber(Damage, EffectProperties.TargetCharacter);
}
}
}
void AAuraPlayerController::ShowDamageNumber_Implementation(float DamageAmount, ACharacter* TargetCharacter)
{
if (IsValid(TargetCharacter) && DamageTextComponent)
{
UDamageTextComponent* DamageText = NewObject<UDamageTextComponent>(TargetCharacter, DamageTextComponent);
DamageText->RegisterComponent();
DamageText->AttachToComponent(TargetCharacter->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
DamageText->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
DamageText->SetDamageText(DamageAmount);
}
}