GameplayEffect 를 통해 데미지를 가할 때 단순히 Attributes 에 계산을 하는 것이 아니라 Meta Attribute 를 통한 계산을 진행한다.
예를 들어 캐릭터가 적에게 데미지 11을 가한다고 가정했을 때, 적에게도 방어, 크리티컬 방어 등등의 Attribute 가 존재하므로 이에 따른 계산이 필요하다.
그러므로 Meta Attribute 에 데미지 11의 값을 더한 다음, 여러 Attribute 에 따라 계산을 진행해 나온 값을 최종적으로 Attribute 에게 전달하여 체력을 감소시킨다.

Meta Attributes 를 생성하기 위해 AuraAttributeSet 클래스에 코드를 추가한다.
// AuraAttributeSet.h
...
public:
...
/*
* Vital Attributes
*/
...
/*
* Meta Attributes
*/
// Meta Attribute는 리플리케이트x
UPROPERTY(BlueprintReadOnly, Category = "Meta Attributes")
FGameplayAttributeData IncomingDamage;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, IncomingDamage);
...
// AuraAttributeSet.cpp
...
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
...
// Data의 Attribute가 IncomingDamage인 경우
if(Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
// 함수 내의 변수에 IncomingDamage를 할당하였으므로 기존의 IncomingDamage는 0으로 초기화 진행
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
// 데미지가 0 초과일 경우 체력 감소 효과 발생
if(LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
// 사망 판정용 bool타입 변수
const bool bFatal = NewHealth <= 0.f;
}
}
}
...
컴파일 후 GE_Damage 에서 Attribute : AuraAttributeSet.IncomingDamage 로 수정하고, Scalable Float Magnitude : 25 (함수 내에서 자체적으로 마이너스 계산을 하기 때문)로 변경한다.

컴파일 후 실행하면 25씩 체력이 감소하는 것을 확인할 수 있다.

이제 Scalable Float Magnitude 의 수치를 하드코딩하는 것이 아니라 Set by Caller 설정을 이용하여 코드를 통해 수치를 지정해준다.
Set by Caller 는 <Key, Value> 페어로 구성되어 있는데 Key : GameplayTag , Value : 수치 이다.
우선 GameplayTag 추가를 위해 코드를 작성한다.
// AuraGameplayTag.h
...
public:
...
FGameplayTag Damage;
// AuraGameplayTags.cpp
...
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
...
/*
* Input Tags
*/
GameplayTags.Damage = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Damage"), FString("Damage"));
}
이어서 투사체 발사 Ability에 태그를 적용시키기 위해서 AuraProjectileSpell 클래스에 코드를 추가한다.
// AuraProjectileSpell.cpp
...
#include "Aura/Public/AuraGameplayTags.h"
...
...
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
...
if (!bIsServer) return;
...
if (CombatInterface)
{
...
/** 코드 추가 */
FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
// Set by Caller 설정의 Magnitude 값을 설정(Key : GameplayTags.Damage, Value : 50.f)
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, 50.f);
/** 코드 추가 */
Projectile->DamageEffectSpecHandle = SpecHandle;
Projectile->FinishSpawning(SpawnTransform);
}
}
컴파일 후 GE_Damage 로 돌아와서 Magnitude Calculation Type : Set by Caller 로 변경하고, Data Tag : Damage 로 설정한다.

컴파일후 실행시 50의 데미지가 적용되는 것을 확인할 수 있다.

FScalableFloat 도 코드에 추가하여 해당 수치에 따라 CurveTable 을 적용하도록 한다.
// AuraGameplayAbility.h
...
public:
...
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Damage")
FScalableFloat Damage;
컴파일 후 GA_FireBolt 를 살펴보면 Damage 를 적용할 수 있다.
기존에 생성해둔 CT_PrimaryAttributes_Warrior.json 파일을 복사하여 CT_Damage로 rename하고 수치를 수정한다.


에디터로 돌아와서 CurveTable 을 생성할 경로로 간 다음

Import 를 통해 CT_Damage 를 임포트한다.



적당히 완만한 곡선을 이루도록 키를 움직거나 각을 조정한다.

GA_FireBolt 에서 데미지의 계수를 1로 지정해주고, Curve Table : CT_Damage 로 설정해준 다음, Select Curve : Abilities.FireBolt 로 설정한다.

AuraProjectileSpell 클래스로 돌아와서 레벨 수치에 따라 데미지가 적용되도록 코드를 수정한다.
(위 그림의 Damage : 1.0 은 FScalableFloat 에 속하는 계수이고, 아래 코드는 레벨에 따른 파이어볼트 데미지 증가 코드. 커브테이블에 있는 레벨에 따른 수치 * 계수로 데미지가 들어감)
// AuraProjectileSpell.cpp
#include "GameplayEffectExtension.h" // Damage.GetValueAtLevel(GetAbilityLevel()); 밑줄표시뜨면 include
...
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
...
if (!bIsServer) return;
...
if (CombatInterface)
{
...
FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
/** 코드 추가 */
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel());
GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Red, FString::Printf(TEXT("FireBolt Damage: %f"), ScaledDamage));
/** 코드 추가 */
/** 코드 수정 : 50.f -> ScaledDamage*/
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, ScaledDamage);
/** 코드 수정 */
Projectile->DamageEffectSpecHandle = SpecHandle;
Projectile->FinishSpawning(SpawnTransform);
}
}
컴파일 후 실행시 Level1 에 대한 데미지인 5가 적용되는 것을 디버그메세지를 통해 확인할 수 있다.

const float ScaledDamage = Damage.GetValueAtLevel(20);
으로 변경하면 20에 해당하는 수치만큼 데미지가 들어가는 것을 확인할 수 있다.


추가
/** 코드 수정 : const 키워드 추가, Damage.GetValueAtLevel(GetAbilityLevel()로 수정 */ const FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get(); const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel()); /** 코드 수정 */ /** 코드 삭제 */ // GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Red, FString::Printf(TEXT("FireBolt Damage: %f"), ScaledDamage)); /** 코드 삭제 */ UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, ScaledDamage); Projectile->DamageEffectSpecHandle = SpecHandle; Projectile->FinishSpawning(SpawnTransform);
태그 생성을 위해 GE_HitReact 를 생성한다.


Duration Policy : Infinite 로 설정하고

태그를 추가하기 위한 코드를 작성한다.
// AuraGameplayTags.h
public:
FGameplayTags Damage;
/** 코드 추가 */
FGameplayTag Effects_HitReact;
/** 코드 추가 */
...
void FAuraGameplayTags::InitializeNativeGameplayTags()
{
...
GameplayTags.Effects_HitReact = UGameplayTagsManager::Get().AddNativeGameplayTag(
FName("Effects.HitReact"), FString("Tag granted when Hit Reacting"));
}
GE_HitReact 로 돌아와서 Gameplay Effect -> Components 옆의 + 클릭 -> Target Tags Gameplay Effect Component -> Add Tags -> Add : Effects.HitReact 로 설정해준다.

해당 태그에 응답하여 HitReact 를 재생시키기 위해 코드를 작성한다.
( ASC 에 있는 GameplayTag 의 변경을 listen 하도록 해야함)
(델리게이트를 바인딩하여 태그 변경시 바인딩된 부분을 수행하도록함)
// AuraEnemy.h
...
public:
...
// Effects.HitReact 태그 생성 혹은 삭제시 실행될 바인딩 함수
void HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
// 피격 여부 bool 타입 변수
UPROPERTY(BlueprintReadOnly, Category = "Combat")
bool bHitReacting = false;
// 기본 이동 속도
UPROPERTY(BlueprintReadOnly, Category = "Combat")
float BaseWalkSpeed = 250.f;
// AuraEnemy.cpp
...
#include "AuraGameplayTags.h"
#include "GameFramework/CharacterMovementComponent.h"
...
...
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
...
if(UAuraUserWidget* AuraUserWidget = Cast<UAuraUserWidget>(HealthBar->GetUserWidgetObject()))
{
...
}
if(const UAuraAttributeSet* AuraAS = Cast<UAuraAttributeSet>(AttributeSet))
{
...
/*
* RegisterGameplayTagEvent() : 특정 GameplayTag가 추가되거나 제거될 때 이벤트를 등록할 수 있도록 함
* 해당 함수의 반환타입은 델리게이트이므로 AddUObject()를 통해 바인딩
* FOnGameplayEffectTagCountChanged& UAbilitySystemComponent::RegisterGameplayTagEvent(...)
* DECLARE_MULTICAST_DELEGATE_TwoParams(FOnGameplayEffectTagCountChanged, const FGameplayTag, int32);
*/
AbilitySystemComponent->RegisterGameplayTagEvent(FAuraGameplayTags::Get().Effects_HitReact, EGameplayTagEventType::NewOrRemoved).AddUObject(
this, &AAuraEnemy::HitReactTagChanged
);
...
}
}
void AAuraEnemy::HitReactTagChanged(const FGameplayTag CallbackTag, int32 Newcount)
{
// NewCount가 0 초과시 피격 발생을 의미
bHitReacting = NewCount > 0;
GetCharacterMovement()->MaxWalkSpeed = bHitReacting ? 0.f : BaseWalkSpeed;
}
...
컴파일 후 에디터로 돌아와서 GameplayAbility 기반 블루프린트 GA_HitReact 를 생성한다.


GA_HitReact 에서 GameplayEffect 를 적용하기 위해 ApplyGameplayEffectToOwner 노드를 생성한다.

적용할 GameplayEffect 인 GE_HitReact 를 적용시키고, Event ActivateAbility 노드와 연결한다.

애니메이션 재생이 끝나면 GameplayEffect 를 제거해야 하므로 Handle 이 필요하다.
ApplyGameplayEffectToOwner 노드는 GameplayEffectHandleStructure 를 리턴한다.

Return Value 를 변수로 승격시키고 Active GE Hit React 로 rename한다.

GetAvatarActorFromActorInfo() 함수를 통해 CombatInterface 로 캐스팅할 수 있다.
CombatInterface 를 통해 HitReactMontage 를 검색할 수 있고, 이를 통해 GameplayAbility 는 특정한 적에 대해 의존하지 않게 된다.
그러므로 HitReturnMontage 를 리턴하는 함수를 CombatInterface 클래스에 작성해야 한다.
// CombatInterface.h
...
class UAnimMontage;
...
...
public:
...
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
UAnimMontage* GetHitReactMontage();
// AuraCharacterBase.h
class UAnimMontage;
...
public:
...
virtual UAnimMontage* GetHitReactMontage_Implementation() override;
protected:
...
private:
...
UPROPERTY(EditAnywhere, Category = "Combat")
TObjectPtr<UAnimMontage> HitReactMontage;
// AuraCharacterBase.cpp
...
UAnimMontage* AAuraCharacterBase::GetHitReactMontage_Implementation()
{
return HitReactMontage;
}
...
컴파일 후 GA_HitReact 에서 GetAvatarActorFromActorInfo 노드를 통해 CombatInterface 로 캐스팅하고, GetHitReactMontage 노드를 추가하여 PlayMontageAndWait 노드와 연결시킨다.

HitReact_Spear 를 우클릭하여 Create -> Create AnimMontage 를 선택하여 AM_SHitReact_GoblinSpear 를 생성한다.


HitReactd_SlingShot 도 동일하게 AM_HitReact_GoblinSlingshot 을 생성한다.

BP_Goblin_Spear 에서 HitReactMontage : AM_HitReact_GoblinSpear 를, BP_Goblin_Slingshot 에서 HitReactMontage : AM_HitReact_GoblinSlingshot 을 적용시킨다.


몽타주를 재생시키기 위해서는 HitReactAbility 를 활성화시켜야 한다.
실행시키기 위해서는 AuraAttributeSet 클래스에 있는
const bool bFatal = NewHealth <= 0.f;
코드를 통해 구현할 수 있다.
해당 bool값을 통해 사망하지 않았다면, Ability 를 활성화하도록 코드를 작성해야 한다.
그전에 적들이 공유해야 하는 DataAsset 들이 있는 CharacterClassInfo 에서 Ability 를 추가해야한다.
// CharacterClassInfo.h
...
class UGameplayAbility;
...
...
public:
...
/** 코드 추가 */
// 적용시킬 GameplayAbility, 블루프린트에서 GA_HitReact 적용
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TArray<TSubclassOf<UGameplayAbility>> CommonAbilities;
/** 코드 추가 */
FCharacterClassDefaultInfo GetClassDefaultInfo(ECharacterClass CharacterClass);
이어서 Ability 를 적에게 사용할 수 있도록 하는 함수를 AuraAbilitySystemLibrary 클래스에 작성한다.
// AuraAbilitySystemLibrary.h
...
public:
...
// Ability를 적이 사용할 수 있도록 하는 함수(Ability 할당)
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|CharacterClassDefaults")
static void GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC);
// AuraAbilitySystemLibrary.cpp
...
void UAuraAbilitySystemLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC)
{
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if(AuraGameMode == nullptr) return;
UCharacterClassInfo* CharacterClassInfo = AuraGameMode->CharacterClassInfo;
// Ability가 저장된 TArray 배열을 순회하면서 Spec을 생성하고 전달해서 Ability 활성화
for(TSubclassOf<UGameplayAbility> AbilityClass : CharacterClassInfo->CommonAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
ASC->GiveAbility(AbilitySpec);
}
}
AuraEnemy 의 BeginPlay() 에서 Ability 를 사용가능하도록 하는 코드를 추가하여 적이 Ability 를 소유하도록 한다.
// AuraEnemy.cpp
...
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
...
/** 코드 추가 */
//
// Ability를 적이 사용할 수 있도록 하는 함수(Ability 할당)
UAuraAbilitySystemLibrary::GiveStartupAbilities(this, AbilitySystemComponent);
/** 코드 추가 */
if(UAuraUserWidget* AuraUserWidget = Cast<UAuraUserWidget>(HealthBar->GetUserWidgetObject())
{
...
}
}
...
이제 bFatal 에 따라 Ability 를 활성화하도록 코드를 추가한다.
// AuraAttributeSet.cpp
...
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
...
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
...
if (LocalIncomingDamage > 0.f)
{
...
const bool bFatal = NewHealth <= 0.f;
if(!bFatal)
{
// TagContainer를 생성하고, FAuraGameplayTags에서 Effects_HitReact를 가져와 컨테이너에 할당
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
// 적용 대상의 ASC에서 해당 태그를 보유중일 경우 Ability Activate
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
}
}
}
}
...
컴파일 후 DA_CharacterClassInfo 에서 Common Abilities : GA_HitReact 로 설정하여 적이 Ability를 발동할 수 있도록 한다.

그리고 GameplayAbility 가 태그를 가지게 하기 위해 GA_HitReact 에서 Tag -> Ability Tags : Effects.HitReact 로 설정해준다.

이제 태그를 TagContainer 에 할당하여 해당 태그에 맞는 Ability 를 활성화할 수 있다.
컴파일 후 실행하면 적이 파이어볼트에 피격시, 애니메이션이 재생되는 것을 확인할 수 있다.

추가
AM_HitReact_GoblinSpear,AM_HitReact_GoblinSlingshot의Blend in과Blend out시간을 0.05초로 조정
해당 능력은 피격마다 계속 적용되어야 하므로 능력을 종료하지 않는다.
그러므로 Instancing Policy : Instance Per Actor 로 설정을 변경하여, 처음 활성화할 때에만 인스턴스화( GameplayAbility 를 객체화하여 소유)하고 이후의 활성에 대해서는 동일한 인스턴스를 사용하도록 한다.

GA_HitReact 는 여러 액터( Goblin_Spear , Goblin_Slingshot 이 사용)가 사용하면, 각 액터마다 별도의 인스턴스가 존재함.이제 몽타주가 끝나면 GameplayEffect를 제거하도록 해야 한다.
On Completed , On Interrupted , On Cancelled 핀과 RemoveGameplayEffectFromOwnerWithHandle 노드를 연결하고, 이전에 변수로 승격시켜준 Active GE Hit React 를 Handle 핀과 연결한다.
(몽타주가 끝나거나, 몽타중 재생중 경직과 같은 판정에 의해 방해받거나, 모종의 이유로 캔슬될 경우)

최종적으로 End Ability 노드와 연결하여 Ability 를 종료시킨다.

컴파일 후 실행하면 몽타주 재생이 끝나기 전까지는 경직 애니메이션이 발생하지 않는다.
(재생이 끝나야 End Ability 가 발동되어 끝나고, 다시 Ability 를 활성화시켜 몽타주 재생이 가능하기 때문)
사망 관련 기능은 캐릭터와 적 둘다 필요하므로 먼저 CombatInterface 클래스에 함수를 선언한다.
// CombatInterface.h
...
public:
...
virtual void Die() = 0;
먼저 AuraCharacterBase 클래스에서 함수를 구현하고, 서버와 클라이언트 양쪽에서 레그돌 기능을 실행하도록 코드를 작성한다.
// AuraCharacterBase.h
...
public:
...
virtual void Die() override;
// MulticastRPC 함수
// NetMulticast: 서버가 모든 클라이언트에게 함수를 호출하도록 함, Reilable: 함수 호출이 반드시 목적지에 도달호도록 보장
UFUNCTION(NetMulticast, Reliable)
virtual void MulticastHandleDeath();
// AuraCharacterBase.cpp
...
void AAuraCharacterBase::Die()
{
// 서버에서 자동으로 해당 액션을 리플리케이트하므로 클라이언트측에서 detach할 필요x
Weapon->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));
MulticastHandleDeath();
}
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
Weapon->SetSimulatePhysics(true);
Weapon->SetEnableGravity(true);
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetEnableGravity(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
// WorldStatics: 벽, 바닥 등과 같은 것들
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
적 클래스에도 함수를 선언하고 override하여 해당 기능을 사용하도록 한다.
대신 적의 경우 LifeSpan 을 설정하여 사망후 일정 시간이 지나면 사라지도록 한다.
// AuraEnemy.h
...
public:
...
/** Combat Interface */
...
virtual void Die() override;
/** Combat Interface */
...
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat")
float LifeSpan = 5.f;
// AuraEnemy.h
...
void AAuraEnemy::Die(LifeSpan);
{
SetLifeSpan(LifeSpan);
Super::Die();
}
마지막으로 AuraAttributeSet 클래스에서 bFatal 이 참일 경우에 대해 사망 관련 기능을 수행하도록 코드를 작성한다.
// AuraAttributeSet.cpp
...
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
...
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
...
}
if(Data.EvaluatedData.Attribute == GetManaAttribute())
{
...
}
if(Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
...
if (LocalIncomingDamage > 0.f)
{
...
/** 코드 추가 및 수정 */
if (bFatal)
{
/** 코드 추가 */
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if(CombatInterface)
{
// 실제로는 인터페이스의 Die() 가 아닌 각 클래스의 구현부에 있는 Die() 호출됨(폴리모르피즘)
CombatInterface->Die();
}
/** 코드 추가 */
}
else
{
/** 코드 이동 */
// 기존에 if(!bFatal) 내에 있던 코드 이동
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FAuraGameplayTags::Get().Effects_HitReact);
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
/** 코드 이동 */
}
/** 코드 수정 */
}
}
}
마지막으로 적을 빠르게 죽이기 위해 AbilityLevel 값을 하드코딩하여 전해주고 컴파일 후 실행한다.
// AuraProjectileSpell.cpp
...
void UAuraProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{
...
if (!bIsServer) return;
...
if (CombatInterface)
{
...
const float ScaledDamage = Damage.GetValueAtLevel(10);
...
}
}
컴파일 후 실행하면 정상적으로 동작하는 것을 확인할 수 있다.

적 사망시 리얼리티를 살리기 위해 사르르 녹아 없어지는 이펙트를 추가한다.

미리보기
MI_GoblinDissolve파일 오픈
콘텐츠 브라우저를 열고 적용시킬 스켈레톤 메시 선택
우측 하단의 벽돌 모양 아이콘 클릭
미리보기 가능
Details패널의Dissolve수치 조정을 통해 사라지는 모습 전체적으로 확인 가능
또는Details탭의Preview Mesh를 통해 미리보기 가능
Dissolve Material Instance 를 적용시키기 위해 코드를 작성한다.
// AuraCharacterBase.h
...
protected:
...
/* Dissolve Effects */
void Dissolve();
// UMaterialInstanceDynamic : 런타임동안 동적으로 material 변경하도록 해줌
UFUNCTION(BlueprintImplementableEvent)
void StartDissolveTimeline(UMaterialInstanceDynamic* DynamicMaterialInstance);
UFUNCTION(BlueprintImplementableEvent)
void StartWeaponDissolveTimeline(UMaterialInstanceDynamic* DynamicMaterialInstance);
// 캐릭터 Mesh Dissolve용
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TObjectPtr<UMaterialInstance> DissolveMaterialInstance;
// 캐릭터 무기 Dissolve용
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TObjectPtr<UMaterialInstance> WeaponDissolveMaterialInstance;
// AuraCharacterBase.cpp
...
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
...
Dissolve();
}
...
void AAuraCharacterBase::Dissolve()
{
// MaterialInstance 유효성 검사후 동적 매테리얼 생성하고
// 메시의 0번째 인덱스 매테리얼 변경후
// StartDissolveTimeline 함수 실행(Dissolve수치 변경)
if(IsValid(DissolveMaterialInstance))
{
UMaterialInstanceDynamic* DynamicMatInst = UMaterialInstanceDynamic::Create(DissolveMaterialInstance, this);
GetMesh()->SetMaterial(0, DynamicMatInst);
StartDissolveTimeline(DynamicMatInst);
}
if(IsValid(WeaponDissolveMaterialInstance))
{
UMaterialInstanceDynamic* DynamicMatInst = UMaterialInstanceDynamic::Create(WeaponDissolveMaterialInstance, this);
Weapon->SetMaterial(0, DynamicMatInst);
StartWeaponDissolveTimeline(DynamicMatInst);
}
}
컴파일 후 BP_Enemy 의 Event Graph 에서 StartDeissolveTimeline 노드를 생성하고
Add Timeline 을 검색하여 노드를 생성한 후, DissolveTimeline으로 rename하고 두 노드를 연결시킨다.

DissolveTimeline 노드를 더블클릭하고 +Track -> Add Float Track 통해 트랙을 추가하고, DissolveTrack 으로 rename한다.

트랙에 우클릭을 하여 Add Key to CurveFloat_0 을 클릭하여 키를 생성하고

키를 생성하고 Auto를 통해 리니어하게 변경한다.
( x : 0, y : -0.1 ) , ( x : 3 , y : 0.55 )

해당 과정을 StartWeaponDissolveTimline 노드도 생성하여 동일하게 진행한다.

수치 확인이 필요하면
Material Instance에서Dissolve수치를 조절하여 어느 수치에 어느 시점에서 Dissolve가 완벽하게 되는지, 어느 수치가 Dissolve 적용되기 전인지 확인
Event Start Dissolve Timeline 노드의 Input 핀에서 Set Scalar Parameter Value 노드를 생성하여 연결하고, 나머지 노드도 연결해준다.
이때 수치를 변경할 Parameter Name : Dissolve 로 반드시 설정해주어야 한다.

BP_Goblin_Spear 와 BP_Goblin_Slingshot 에서 각 매테리얼을 적용시켜주고


실행하면 정상적으로 동작하는 것을 확인할 수 있다.

데미지 표시를 위한 위젯을 생성해야 한다.
UserWidget 기반 블루프린트 WBP_DamageText 를 생성한다.



WBP_DamageText 파일을 열고 텍스트 표시를 위한 Overlay 와 Text 를 추가한 다음

텍스트의 IsVariable 을 활성화한다.

Text_Damage 의 사이즈를 Custom 으로 설정가능하도록 변경한 다음

값 확인을 위해 적당한 아무 수나 입력한다.

중앙정렬을 시키고

Font : Amarante-Regular_Font, Size : 26 , Outline Size : 1 으로 설정을 변경한다.

이제 글자가 생성되고 사라지는 애니메이션을 추가해야 한다.
Animations 를 눌러 애니메이션 탭을 열고 + Animation 을 눌러 DamageAnim 애니메이션을 생성한다.


+ Track -> Text_Damgae 를 선택하여 트랜스폼을 적용시킬 대상의 트랙을 생성한다.

TODO: chap.140/03:29트랜스폼 추가부터 시작
Text_Damage 옆의 + 아이콘을 클릭하고 Transform 을 추가한다.

Text_Damage -> Transfrom -> Translation -> y : -84 로 설정하여 시작 지점이 기본보다 84만큼 위에서 시작하도록 한다.

그리고 0.2초 지점에서 x : 90 으로 설정하여 0.2초 뒤에 우측으로 90만큼 이동하도록 한다.

이어서 아래의 표를 따라 각 시간에 대해 텍스트가 이동하도록 수치를 설정해준다.
| sec | x | y |
|---|---|---|
| 0 | 0 | -84 |
| 0.2 | 90 | -84 |
| 0.4 | 112 | -94 |
| 0.7 | 88 | -220 |
| 1 | -100 | -300 |



시간에 따라 텍스트의 scale 변경을 위해 Text_Damage -> Transform -> Scale 를 통해 수치를 적절히 설정해준다.
| sec | x | y |
|---|---|---|
| 0 | 1.0 | 1.0 |
| 0.1 | 2.25 | 2.25 |
| 0.2 | 1.0 | 1.0 |
| 1 | 0.0 | 0.0 |



마지막으로 시간에 따라 텍스트의 투명도 조절을 위해 Text_Damage 옆의 + 아이콘을 클릭하고 Render Opacity 를 선택하고 0sec에서 Render Opacity : 1 , 1sec 도달시 Render Opacity : 0 이 되도록 설정해준다.


설정을 끝마쳤다면 Graph 탭에서 DamageAnim 노드를 생성해 Play Animation 노드와 연결하고, 실행핀을 Event Construct 노드와 연결하여 애니메이션을 재생하도록 노드를 구성해준다.

이제 위젯이 뷰포트에서 표시되도록 추가해야 한다.
그러기 위해선 적에게 Widget Component 를 부착시켜주고, 데미지를 받을 때 위젯이 나타나도록 해야한다.
먼저 WidgetComponent 기반의 DamageTextComponent 클래스를 추가한다.



// DamageTextComponent.h
...
public:
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void SetDamageText(float Damage);
컴파일 후 에디터로 돌아와서 DamageTextComponent 기반 BP_DamageTextComponent 블루프린트를 생성한다.


파일을 열면 Details 패널에서 Widget Class 를 설정해 줄 수 있다.
Widget Class : WBP_DamageText 로 설정해주고

Event Graph 탭에서 Event Set Damage Text 이벤트 노드를 생성하고
Get User Widget Object 노드를 생성하여 Cast To WBP_DamageText 로 캐스팅을 진행한다.

BP_DamageText 로 돌아와서 UpdateDamageText 함수를 추가하고, float 타입의 Input : Damage 를 추가한다.



Text_Damage 노드를 통해 Set Text(Text) 노드를 생성하여 연결하고, 실행핀을 Update Damage Text 노드와 연결시킨 다음, Damage 핀에서 To Text(Float) 노드를 생성하여 Set Text(Text) 노드의 In Text 핀과 연결한다.

To Text(Float) 노드에 있는 Maximum Fractional Digits : 0 으로 설정해준다(소숫점 사용x).

다시 BP_DamageTextComponent 의 Event Graph 로 돌아와, 캐스팅 이후 방금 만든 함수인 Update Damage Text 노드를 생성하여 연결시키고, Event Set Damage Text 노드의 Damage 핀과 연결시킨다.

이제 위젯 컴포넌트를 뷰포트에 출력시키기 위한 코드를 작성해야 한다.
먼저 데미지를 표시하는 기능을 가진 함수를 생성한다.
// AuraPlayerController.h
...
class UDamageTextComponent;
...
...
public:
...
// 서버가 캐릭터 컨트롤시 서버에서 함수를 실행시키고 서버측에서 데미지 보임
// 클라이언트가 캐릭터 컨트롤시 서버에서 함수를 실행시키지만 클라이언트측에서 데미지 보임
UFUNCTION(Client, Reliable)
void ShowDamageNumber(float DamageAmount, ACharacter* TargetCharacter);
protected:
...
private:
...
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UDamageTextComponent> DamageTextComponentClass;
// AuraPlayerController.cpp
...
#include "GameFramework/Character.h"
#include "UI/Widget/DamageTextComponent.h"
...
...
void AAuraPlayerController::ShowDamageNumber_Implementation(float DamageAmount, ACharacter* TargetCharacter)
{
// Target과 클래스 유효성 검사
if(IsValid(TargetCharacter) && DamageTextComponentClass)
{
// DamageText생성
UDamageTextComponent* DamageText = NewObject<UDamageTextComponent>(TargetCharacter, DamageTextComponentClass);
// RegisterComponent() : 컴포넌트를 현재 월드에 등록, 컴포넌트, 생성 후 월드에서 동작하도록 하게끔 하기 위해 호출
DamageText->RegisterComponent();
DamageText->AttachToComponent(TargetCharacter->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
DamageText->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
DamageText->SetDamageText(DamageAmount);
}
}
이어서 AuraAttributeSet 클래스에 데미지를 출력하도록 코드를 이어서 작성한다.
// AuraAttributeSet.h
...
private:
...
void ShowFloatingText(const FEffectProperties& Props, float Damage) const;
#include "..."
#include "Player/AuraPlayerController.h"
...
void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
...
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
...
}
if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
...
}
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
...
if (LocalIncomingDamage > 0.f)
{
...
if (bFatal)
{
...
if (CombatInterface)
{
...
}
}
else
{
...
}
// Damage가 0 초과일 경우에 텍스트를 출력해야 되므로 여기에 코드 작성
ShowFloatingDamage(Props, LocalIncomingDamage);
}
}
}
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, float Damage) const
{
// 자기 자신에게 데미지를 가하는게 아닌 경우에만 출력하도록 함
if(Props.SourceCharacter != Props.TargetCharacter)
{
if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
{
PC->ShowDamageNumber(Damage, Props.TargetCharacter);
}
}
}
...
컴파일 후 BP_PlayerController 에서 Damage TextComponent Class : BP_DamageTextComponent 로 설정하고

BP_DamageTextComponent 에서 Delay 노드를 추가해 1초의 딜레이를 준 후 Destroy Component 노드를 이용해 파괴한다.

그리고 User Interface -> Space : Screen 으로 변경한다.

컴파일 후 실행시 데미지 텍스트가 생기는 것을 확인할 수 있다.
(Client측에서 출력 안되는건 현재 정상임. 추후에 해당 오류 수정 예정)

추가
1.WBP_DamageText에서DamageAnim의 스케일 수정
sec x y 0 0.44 0.44 0.1 1.0 1.0 0.2 0.44 0.44 1 0 0
Font->Size : 58수정Font->Outline Settings->Outline Size : 2수정Font->Letter Spacing : 100수정
이제 실제로 적이 받는 데미지를 전달해야 한다.
UGameplayEffectExecutionCalculation 클래스는 캡쳐된 Attribute 의 값에 대한 계산을 진행한다.
UGameplayEffectExecutionCalculation
Attribute캡쳐- 하나 이상의
Attribute에 대해 캡쳐 가능- 어떤
Programmer Logic도 할 수 있음(유연성 보유)- 예측x(예측을 통한 데미지 계산x)
즉Instant또는Periodic GameplayEffect에만 적용됨- 캡쳐링은
PreAttributeChange을 실행하지 않고,
그곳에서 수행된Clamping은 반드시 그곳에서 다시 수행되어야 함.
원문 : Captureing doesn't runPreAtributeChange: any clamping done there must be done again.- 로컬 예측, 서버 시작, 그리고 서버 전용
Net Execution Policies가 있는Gameplay Abilities에 대해서만 서버에서 실행됨.
원문 : Only executed on the Server from Gameplay Abilities with Local Predicted, Server Initiated, and Server Only Net Execution Policies.- Attribute Capturing 진행시 [Snapshot] 여부 선택 가능
이는 효과를 적용하는 주체인 Source로부터 [Attribute]를 캡쳐할때만 중요하며, Target은 중요하지 않음
원문 : And this really only matters when we're capturing attributes from the source, the thing that's applying the effect, not the target.Snapshot은 GamepayEffectSpec이 생성될 때Attribute의 값을 캡쳐함.
즉 Execution Calcultaion은 GameplayEffect에 의해 사용되고 있고,Spec이 생성되었을 때Attribute는Snapshot될 준비가 된 상태가 됨
EX) 파이어볼트를 스폰할 때, 파이어볼트는 자기 자신에게 적용되는 것이 아닌 무언가를 타격하기까지 기다림. 타격에 대한GameplayEffecdtSpec이 적용될 때, 여기서 Snapshot을 통해 파이어볼트의 데미지값을 캡쳐한다.
먼저 Custom Calculation Class 생성을 해야 한다.
GameplayEffectExecutionCalculation 클래스 기반 ExecCalc_Damage 클래스를 생성한다.



// ExecCalc_Damage.h
public:
UExecCalc_Damage();
// ExecCalc_Damage 클래스 호출시 실행될 함수
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
#include "..."
#include "AbilitySystemComponent.h"
#include "AbilitySystem/AuraAttributeSet.h"
// Capture Definitaion을 위한 구조체
// prefix F 없는 이유 : low struct이기 때문(블루프린트에 reflect를 하지 않으므로 굳이 F prefix 필요없음
struct AuraDamageStatics
{
/*
* #define DECLARE_ATTRIBUTE_CAPTUREDEF(P) \
* FProperty* P##Property; \
* FGameplayEffectAttributeCaptureDefinition P##Def; \
*/
// Attribute Capture 매크로
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
AuraDamageStatics()
{
/// MMC_MaxHealth, MMC_MaxMana의 생성자에 있는 코드를 하나의 매크로를 이용해 나타낸 코드
/*
* #define DECLARE_ATTRIBUTE_CAPTUREDEF(S, P, T, B) \
* { \
* P##Property = FindFieldChecked<FProperty>(S::StaticClass(), GET_MEMBER_NAME_CHECKED(S, P)); \
* P##Def = FGameplayEffectAttributeCaptureDefinition(P##Property, EGameplayEffectAttributeCaptureSource::T, B); \
* }
*/
// Attribute가 선언된 클래스, Attribute명, 계산 적용 대상(적 방어력에 따라 가해지는 데미지 계산이므로 Target), Snapshot 여부
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
}
};
// 전역함수, AuraDamageStatics 구조체 DStatics 생성후 리턴
static const AuraDamageStatics& DamageStatics()
{
static AuraDamageStatics DStatics;
return DStatics;
}
UExecCalc_Damage::UExecCalc_Damage()
{
// Calculation과 관련된 Attribute를 캡쳐
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
}
void Execute_Implementation(const FGamplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
// FGameplayEffectdCustomExecutionParameters : GAS에서 사용되는 구조체로써, Custom Execution을 수행하는 동안 GameplayEffect의 실행 파라미터를 캡쳐하고 전달하는 역할
// 주요 구성 요소로는 Source/Target Tag, Source/Target ASC, GetOwningSpec() 등이 있음
const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
const AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
const AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
// GetAggregatedTags : Spec과 태그 combination 반환
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
// Attribute Value를 평가하거나 계산할 때, 계산이 어떤 방식으로 이루어져야 하는지에 대한 정보를 제공
// 주요 구성 요소로는 Source/Target Tags, IncludePredictiveMods, Bias, SanpToInterval 등이 있음
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvuationParameters.TargetTags = TargetTags;
float Armor = 0.f;
// AttemptCalculateCapturedAttributeMagnitude : 특정 Attribute의 Magnitude를 계산하려 할 때 사용
// 파라미터로 CaptureDefinition(위에서 선언한 구조체), FAggregatorEvaluateParameters(속성 값을 계산할 때 사용), 계산된 크기 할당
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, Armor);
Armor = FMath::Max<float>(0.f, Armor);
++Armor;
// Modifier가 평가된 결과 데이터를 저장하는 구조체, Property에 설정한 연산을 통해 Armor값을 적용한 결과를 포함
// 파라미터 : Property, ModOp, 계산된 Attribute 값
const FGameplayModifierEvaluatedData EvaluatedData(DamageStatics().ArmorProperty, EGameplayModOp::Additive, Armor);
// 최종적으로 GameplayEffect에 결과 반영
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
컴파일 후 GE_Damage 에서 Executions -> + -> Calculation Class : ExecCalc_Damage 로 설정해준다.

코드의 Execute_Implementation() 함수의 끝부분에 breakpoint를 걸어두고

컴파일 후 실행하여 적을 공격하면 Armor 수치가 증가하는 것을 확인할 수 있다.



이제 ExecCalc_Damage 클래스가 어떻게 구성되는지 알았으니 적에게 데미지를 주는 기능을 Modifiers 가 아닌 Execution 을 통해 실행하도록 수정한다.
먼저 GE_Damage 의 Modifier 를 삭제한다.

이어서 ExecCalc_Damage 클래스에 코드를 추가한다.
#include "..."
#include "AuraGameplayTags.h"
...
void Execute_Implementation(const FGamplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
...
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
/** 코드 추가 */
// 태그를 통해 value를 불러와 Damage에 할당
float Damage = Spec.GetSetByCallerMagnitude(FAuraGameplayTags::Get().Damage);
/** 코드 추가 */
/** 코드 삭제 */
// float Armor = 0.f;
// ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvalutationParamters, Armor);
// Armor = FMath::Max<float>(0.f, Armor);
// ++Armor;
/** 코드 삭제 */
/** 코드 수정 */
// Modifier가 평가된 결과 데이터를 저장하는 구조체, Property에 설정한 연산을 통해 Armor값을 적용한 결과를 포함
// 파라미터 : Property, ModOp, 계산된 Attribute 값
const FGameplayModifierEvaluatedData EvaluatedData(UAuraAttributeSet::GetIncomingDamageAttribute(), EGameplayModOp::Addtive, Damage);
/** 코드 수정 */
OutExecutionOutput.AddOutputModifier(EvaluatedData);
}
...
컴파일 후 실행하면 정상적으로 데미지가 들어가는 것을 확인할 수 있다.

이제 BlockChance 를 구현하기 위한 코드를 추가한다.
#include "..."
struct AuraDamageStatics
{
...
DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
AuraDamageStatics()
{
...
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, BlockChance, Target, false);
}
};
...
UExecCalc_Damage::UExecCalcDamage()
{
...
RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
}
void Execute_Implementation(const FGamplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
...
float Damage = ...;
/** 코드 추가 */
// Capture BlockChance on Target, and determine if there was a successful block
// If Block, halve the damage
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters, TargetBlockChance);
TargetBlockChance = FMath::Max<float>(TargetBlockChance, 0.f);
const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
Damage = bBlocked ? Damage / 2.f : Damage;
/** 코드 추가 */
const FGameplayModifierEvaluatedData EvaluatedData(...);
...
}
...
컴파일 후 실행하면 확률적으로 16이 들어가던 데미지가 8로 들어가는 것을 확인할 수 있다.

추가
BlockChance를 100%로 헤서 정상적으로 동작하는지 확인하고 싶다면GE_SecondaryAttributes_Enemy 파일 GameplayEffect -> Modifiers -> BlockChance -> Modifier Magnitude 에서 Magnitude Calculation Type : Scalable Float 변경 및 수치 100 설정
매번GE_SecondaryAttributes_Enemy에서AuraAttributes.BlockChance설정을 수정하는게 귀찮다면
GE_SecondaryAttributes_Enemy를 복제한GE_SecondaryAttributes_TEST생성 후,GE_SecondaryAttributes_TEST에서BlockChance를 위와 동일하게 직접 수치조정 가능하도록 설정하고
DA_CharacterClassInfo에서Secondary Attributes : GE_SecondaryAttributes_Test로 변경만 해주면 된다.
Armor 와 ArmorPenetration 도 추가한다.
BlockChance 를 통해 먼저 받는 데미지를 반으로 줄인 다음, 해당 수치에 대해 Armor 와 ArmorPenetration 을 적용시킨다.
#include "..."
struct AuraDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
/** 코드 추가 */
DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
/** 코드 추가 */
DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
AuraDamageStatics()
{
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, Armor, Target, false);
/** 코드 추가 */
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, ArmorPenetration, Source, false);
/** 코드 추가 */
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, BlockChance, Target, false)
}
};
...
UExecCalc_Damage::UExecCalcDamage()
{
...
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
/** 코드 추가 */
RelevantAttributesToCapture.Add(DamageStatics().ArmorPenetrationDef);
/** 코드 추가 */
RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
}
void Execute_Implementation(const FGamplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
...
Damage = bBlocked ? Damage / 2.f : Damage;
/** 코드 추가 */
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, TargetArmor);
TargetArmor = FMath::Max<float>(TargetArmor, 0.f);
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef, EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f);
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * 0.25f) / 100.f;
Damage *= (100 - EffectiveArmor * 0.333f) / 100.f;
/** 코드 추가 */
...
}
컴파일 후 실행시 정상적으로 Armor 와 ArmorPenetration 이 적용되는 것을 확인할 수 있다.
(원래 16의 데미지가 발생해야 하지만 Armor 와 ArmorPenetration 이 적용되어 15의 데미지 발생)

GE_SecondaryAttributes_TEST 에서 동일하게 Magnitude Calculation Type : Scalable Float 로 변경하고 Scalable Magnitude Float 수치를 원하는 대로 변경한 다음 실행하면 다양한 다른 수치에서 어떻게 적용되는지 확인할 수 있다.
1. Armor 수치 45로 변경(아직 BlockChance: 100)

2. BlockChance 에 의해 8로 변경된 데미지와 Armor 수치 변경으로 인해 들어오는 데미지 변경

3. Armor 수치 90으로 변경

4. Armor 수치 250으로 변경

추가
추후 원할한 테스트를 위해Armor수치를 150으로 변경
chap.147
코드상에 계수를 하드코딩하는 부분이 있는데, 해당 부분을 수정한다.

DA_CharacterClassInfo 에서 공통적으로 관리하기 때문에 이곳에 관련된 값들을 설정하여 적용시킬 수 있도록 한다.

먼저 계수 관련된 CurveTable 블루프린트 CT_DamageCalculationCoefficients 을 생성한다.
생성경로: Blueprints/AbilitySystem/Data


CT_DamageCalculationCoefficients 에서 ArmorPenetration 을 추가하고 아래의 테이블에 따라 수치를 설정한다.
| key | value |
|---|---|
| 1 | 0.25 |
| 10 | 0.15 |
| 20 | 0.085 |
| 40 | 0.035 |


EffectArmor 또한 추가한 다음 아래의 테이블에 따라 수치를 설정한다.
| key | value |
|---|---|
| 1 | 0.333 |
| 10 | 0.25 |
| 20 | 0.15 |
| 40 | 0.085 |


두 커브를 DA_CharacterClassInfo 에서 사용할 수 있도록 CharacterClassInfo 클래스에 코드를 추가한다.
#include "..."
...
UCLASS()
class AURA_API UCharacterClassInfo : public UDataAsset
{
GENERATED_BODY()
public:
...
/** 코드 추가 */
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults|Damage")
TObjectPtr<UCurveTable> DamageCalculationCoefficients;
/** 코드 추가 */
FCharacterClassDefaultsInfo GetClassDefaultInfo(ECharacterClass CharacterClass);
};
DA_CharacterClassInfo 를 열고 DamageCalculationCoefficients : CT_DamageCalculationCoefficients 로 설정한다.

추가 여기서부터 10.3.6챕터 전까지 오류 발생
일단 따로 선언안하고 직접 호출해서 사용
직접호출 코드
- ExecCalc_Damage.cpp
// 필요한 헤더파일 확인되면 작성 #include "..." ... void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const { ... /** 코드 수정 : const 삭제 */ AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr; AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr; /** 코드 수정 */ /** 코드 추가 */ // CombatInterface에 있는 레벨 리턴 함수 사용을 위함 ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar); ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar); /** 코드 추가 */ ... SourceArmorPenetration = FMath::Max<float>(SourceArmorPenetration, 0.f); /** 코드 추가 */ // GameMode 직접 캐스팅하여 사용 const AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(SourceAvatar)); if (AuraGameMode == nullptr) return; // AuraGameMode->CharacterClassInfo; /* chap.10.3.5 커브테이블 생성 및 적용 이후 오류 생겨서 임시로 직접 캐스팅후 진행, 해결방법 확인되면 수정 */ const UCharacterClassInfo* CharacterClassInfo = AuraGameMode- >CharacterClassInfo; const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("ArmorPenetration"), FString()); const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel()); /** 코드 추가 */ /** 코드 수정 : 0.25f -> ArmorPenetrationCoefficient */ const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f; /** 코드 수정 */ /** 코드 추가 */ const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("EffectiveArmor"), FString()); const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel()); /** 코드 추가 */ /** 코드 수정 : 0.333f -> EffectiveArmorCoefficients */ Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f; /** 코드 수정 */ }
CT_DamageCalculationCoefficients 에 접근하여 필요한 값을 호출할 수 있도록 코드를 수정한다.
CharacterClassInfo 는 GameMode 에 선언되어 있고, ExecCalc_Damage 클래스에서 접근 코드를 작성하면 매번 캐스팅을 진행해야 하는데 이는 효율이 낮아진다.
AuraAbilitySystemLibrary 클래스에 먼저 접근 관련 함수를 추가한다.
#include "..."
UCLASS()
class AURA_API UAuraAbilitySystemLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
...
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|CharacterClassDefaults")
static UCharacterClassInfo* GetCharacterClassInfo(const UObject* WorldContextObject);
};
#include "..."
...
void UAuraAbilitySystemLibaray::InitializeDefaultAttributes(const UObject* WorldContextObject, EHcaracterClass CharacterClass, float Level, TODO: 파라미터 확인후 추가...)
{
/** 코드 삭제 */
// GetCharacterClassInfo() 함수 내에서 AuraGameMode에 대한 캐스팅을 진행하므로 필요없음
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if(AuraGameMode == nullptr) return;
/** 코드 삭제 */
AActor* AvatarActor = ASC->GetAvatarActor();
/** 코드 수정 */
UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
/** 코드 수정 */
...
}
void UAbilitySystemLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC)
{
/** 코드 삭제 */
// GetCharacterClassInfo() 함수 내에서 AuraGameMode에 대한 캐스팅을 진행하므로 필요없음
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if( AuraGameMode == nullptr) return;
/** 코드 삭제 */
/** 코드 수정 */
// GetCharacterClassInfo() 함수를 생성했으므로 기존의 AuraGAmeMode->CharacterClassInfo; 부분 수정
UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
/** 코드 수정 */
...
}
...
UCharacterClassInfo* UAuraAbilitySystemLibrary::GetCharacterClassInfo(const UObject* WorldContextObject)
{
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if(AuraGameMode == nullptr) return nullptr;
return AuraGameMode->CharacterClassInfo;
}
이제 ExecCalc_Damage 클래스에서 CharacterClassInfo 에 접근하여 CurveTable 을 사용가능하도록 코드를 추가한다.
#include "..."
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
#include "Interaction/CombatInterface.h"
...
void Execute_Implementation(const FGamplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
...
/** 코드 수정 : const 키워드 삭제 */
// 아래 Curve->Eval() 에서 파라미터로 GetPlayerLevel() 함수를 호출하기 위함
// const 키워드 존재시, Source or TargetCombatInterface도 const 키워드 지정을 해주어야 함
// 하지만 GetPlayerLevel() 함수의 경우 const가 아니므로 호출이 불가능함
AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor : nullptr;
AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor : nullptr;
/** 코드 수정 */
/** 코드 추가 */
// CombatInterface에 있는 레벨 리턴 함수 사용을 위함
ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);
/** 코드 추가 */
...
/** 코드 추가 */
const UCharacterClassInfo* CharacterClassInfo = UAuraAbilitySystemLibrary::GetCharacterClassInfo(SourceAvatar);
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
/** 코드 추가 */
/** 코드 수정 : 0.25f -> ArmorPenetrationCoefficient */
const float EffectiveArmor = TargetArmor * (100 - SourceArmorPenetration * ArmorPenetrationcoefficient) / 100.f;
/** 코드 수정 */
/** 코드 추가 */
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
/** 코드 추가 */
/** 코드 수정 : 0.333f -> EffectiveArmorCoefficient */
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
/** 코드 수정 */
}
ExecCalc_Implementation() 함수의 마지막 부분에 breakpoint를 두고

컴파일 후 실행하면 각 레벨에 따라 정상적으로 값이 적용되는 것을 확인할 수 있다.
레벨은 뷰포트에서 변경가능하다.

교차 검증을 위한 CT_DamageCalculationCoefficients 파일의 EffectiveArmor
(1, 10, 20, 40레벨일 때의 수치)





Critical Hit 관련 Attribute 를 캡쳐하기 위한 코드를 작성한다.
#include "..."
struct AuraDamageStatics
{
...
DECLARE_ATTRIBUTE_CAPTUREDEF(...);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitChance);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitDamage);
DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitResistance);
AuraDamageStatics()
{
DEFINE_ATTRIBUTE_CAPTUREDEF(...);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitChance, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitDamage, Source, false);
DEFINE_ATTRIBUTE_CAPTUREDEF(UAuraAttributeSet, CriticalHitResistance, Target, false);
}
};
...
UExecCalc_Damage::UEExecCalc_Damage()
{
...
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitChanceDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitDamageDef);
RelevantAttributesToCapture.Add(DamageStatics().CriticalHitResistanceDef);
}
void UExecCalc_Damage::Execute_Implementation(...)
{
Damage *= (100 - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;
/** 코드 추가 */
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef, EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max<float>(SourceCriticalHitChance, 0.f);
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef, EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max<float>(TargetCriticalHitResistance, 0.f);
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef, EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
// CriticalHitResistance 수치에 따라 CriticalHitChance 확률 설정
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance * 0.15f;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
// 크리티컬 여부에 따른 데미지 선정
Damage = bCriticalHit ? 2.f * Damage + SourceCriticalHitDamage : Damage;
/** 코드 추가 */
const FGameplayModifierEvaluateData EvaluateData(...);
OutExecutionOutput.AddOutputModifier(EvaluateData);
}
컴파일 후 CT_DamageCalculationCoefficients 에서 CriticalHitResistance 커브를 추가하고 아래 테이블을 따라 수치를 입력한다.
| key | value |
|---|---|
| 1 | 0.15 |
| 10 | 0.1 |
| 20 | 0.08 |
| 40 | 0.06 |

커브 테이블에 있는 수치를 사용하기 위해 코드를 수정한다.
#include "..."
...
void UExecCalc_Damage::Execute_Implementation(...)
{
...
SourceCriticalHitDamage = FMath::Max<float>(SourceCriticalHitDamage, 0.f);
/** 코드 추가 */
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficients = CriticalHitResistanceCurve->Eval(TargetCombatInterface->GetPlayerLevel());
/** 코드 추가 */
/** 코드 수정 : 0.15f -> CriticalHitResistanceCoefficients */
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance * CriticalHitResistanceCoefficients;
}
DA_CharacterClassInfo 에서 GE_SecondaryAttributes_TEST 를 사용하는지 확인하고

실행하여 정상적으로 동작하는지 확인한다.
먼저 뷰포트에서 레벨을 1로 다시 복구시키고

공격을 시도하다보면 크리티컬이 발생하는 것을 확인할 수 있다.
기본으로 발생하는 데미지 4

크리티컬로 인해 발생하는 데미지 8

(Block에 의해 데미지 감소 + Critical Resistance에 의한 데미지 감소 등이 적용되어서 데미지가 8이 아닌 다른 숫자가 나오는 경우도 있는 것 같음)
추가
BlockChance의 수치가 너무 높아서 크리티컬 확률이 낮기 때문에 수치 조정
GE_SecondaryAttributes_TEST에서BlockChance의 수치를 12로 변경