0. 개요

지금은 GAS의 어빌리티 시스템 컴포넌트에 태그를 직접 부여하고, ASC에 부여된 태그를 직접 조회하여 업그레이드를 적용하고 있다.

하지만 추후에 UI에 업그레이드 적용 여부를 표시하는 등의 작업을 하게 되면, UI가 ASC와 강하게 결합되는 문제가 발생할 수 있다.


- ASC 사용 시 유의점

ASC는 반드시 GAS 시스템 내부의 요소와 결합되어야 함.


따라서 플레이어 스테이트에서 업그레이드 태그를 관리하고, 업그레이드를 부여, 제거, 조회하는 작업은 플레이어 스테이트를 거치도록 코드를 리펙토링한다.

 

 

1. 플레이어 상태

(1) 업그레이드 태그를 저장하는 게임플레이 태그 컨테이너 생성

	// 어빌리티 업그레이드
	UPROPERTY(ReplicatedUsing = OnRep_AbilityUpgradeTags)
	FGameplayTagContainer OwnedAbilityUpgradeTags;

이전에 XP, Level, 속성 포인트, 스펠 포인트의 변화가 발생하면 ReplicatedUsing으로 연결된 콜백 함수를 호출하고, 해당 함수 내에서 델리게이트를 호출해서 다른 클래스 또는 블루프린트로 전달하는 작업을 했다.

따라서 레플리케이션 콜백 함수를 만들어서 동일한 작업을 한다.

	UFUNCTION()
	void OnRep_AbilityUpgradeTags();

 

 

(2) 업그레이드 태그 변경 델리게이트 선언 및 호출

블루프린트에서 사용할 수 있도록 동적 멀티캐스트 델리게이트로 선언한다.

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAbilityUpgradeTagsChanged);

블루프린트에서 바인딩할 수 있도록 BlueprintAssignable로 지정한다.

	// 업그레이드 태그 변화 델리게이트
	UPROPERTY(BlueprintAssignable)
	FOnAbilityUpgradeTagsChanged OnAbilityUpgradeTagsChangedDelegate;

이제 태그 컨테이너에 변화가 생기면 위의 델리게이트가 호출되도록 레플리케이션 콜백 함수를 작성한다.

void AAuraPlayerState::OnRep_AbilityUpgradeTags()
{
    OnAbilityUpgradeTagsChangedDelegate.Broadcast();
}

 

 

 

(3) 서버에서 업그레이드 태그 부여 및 제거하는 함수 - 서버 RPC 함수

	UFUNCTION(Server, Reliable)
	void Server_AddAbilityUpgradeTag(FGameplayTag UpgradeTag);
	
	UFUNCTION(Server, Reliable)
	void Server_RemoveAbilityUpgradeTag(FGameplayTag UpgradeTag);
void AAuraPlayerState::Server_AddAbilityUpgradeTag_Implementation(FGameplayTag UpgradeTag)
{
    OwnedAbilityUpgradeTags.AddTag(UpgradeTag);
}

void AAuraPlayerState::Server_RemoveAbilityUpgradeTag_Implementation(FGameplayTag UpgradeTag)
{
    OwnedAbilityUpgradeTags.RemoveTag(UpgradeTag);
}

이제 게임 모드에서 태그를 직접 ASC에 부여하지 말고, 게임 모드에서 플레이어 컨트롤러의 플레이어 스테이트에 접근해 서버 RPC 함수를 호출하도록 한다.

 

 

2. 플레이어 컨트롤러의 업그레이드 태그 부여 코드 리펙토링

(1) 업그레이드 카드의 선택 버튼 콜백 함수

void AAuraPlayerController::HandleAbilityCardSelected(FGameplayTag SelectedUpgradeTag)
{
    Server_SelectUpgrade(SelectedUpgradeTag);

    // 카드 선택 UI 닫기
    if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(GetHUD()))
    {
        AuraHUD->CardSelectionWidget->RemoveFromParent();
    }
}

이 함수 내의 Server_SelectUpgrade 서버 RPC 함수에 의해 태그가 부여되고 있다.

void AAuraPlayerController::Server_SelectUpgrade_Implementation(FGameplayTag SelectedUpgradeTag)
{
    if (!HasAuthority())
        return;
    
    AActor* AvatarActor = GetPawn();
    if (AvatarActor == nullptr)
        return;

    // 업그레이드 태그 적용
    // UAuraAbilitySystemLibrary::ApplyGameplayTagEffectToSelf(SelectedUpgradeTag, AvatarActor);
    if (AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>())
    {
        // 플레이어 상태의 태그 컨테이너 내에 저장
        AuraPlayerState->Server_AddAbilityUpgradeTag(SelectedUpgradeTag);
    }
}

더 이상 ASC에 태그를 부여하지 않고, 새로 만든 PlayerState 내의 업그레이드 태그 컨테이너에 해당 태그를 추가하게 한다.

 

 

(2) 업그레이드 태그 제거 서버 RPC 함수

이후에 사용할 수도 있으므로 태그 제거 서버 RPC 함수도 만들어 놓는다.

	UFUNCTION(Server, Reliable)
	void Server_RemoveUpgrade(FGameplayTag RemoveTag);
void AAuraPlayerController::Server_RemoveUpgrade_Implementation(FGameplayTag RemoveTag)
{
    if (!HasAuthority())
        return;
    
    AActor* AvatarActor = GetPawn();
    if (AvatarActor == nullptr)
        return;

    // 업그레이드 태그 제거
    if (AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>())
    {
        // 플레이어 상태의 태그 컨테이너에서 해당 태그 제거
        AuraPlayerState->Server_RemoveAbilityUpgradeTag(RemoveTag);
    }
}

 

 

 

3. 게임플레이 어빌리티

이제 각 게임플레이 어빌리티 내의 함수에서 ASC를 조회하여 업그레이드 태그를 확인하여 업그레이드를 적용하는 로직을 플레이어 스테이트를 조회하도록 변경해야 한다.

 

(1) 업그레이드 체크 함수

virtual bool CheckAbilityUpgrades(FGameplayTag AbilityTag) override;

모든 게임플레이 어빌리티는 위의 함수를 오버라이드 하고 있다.

파이어볼트 어빌리티의 함수를 참고하면

bool UAuraFirebolt::CheckAbilityUpgrades(FGameplayTag AbilityTag)
{
	bool bUpgradesApplied = false;
	
	TArray<FAuraAbilityUpgradeInfo> Upgrades = GetAbilityUpgradeForTag(GetAvatarActorFromActorInfo(), AbilityTag);
	if (Upgrades.IsEmpty())
		return false;
	
	const auto& Tags = FAuraGameplayTags::Get();

	for (const auto& Upgrade : Upgrades)
	{
		// 업그레이드 태그 검증
		if (HasUpgradeTag(GetAvatarActorFromActorInfo(), Tags.Upgrades_Fire_FireBolt_IncreaseNum))
		{
			// 투사체 갯수 증가
			int32 StackCount = GetUpgradeStackCount(GetAvatarActorFromActorInfo(), Upgrade.UpgradeEffectTag);
			NumProjectiles += StackCount;
			bUpgradesApplied = true;
		}
	}

	return bUpgradesApplied;
}

ASC로부터(아바타 액터로부터 ASC를 참조하는 방식) 업그레이드 태그를 검증하고 있다.

해당 구조를 PlayerState를 조회하도록 변경한다.

 

bool UAuraGameplayAbility::HasUpgradeTag(AActor* AvatarActor, FGameplayTag Tag)
{
    if (!AvatarActor)
        return false;
    
    if (AActor* OwnerActor = AvatarActor->GetOwner())
    {
        if (AAuraPlayerController* AuraPC = Cast<AAuraPlayerController>(OwnerActor))
        {
            if (AAuraPlayerState* AuraPlayerState = AuraPC->GetPlayerState<AAuraPlayerState>())
            {
                return AuraPlayerState->OwnedAbilityUpgradeTags.HasTagExact(Tag);
            }
        }
    }
    return false;
}

검증 함수 내의 HasUpgradeTag 함수를 수정하여 PlayerState의 태그 컨테이너에서 태그를 찾는다.

 

 

(2) 태그 중첩 횟수 리턴 함수 수정

- AuraPlayerState

int32 AAuraPlayerState::GetUpgradeTagCount(FGameplayTag UpgradeTag)
{
    int TagCount = 0;
    
    if (!UpgradeTag.IsValid())
        return 0;

    for (auto& OwnedTag : OwnedAbilityUpgradeTags)
    {
        if (OwnedTag.MatchesTagExact(UpgradeTag))
            TagCount += 1;
    }
    
    return TagCount;
}

 

- AuraGameplayAbility

int32 UAuraGameplayAbility::GetUpgradeStackCount(AActor* AvatarActor,FGameplayTag Tag)
{
    if (!Tag.IsValid() || !AvatarActor)
        return 0;

    if (APlayerController* PC = Cast<APlayerController>(AvatarActor->GetOwner()))
    {
        if (AAuraPlayerState* AuraPS = PC->GetPlayerState<AAuraPlayerState>())
        {
            return AuraPS->GetUpgradeTagCount(Tag);
        }
    }
    return 0;
}

 

 

(3) 파이어볼트 투사체 갯수 조정

기본 투사체 갯수는 0으로 지정하고, 파이어볼트를 스폰할 때 스펠 레벨과 업그레이드 수를 따라가도록 한다.

void UAuraFirebolt::SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag, bool bOverridePitch, float PitchOverride, AActor* HomingTarget)
{
	// 서버 상에 있는지 확인
	const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority();
	if (!bIsServer)
		return;
 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	/* 
	* 다수의 투사체 퍼트리기
	*/

	const FVector Forward = Rotation.Vector();
	const int32 AbilityLevel = GetAbilityLevel();
	const int32 EffectiveNumProjectiles = FMath::Min(MaxNumProjectiles, NumProjectiles + AbilityLevel);
	TArray<FRotator> Rotators = UAuraAbilitySystemLibrary::EvenlySpacedRotators(Forward, FVector::UpVector, ProjectileSpread, EffectiveNumProjectiles);