UE 5 스터디/Gameplay Ability System(GAS)

22-21. 스펠 메뉴 UI - (21) 어빌리티 장착

Crat3 2025. 2. 19. 16:45

0. 개요

스펠 메뉴에서 습득한 어빌리티를 특정 버튼에 장착하도록 기능을 구현한다.

 

 

1. 어빌리티 장착

(1) Aura ASC에서 태그를 이용하여 어빌리티 상태를 가져오는 함수 선언

FGameplayTag GetStatusFromAbilityTag(const FGameplayTag& AbilityTag);
FGameplayTag UAuraAbilitySystemComponent::GetStatusFromAbilityTag(const FGameplayTag& AbilityTag)
{
    if (const FGameplayAbilitySpec* Spec = GetSpecFromAbilityTag(AbilityTag))
    {
        return GetStatusFromSpec(*Spec);
    }
    return FGameplayTag();
}

어빌리티 태그를 넣으면 게임플레이 어빌리티 스펙을 가져와 스펙에서 어빌리티 상태를 리턴하는 Getter 함수를 만든다.

 

 

(2) 태그를 이용하여 입력(Input) 태그를 가져오는 함수 선언

	FGameplayTag GetInputTagFromAbilityTag(const FGameplayTag& AbilityTag);
FGameplayTag UAuraAbilitySystemComponent::GetInputTagFromAbilityTag(const FGameplayTag& AbilityTag)
{
    if (const FGameplayAbilitySpec* Spec = GetSpecFromAbilityTag(AbilityTag))
    {
        return GetInputTagFromSpec(*Spec);
    }
    return FGameplayTag();
}

어빌리티 태그를 넣으면 스펙으로부터 입력 태그를 리턴하는 Getter 함수를 만든다.

 

 

(3) 스펠 메뉴 위젯 컨트롤러

(3-1) 장착 버튼 클릭 함수

	UFUNCTION(BlueprintCallable)
	void EquipButtonPressed();
void USpellMenuWidgetController::EquipButtonPressed()
{
	// (1) equip 애니메이션 재생
	const FGameplayTag& AbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;

	WaitForEquipDelegate.Broadcast(AbilityType);
	bWaitForEquipSelection = true;

	// 어빌리티 상태 가져오기
	const FGameplayTag SelectedStatus = GetAuraASC()->GetStatusFromAbilityTag(SelectedAbility.Ability);

	// 장착된 어빌리티에 대해 입력 태그 가져오기
	if (SelectedStatus.MatchesTagExact(FAuraGameplayTags::Get().Abilities_Status_Equipped))
	{
		SelectedSlot = GetAuraASC()->GetInputTagFromAbilityTag(SelectedAbility.Ability);
	}

	// 장착

}

선택한 어빌리티에 대해 어빌리티의 상태를 가져온다.

이미 장착된 어빌리티면 해당 어빌리티의 입력 태그를 가져온다.

이후에는 사용자가 UI에서 선택한 글로브에 어빌리티를 할당하고 그 어빌리티의 입력 태그를 변경하는 작업이 이루어진다.

 

 

(3-2) 장착 행의 글로브를 누르면 호출되는 함수

UI와의 연결을 위해 블루프린트에서 호출할 수 있는 함수를 만든다.

	UFUNCTION(BlueprintCallable)
	void SpellRowGlobePressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType);

 

선택한 글로브의 태그(왼쪽버튼, 오른쪽버튼, 1, 2, 3, 4)와 액티브 / 패시브를 판단하는 어빌리티 타입의 태그를 인자로 받는다.

void USpellMenuWidgetController::SpellRowGlobePressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType)
{
	if (bWaitForEquipSelection == false)
		return;

	// 액티브 어빌리티를 패시브 칸에 장착할 수 없음, 반대도 동일
	const FGameplayTag& SelectedAbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
	if (SelectedAbilityType.MatchesTagExact(AbilityType) == false)
		return;

	// 서버에게 알림

}

예외 케이스를 처리한 이후에 어빌리티의 입력 태그 등을 변경할 것이므로 서버로 요청을 전송할 것이다.

 

 

(4) AuraASC의 어빌리티 장착 서버 RPC 함수

	UFUNCTION(Server, Reliable)
	void ServerEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Slot);
void UAuraAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Slot)
{
    //
    if (FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
    {
        // 슬롯 바꾸기
        const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec);
        const FGameplayTag& Status = GetStatusFromSpec(*AbilitySpec);

        const bool bStatusValid = Status == FAuraGameplayTags::Get().Abilities_Status_Equipped || Status == FAuraGameplayTags::Get().Abilities_Status_Unlocked;
        if (bStatusValid)
        {
            // 어빌리티가 장착되었거나 해금된 경우
            // 해당 슬롯에 있는 어빌리티를 제거

        }
    }
}

이제 장착하려는 슬롯의 입력 태그, 어빌리티의 상태를 이용하여

어빌리티가 장착 중이거나 해금이 된 상태일 때 슬롯을 장착할 수 있게 된다.

 

(4-1) 입력 태그 확인 함수

주어진 어빌리티 스펙의 태그를 순회하여 슬롯의 입력 태그와 일치하면 이미 슬롯에 할당된 어빌리티이므로 true를 반환하는 함수이다.

	static bool AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot);
bool UAuraAbilitySystemComponent::AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot)
{
    for (FGameplayTag Tag : Spec->DynamicAbilityTags)
    {
        if (Tag.MatchesTagExact(Slot))
        {
            return true;
        }
    }
    return false;
}

 

(4-2) 입력 태그 초기화

어빌리티의 입력 태그를 초기화하고 복제한다.

	void ClearSlot(FGameplayAbilitySpec* Spec);
void UAuraAbilitySystemComponent::ClearSlot(FGameplayAbilitySpec* Spec)
{
    //
    const FGameplayTag Slot = GetInputTagFromSpec(*Spec);
    Spec->DynamicAbilityTags.RemoveTag(Slot);
    MarkAbilitySpecDirty(*Spec);
}

 

(4-3) 슬롯에 할당된 어빌리티 초기화

void UAuraAbilitySystemComponent::ClearAbilitiesOfSlot(const FGameplayTag& Slot)
{
    FScopedAbilityListLock ActiveScopLock(*this);
    for (FGameplayAbilitySpec& Spec : GetActivatableAbilities())
    {
        if (AbilityHasSlot(&Spec, Slot))
        {
            ClearSlot(&Spec);
        }
    }
}

 

(4-4) ServerEquipAbility 함수 수정

void UAuraAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Slot)
{
    //
    if (FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
    {
        // 슬롯 바꾸기
        const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec);
        const FGameplayTag& Status = GetStatusFromSpec(*AbilitySpec);

        // 어빌리티가 장착되었거나 해금된 경우
        const bool bStatusValid = Status == FAuraGameplayTags::Get().Abilities_Status_Equipped || Status == FAuraGameplayTags::Get().Abilities_Status_Unlocked;
        if (bStatusValid)
        {
            // 해당 슬롯에 있는 어빌리티를 제거
            ClearAbilitiesOfSlot(Slot);
            // 이 어빌리티 슬롯을 초기화
            ClearSlot(AbilitySpec);
            // 슬롯 바꿔치기
            AbilitySpec->DynamicAbilityTags.AddTag(Slot);

            // 어빌리티가 해금된 경우
            if (Status.MatchesTagExact(FAuraGameplayTags::Get().Abilities_Status_Unlocked))
            {
                // 장착 태그 부여
                AbilitySpec->DynamicAbilityTags.RemoveTag(FAuraGameplayTags::Get().Abilities_Status_Unlocked);
                AbilitySpec->DynamicAbilityTags.AddTag(FAuraGameplayTags::Get().Abilities_Status_Equipped);
            }
            // 복제
            MarkAbilitySpecDirty(*AbilitySpec);
        }
    }
}

이 함수는 아래와 같이 작동한다.

 

- 스펠 트리에서 선택한 어빌리티가 장착되었거나 해금된 상태인지 확인

- 변경하기로 선택한 슬롯에 할당된 어빌리티 제거

- 해당 어빌리티의 입력 태그 제거

- 선택한 슬롯의 입력 태그를 어빌리티에 추가

- 어빌리티가 해금(Unlocked) 상태라면 장착됨(Equipped) 상태로 바꿈

 

 

(5) AuraASC의 클라이언트 RPC 함수

이제 서버 RPC 함수에서 클라이언트 RPC 함수를 호출하여 정보를 클라이언트로 전송하고, 클라이언트에서는 해당 정보를 블루프린트로 넘길 것이다.

 

(5-1) 클라이언트 RPC 함수 선언

	UFUNCTION(Client, Reliable)
	void ClientEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PrevSlot);

 

서버 RPC 함수 하단에 클라이언트 RPC 함수로 인자를 넘겨주는 코드를 작성한다.

            // 복제
            MarkAbilitySpecDirty(*AbilitySpec);
        }
        // 클라이언트 RPC
        ClientEquipAbility(AbilityTag, FAuraGameplayTags::Get().Abilities_Status_Equipped, Slot, PrevSlot);
    }

 

 

(5-2) 델리게이트 선언과 호출

DECLARE_MULTICAST_DELEGATE_FourParams(FAbilityEquipped, const FGameplayTag& /*어빌리티 태그*/, const FGameplayTag& /*어빌리티 상태*/, const FGameplayTag& /*슬롯*/, const FGameplayTag& /*이전 슬롯*/);
	FAbilityEquipped AbilityEquipped;
void UAuraAbilitySystemComponent::ClientEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PrevSlot)
{
    AbilityEquipped.Broadcast(AbilityTag, Status, Slot, PrevSlot);
}

 

 

(5-3) 콜백 함수 바인딩

void USpellMenuWidgetController::BindCallbacksToDependencies()
{
//~~~~~~~~~~~~~~~~~
	// 어빌리티 장착 델리게이트 바인딩
	GetAuraASC()->AbilityEquipped.AddUObject(this, &USpellMenuWidgetController::OnAbilityEquipped);
void USpellMenuWidgetController::OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PrevSlot)
{
	// 불리언 플래그
	bWaitForEquipSelection = false;

	const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();

	FAuraAbilityInfo LastSlotInfo;
	LastSlotInfo.StatusTag = GameplayTags.Abilities_Status_Unlocked;
	LastSlotInfo.InputTag = PrevSlot;
	LastSlotInfo.AbilityTag = GameplayTags.Abilities_None;

	// 변경할 슬롯에 이미 어빌리티가 있다면 빈 어빌리티 정보를 보냄
	AbilityInfoDelegate.Broadcast(LastSlotInfo);

	// 변경할 슬롯에 선택한 어빌리티의 정보를 채움
	FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
	Info.StatusTag = Status;
	Info.InputTag = Slot;
	AbilityInfoDelegate.Broadcast(Info);

	// 장착 행 강조 애니메이션 중단
	StopWaitForEquipDelegate.Broadcast(AbilityInfo->FindAbilityInfoForTag(AbilityTag).AbilityTag);
}

클라이언트에서는 전달받은 매개변수를 이용하여 장착하고자 하는 슬롯에 이미 스펠(어빌리티)가 있다면 비우고 다시 채우는 작업을 수행한다.

 

 

(6) 서버 RPC 함수 호출

이제 서버 RPC 함수에서 클라이언트 RPG 함수를 호출하여 정보를 보냈으므로

서버 RPC 함수를 호출해주면 된다.

void USpellMenuWidgetController::SpellRowGlobePressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType)
{
	if (bWaitForEquipSelection == false)
		return;

	// 액티브 어빌리티를 패시브 칸에 장착할 수 없음, 반대도 동일
	const FGameplayTag& SelectedAbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
	if (SelectedAbilityType.MatchesTagExact(AbilityType) == false)
		return;

	// 서버에게 알림
	GetAuraASC()->ServerEquipAbility(SelectedAbility.Ability, SlotTag);
}

 

 

2. 스펠 장착 행 블루프린트

(1) 스펠 장착 행 글로브에 어빌리티 타입 변수 생성

스펠 장착 행의 각 글로브가 액티브인지 패시브인지 파악할 수 있도록 타입 변수를 생성한다.

 

(2) 스펠 장착 행 글로브에 어빌리티 타입 지정

각 글로브에 액티브 / 패시브 타입을 지정한다.

 

(3) 스펠 장착 행 글로브 이벤트 그래프

먼저 버튼의 OnClicked 이벤트를 사용하기 위해 변수로 체크한다.

이제 어빌리티의 장착 버튼을 누르고 스펠 장착 행의 글로브를 누르면 정상적으로 어빌리티의 입력 태그가 바뀌고, 슬롯에 할당되는 것을 볼 수 있다.

 

 

3. 디버그

(1) 스펠 장착 행에서 재할당된 어빌리티가 하얗게 보이는 버그

스펠 장착 행은 장착 글로브 버튼에서부터 어빌리티 정보(AbilityInfo)를 델리게이트로 받고 있다.

어빌리티의 태그를 확인하여 None으로 지정되었다면 글로브의 배경과 아이콘을 초기화하는 함수를 호출하게 한다.

 

 

(2) 오버레이에 반영되지 않는 현상

다음 포스트에서 해결할 것이다.

 

(3) 스펠을 장착하면 다른 스펠의 이미지가 초기화 되는 현상

다음 포스트에서 해결할 것이다.