0. 개요

이전 포스트에서 스택을 활용한 체력 회복 / 감소 액터를 만들었다.

그러나 체력이 최대 체력보다 커지거나 0보다 작아지는 현상은 아직 수정하지 않았다.

이번 포스트에서는 속성 세트의 PreAttributeChange 함수를 오버라이딩하여 체력을 클램핑(Clamping) 하도록 한다.


- 클램프(Clamp)

어떤 값이 증가하거나 감소할 때, 특정 최소값과 최댓값을 넘지 못하도록 고정시키는 것.

 

- std::clamp 함수

모던 C++에서 추가된 Clamp 함수를 사용하여 쉽게 값을 고정하는 방법도 있다.


 

 

1. PreAttributeChange 함수 오버라이드

속성이 변경되기 직전에 호출되는 함수이다.

언리얼 공식에서는 해당 함수를 값을 클램핑할 때만 사용하도록 권장하고 있다.

 

void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute & Attribute, float & NewValue)
{
    // 클램핑 용으로만 사용할 것
    Super::PreAttributeChange(Attribute, NewValue);

    // 인수로 넘겨받은 Attribute가 해당 속성과 동일한지 확인하기
    if (Attribute == GetHealthAttribute())
    {
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
    }
    if (Attribute == GetMaxHealthAttribute())
    {
        
    }
    if (Attribute == GetManaAttribute())
    {
        NewValue = FMath::Clamp(NewValue, 0.f, GetMaxMana());
    }
    if (Attribute == GetMaxManaAttribute())
    {
        
    }
}

FMath 라이브러리의 Clamp 함수를 사용하여 값이 최소값과 최대값 사이에만 존재할 수 있도록 고정할 수 있다.

 

그러나 PreAttributeChange는 수정자의 값을 반환하여 변경하는 것이기 때문에 현재값을 수정하지 않는다.

(잠재값-Pending Value-을 수정)

-> 즉 현재값을 수정하기 위한 추가적인 스텝을 취해야 한다.

 

 

2. PostGameplayEffectExecute 함수

게임플레이 이펙트가 속성을 변경한 이후에 호출된다.


- 함수 호출 순서

PreAttributeChange -> 속성 변경 -> PostGameplayEffectExecute


GameplayEffectModCallbackData를 인수로 사용한다.

 

(1) 콜백 데이터의 멤버 변수

- EffectSpec

GameplayEffectSpec 구조체 형의 스펙이다.

정의(Def), 유지 시간(Period), 작동 시간(Duration) 등의 정보를 가진다.

EffectSpec의 EffectContext에 접근해 발생자에 대한 정보를 얻을 수 있다.

 

- EvaluatedData

속성(Attribute), 수정자 연산자(Modifier Op), 크기(Magnitude), 핸들의 정보를 가진다.

 

- Target

UAbilitySystemComponent를 가진다.

-> 대상의 Ability info에 접근할 수 있다.

 

 

(2) 코드

void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
{
    Super::PostGameplayEffectExecute(Data);

    // 게임플레이 이펙트가 속성을 변경한 후에 실행(PreAttributeChange -> 속성 변경 -> 이 함수)
    const FGameplayEffectContextHandle EffectContextHandle = Data.EffectSpec.GetContext();

    // 소스 - 이펙트 발동자, 타겟 - 이펙트의 대상(속성 세트의 주인)
    const UAbilitySystemComponent* SourceASC = EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();

    if (SourceASC && SourceASC->AbilityActorInfo.IsValid() && SourceASC->AbilityActorInfo->AvatarActor.IsValid())
    {
        AActor* SourceAvatarActor = SourceASC->AbilityActorInfo->AvatarActor.Get();
        const AController* SourceController = SourceASC->AbilityActorInfo->PlayerController.Get();
        if (SourceController == nullptr && SourceAvatarActor != nullptr)
        {
            // 폰에서 플레이어 컨트롤러 가져오기
            if (const APawn* Pawn = Cast<APawn>(SourceAvatarActor))
            {
                SourceController = Pawn->GetController();
            }
        }
        if (SourceController)
        {
            ACharacter* SourceCharacter = Cast<ACharacter>(SourceController->GetPawn());
        }
    }

    // 타겟의 어빌리티 시스템 컴포넌트에 접근하기
    if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
    {
        AActor* TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
        AController* TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
        ACharacter* TargetCharacter = Cast<ACharacter>(TargetAvatarActor);
        UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetAvatarActor);
    }
}

- 이펙트 발생자(Source)의 정보 가져오기

Data의 EffectSpec에서 GetContext 함수를 사용하여 이펙트 컨텍스트 핸들을 통해 대상의 어빌리티 시스템 컴포넌트에 접근하여 게임플레이 이펙트를 발생시킨 액터의 정보를 가져올 수 있다.

 

- 이펙트 대상(Target)의 정보 가져오기

Data의 Target에서 AbilityActorInfo에 직접 접근하여 타겟의 정보에 대한 여러 포인터를 가져올 수 있다.

 

 

3. 소스와 대상에 쉽게 접근할 수 있는 구조체 만들기 - FEffectProperties 구조체

위에서 확인했듯 여러가지 구성요소를 통해 소스와 타겟의 정보를 꺼내올 수 있었다.

이제 더 간단하게 정보를 얻어올 수 있도록 구조체와 Getter 함수를 정의한다.

 

(1) 코드

USTURCT()
struct FEffectProperties
{
	GENERATED_BODY()

	FEffectProperties() {}

	FGameplayEffectContextHandle EffectContextHandle;

	// 소스
	UPROPERTY()
	UAbilitySystemComponent* SourceASC = nullptr;

	UPROPERTY()
	AActor* SourceAvatarActor = nullptr;

	UPROPERTY()
	AController* SourceController = nullptr;

	UPROPERTY()
	ACharacter* SourceCharacter = nullptr;

	// 타겟
	UPROPERTY()
	UAbilitySystemComponent* TargetASC = nullptr;

	UPROPERTY()
	AActor* TargetAvatarActor = nullptr;

	UPROPERTY()
	AController* TargetController = nullptr;

	UPROPERTY()
	ACharacter* TargetCharacter = nullptr;
};

 

이제 기존에 PostGameplayEffectExecute 함수에서 소스와 타겟의 정보를 꺼내오던 것은 함수로 래핑해서 이용할 것이다.

private:
	// 콜백 데이터에서 꺼내와 구조체에 저장
	void SetEffectProperties(const FGameplayEffectModCallbackData & Data, FEffectProperties& Props) const;
void UAuraAttributeSet::SetEffectProperties(const FGameplayEffectModCallbackData & Data, FEffectProperties& Props) const
{
    // 게임플레이 이펙트가 속성을 변경한 후에 실행(PreAttributeChange -> 속성 변경 -> 이 함수)
    Props.EffectContextHandle = Data.EffectSpec.GetContext();

    // 소스 - 이펙트 발동자, 타겟 - 이펙트의 대상(속성 세트의 주인)
    Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();

    if (Props.SourceASC && Props.SourceASC->AbilityActorInfo.IsValid() && Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
    {
        Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
        Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
        if (Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
        {
            // 폰에서 플레이어 컨트롤러 가져오기
            if (const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
            {
                Props.SourceController = Pawn->GetController();
            }
        }
        if (Props.SourceController)
        {
            Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
        }
    }

    // 타겟의 어빌리티 시스템 컴포넌트에 접근하기
    if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
    {
        Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
        Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
        Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
        Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
    }
}

 

그리고 PostGameplayEffectExecute 함수에서 이 함수에 콜백 데이터를 넘겨주면 된다.

void UAuraAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
{
    Super::PostGameplayEffectExecute(Data);

    // 소스가 캐릭터가 아닐 경우에 컨트롤러를 얻지 못할 수 있음!!
    FEffectProperties Props;
    SetEffectProperties(Data, Props);
}

게임플레이 이펙트를 발동한 주체가 Character가 아닐 경우에는 컨트롤러를 얻을 수 없음에 주의하자.