Unreal GAS (31) - Enemy Hit React & Death

wnsduf0000·2025년 12월 1일

Unreal_GAS

목록 보기
32/34
  • 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가 작동하도록 함.

        • 이 때, GA_HitReact의 GameplayTag 설정에서 AbilityTags에 반드시 피격 반응을 위한 GameplayTag가 설정되어 있어야 작동됨.
          (FAuraGameplayTag에 설정한 피격 반응용 태그와 동일한 태그를 설정)

        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);
      	}
      }
      • AuraAttributeSet에 ShowFloatingText() 함수로 AuraPlayerController의 ShowDamageNumber() 함수를 호출하여 데미지 표시 위젯을 생성함.
      • AuraPlayerController의 ShowDamageNumber()의 작동 방식은, DamageTextComponent를 생성하고, 이를 일시적으로 타겟 액터에 Relative Transform으로 부착하였다가 Transform을 World 기준으로 유지하면서 때어냄으로서, 해당 위치에서 데미지 표시 위젯을 출력하는 것이다.
profile
저는 게임 개발자로 일하고 싶어요

0개의 댓글