0. 개요
캐릭터가 습득한 어빌리티의 정보를 저장하여 불러온다.
1. 어빌리티 저장
(1) LoadScreenSaveData 저장 오브젝트 - 어빌리티 저장을 위한 구조체 선언
USTRUCT(BlueprintType)
struct FSavedAbility
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Class Defaults")
TSubclassOf<UGameplayAbility> GameplayAbility;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
FGameplayTag AbilityTag = FGameplayTag();
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
FGameplayTag AbilityStatus = FGameplayTag();
// 어빌리티가 장착된 슬롯(인풋 태그)
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
FGameplayTag AbilitySlot = FGameplayTag();
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
FGameplayTag AbilityType = FGameplayTag();
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
int32 AbilityLevel = 1;
};
저장 데이터에서 보관할 어빌리티, 어빌리티 태그, 어빌리티 상태, 어빌리티 타입, 인풋 태그, 어빌리티 레벨을 정의한다.
(2) 세이브 데이터에서 어빌리티를 배열로 저장
/* Ability */
UPROPERTY()
TArray<FSavedAbility> SavedAbilities;
};
(3) 활성화된 어빌리티 저장하기
void AAuraCharacter::SaveProgress_Implementation(const FName& CheckpointTag)
{
SaveData->bFirstTimeLoading = false;
if (HasAuthority() == false)
return;
// 델리게이트 생성 및 바인딩
UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(AbilitySystemComponent);
FForEachAbility SaveAbilityDelegate;
SaveAbilityDelegate.BindLambda([this, AuraASC, SaveData](const FGameplayAbilitySpec& AbilitySpec)
{
// 어빌리티 스펙에서 태그 가져오기
const FGameplayTag AbilityTag = AuraASC->GetAbilityTagFromSpec(AbilitySpec);
// 어빌리티 정보 가져오기
UAbilityInfo* AbilityInfo = UAuraAbilitySystemLibrary::GetAbilityInfo(this);
// 어빌리티 태그에 해당하는 어빌리티 정보 가져오기
FAuraAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
FSavedAbility SavedAbility;
SavedAbility.GameplayAbility = Info.Ability;
SavedAbility.AbilityTag = AbilityTag;
SavedAbility.AbilityLevel = AbilitySpec.Level;
SavedAbility.AbilitySlot = AuraASC->GetSlotFromAbilityTag(AbilityTag);
SavedAbility.AbilityStatus = AuraASC->GetStatusFromAbilityTag(AbilityTag);
SavedAbility.AbilityType = Info.AbilityType;
// 어빌리티 저장
SaveData->SavedAbilities.Add(SavedAbility);
});
저장된 어빌리티를 순회하여 델리게이트를 호출하는 ForEachAbility 시그니쳐를 이용하여 델리게이트를 생성한다.
그리고 람다 함수 바인딩을 통해 어빌리티의 정보들을 가져와 세이브 데이터에 저장한다.
void UAuraAbilitySystemComponent::ForEachAbility(const FForEachAbility& Delegate)
{
// 어빌리티가 차단되거나 변경되었을 수 있음, 어빌리티 잠금
FScopedAbilityListLock ActiveScopeLock(*this);
for (const FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
if (Delegate.ExecuteIfBound(AbilitySpec) == false)
{
UE_LOG(LogAura, Error, TEXT("Failed to execute delegate in %hs"), __FUNCTION__);
}
}
}
해당 델리게이트는 활성화된 어빌리티를 순회하는 함수이다.
활성화된 어빌리티에 대하여 매개 변수로 전달된 델리게이트를 호출한다.
따라서, 활성화된 모든 어빌리티에 대해 저장 데이터에 어빌리티 정보를 저장하도록 람다 함수가 호출된다.
최종적으로는 SaveData의 SavedAbilities 배열에 활성화된 어빌리티가 모두 저장된다.
2. 어빌리티 불러오기
저장 데이터 오브젝트에 저장된 배열을 이용하여, 게임 시작 시 어빌리티를 캐릭터에게 부여하도록 한다.
(1) AuraASC에서 세이브 데이터에서 어빌리티 배열을 가져와 캐릭터에게 부여하기
// 어빌리티 부여
void AddCharacterAbilitiesFromSaveData(ULoadScreenSaveGame* SaveData);
void UAuraAbilitySystemComponent::AddCharacterAbilitiesFromSaveData(ULoadScreenSaveGame* SaveData)
{
// 저장된 어빌리티 순회
for (const FSavedAbility& Data : SaveData->SavedAbilities)
{
const TSubclassOf<UGameplayAbility> LoadedAbilityClass = Data.GameplayAbility;
FGameplayAbilitySpec LoadedAbilitySpec = FGameplayAbilitySpec(LoadedAbilityClass, Data.AbilityLevel);
LoadedAbilitySpec.DynamicAbilityTags.AddTag(Data.AbilitySlot);
LoadedAbilitySpec.DynamicAbilityTags.AddTag(Data.AbilityStatus);
if (Data.AbilityType == FAuraGameplayTags::Get().Abilities_Type_Offensive)
{
GiveAbility(LoadedAbilitySpec);
}
else if (Data.AbilityType == FAuraGameplayTags::Get().Abilities_Type_Passive)
{
GiveAbilityAndActivateOnce(LoadedAbilitySpec);
}
}
bStartupAbilitiesGiven = true;
AbilitiesGivenDelegate.Broadcast();
}
저장된 어빌리티를 순회하여 게임플레이 어빌리티 스펙을 생성한다.
생성된 스펙에 입력 태그와 상태에 대한 태그를 추가한다.
액티브 스펠이면 어빌리티를 부여하고, 패시브 스펠이면 어빌리티 부여 후 실행한다.
어빌리티 부여 플래그를 true로 바꾼다.
어빌리티가 주어지면 UI에 반영하도록 델리게이트를 호출한다.
(2) AuraCharacter의 진행상황 불러오기 함수 수정
void AAuraCharacter::LoadProgress()
{
// 첫 로딩일 때
if (SaveData->bFirstTimeLoading)
{
// 기본 1차 속성 적용
InitializeDefaultAttributes();
AddCharacterAbilites();
}
else
{
UAuraAbilitySystemComponent* AuraASC = CastChecked<UAuraAbilitySystemComponent>(AbilitySystemComponent);
// 서버에서만 실행
if (HasAuthority() == false)
return;
// 저장된 세이브에서 어빌리티 불러오기
AuraASC->AddCharacterAbilitiesFromSaveData(SaveData);
(3) GA_ListenForEvent 게임플레이 어빌리티
이 어빌리티는 1차 속성과 IncomingXP에 대해, 해당 태그로 발생한 게임플레이 이벤트를 감지하고, 값을 가져와 캐릭터에 적용하는 게임플레이 어빌리티이다.
게임이 시작되면 부여되고, 속성 값 변화가 캐치되면 캐릭터에 반영하는 식으로 작동한다.
각 속성의 변화가 생기면 Set by Caller를 이용하여 값을 할당하고 게임플레이 이펙트를 적용한다.
저장 데이터 로드 시, 해당 어빌리티가 게임이 시작될 때 자동으로 부여 되도록 설정이 필요하다.
(3-1) DA_AbilityInfo에서 GA_ListenForEvent 어빌리티 정보 등록
GA_ListenForEvent를 위한 게임플레이 태그를 만든다.
(3-2) GA_ListenForEvent 게임플레이 어빌리티에 태그 부여
(3-3) BP_AuraCharacter의 초기 어빌리티 정리
3. 디버그
(1) 패시브 어빌리티가 두 번 활성화 됨
AuraASC에서 패시브 어빌리티를 부여하는 함수를 수정한다.
void UAuraAbilitySystemComponent::AddCharacterPassiveAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupPassiveAbilities)
{
for (TSubclassOf<UGameplayAbility> AbilityClass : StartupPassiveAbilities)
{
// 게임플레이 어빌리티 스펙 생성
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
AbilitySpec.DynamicAbilityTags.AddTag(FAuraGameplayTags::Get().Abilities_Status_Equipped);
GiveAbilityAndActivateOnce(AbilitySpec);
}
}
어빌리티 스펙에 장착됨을 표시하는 태그를 추가한다.
void UAuraAbilitySystemComponent::AddCharacterAbilitiesFromSaveData(ULoadScreenSaveGame* SaveData)
{
// 저장된 어빌리티 순회
for (const FSavedAbility& Data : SaveData->SavedAbilities)
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (Data.AbilityType == FAuraGameplayTags::Get().Abilities_Type_Offensive)
{
GiveAbility(LoadedAbilitySpec);
}
else if (Data.AbilityType == FAuraGameplayTags::Get().Abilities_Type_Passive)
{
if (Data.AbilityStatus.MatchesTagExact(FAuraGameplayTags::Get().Abilities_Status_Equipped))
{
GiveAbilityAndActivateOnce(LoadedAbilitySpec);
}
else
{
GiveAbility(LoadedAbilitySpec);
}
}
}
패시브 어빌리티 상태가 장착됨으로 표시된 경우에만 활성화하도록 설정한다.
(1-1) AuraCharacter의 진행 상황 저장 함수 살펴보기
void AAuraCharacter::SaveProgress_Implementation(const FName& CheckpointTag)
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
FSavedAbility SavedAbility;
SavedAbility.GameplayAbility = Info.Ability;
SavedAbility.AbilityTag = AbilityTag;
SavedAbility.AbilityLevel = AbilitySpec.Level;
SavedAbility.AbilitySlot = AuraASC->GetSlotFromAbilityTag(AbilityTag);
SavedAbility.AbilityStatus = AuraASC->GetStatusFromAbilityTag(AbilityTag);
SavedAbility.AbilityType = Info.AbilityType;
// 어빌리티 저장
SaveData->SavedAbilities.Add(SavedAbility);
});
SavedAbilities에 Add를 통해 어빌리티를 부여하는데 중복으로 수행되는 것으로 보인다.
해당 코드를 아래와 같이 바꾼다.
SaveData->SavedAbilities.AddUnique(SavedAbility);
(1-2) LoadScreenSaveGame에서 AddUnique를 사용하기 위해 연산자 오버로딩하기
AddUnique 함수는 배열에 동일한 요소가 있으면 추가하지 않도록 한다.
커스텀 구조체에 '==' 연산자에 대한 정보가 없으므로 에러가 발생할 것이다.
에러를 해결하기 위해 커스텀 구조체에서 연산자를 오버로딩한다.
inline bool operator==(const FSavedAbility& Left, const FSavedAbility& Right)
{
return Left.AbilityTag.MatchesTagExact(Right.AbilityTag);
}
FSavedAbility 구조체 아래, LoadScreenSaveGame 클래스 위에 작성한다.
(2) 장착된 어빌리티의 상태가 제대로 반영되지 않음
AuraASC에서 서버에 의해 어빌리티를 장착하게 될 때, 기존의 상태를 제거하고 어빌리티가 장착되었다는 태그를 넘겨주기로 한다.
void UAuraAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Slot)
{
// 아직 장착 및 활성화 되지 않은 어빌리티
if (AbilityHasAnySlot(*AbilitySpec) == false)
{
if (IsPassiveAbility(*AbilitySpec))
{
// 어빌리티 활성화
TryActivateAbility(AbilitySpec->Handle);
// 패시브 이펙트 활성화
MulticastActivatePassiveEffect(AbilityTag, true);
}
AbilitySpec->DynamicAbilityTags.RemoveTag(GetStatusFromSpec(*AbilitySpec));
AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Equipped);
}
(2-1) 저장 데이터의 배열 비우기
void AAuraCharacter::SaveProgress_Implementation(const FName& CheckpointTag)
{
FForEachAbility SaveAbilityDelegate;
SaveData->SavedAbilities.Empty();
SaveAbilityDelegate.BindLambda([this, AuraASC, SaveData](const FGameplayAbilitySpec& AbilitySpec)
{
람다 함수가 바인드 되기 전에 SavedAbilities가 채워질 가능성이 있기 때문에 배열을 비우고 시작한다.
(2-2) PassiveNiagaraComponent 클래스에서 패시브 어빌리티 이펙트 표시
void UPassiveNiagaraComponent::ActivateIfEquipped(UAuraAbilitySystemComponent* AuraASC)
{
const bool bStartupAbilitiesGiven = AuraASC->bStartupAbilitiesGiven;
if (bStartupAbilitiesGiven)
{
if (AuraASC->GetStatusFromAbilityTag(PassiveSpellTag) == FAuraGameplayTags::Get().Abilities_Status_Equipped)
{
Activate();
}
}
}
패시브 어빌리티가 적절하게 주어져 상태가 장착됨으로 변경되면 나이아가라 컴포넌트를 작동한다.
void UPassiveNiagaraComponent::BeginPlay()
{
Super::BeginPlay();
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetOwner())))
{
AuraASC->ActivatePassiveEffect.AddUObject(this, &UPassiveNiagaraComponent::OnPassiveActivate);
ActivateIfEquipped(AuraASC);
}
else if(ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetOwner()))
{
CombatInterface->GetOnASCRegisteredDelegate().AddLambda([this](UAbilitySystemComponent* ASC)
{
if (UAuraAbilitySystemComponent* AuraASC = Cast<UAuraAbilitySystemComponent>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetOwner())))
{
AuraASC->ActivatePassiveEffect.AddUObject(this, &UPassiveNiagaraComponent::OnPassiveActivate);
ActivateIfEquipped(AuraASC);
}
});
}
}
3. 디버그
(1) 로드 후 아케인 파편 스펠을 실행하면 패시브 어빌리티를 비활성화하는 문제
void UAuraAbilitySystemComponent::AbilityInputTagPressed(const FGameplayTag& InputTag)
{
if (InputTag.IsValid() == false)
return;
FAuraGameplayTags GameplayTags = FAuraGameplayTags::Get();
FScopedAbilityListLock ActiveScopeLoc(*this);
for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
{
/*
1. Player.Abilities.WaitForExecute 태그가 부여된 ASC에 대해
2. Player.Abilities.WaitForExecute 태그를 가진 어빌리티의
3. 입력태그를 체크
- 어빌리티의 입력태그가 LMB인 경우?
- 어빌리티의 입력태그가 LMB가 아닌 경우?
- 다른 키를 누르면 입력을 취소?
*/
if (HasMatchingGameplayTag(GameplayTags.Player_Abilities_WaitForExecute))
{
if (AbilitySpec.IsActive())
{
// 왼쪽 버튼 클릭 혹은 어빌리티 버튼 입력
if (InputTag.MatchesTagExact(GameplayTags.InputTag_LMB)
|| AbilitySpec.DynamicAbilityTags.HasTag(InputTag))
{
// 해당 어빌리티에 대해 누르기 발동
AbilitySpecInputPressed(AbilitySpec);
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, AbilitySpec.Handle, AbilitySpec.ActivationInfo.GetActivationPredictionKey());
continue;
}
else
{
// 다른 키 입력 시 어빌리티 취소
CancelAbilityHandle(AbilitySpec.Handle);
continue;
}
}
}
// 어빌리티에 할당된 입력태그가 입력으로 받은 입력태그와 일치
else if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag))
{
// 해당 어빌리티에 대해 누르기 발동
AbilitySpecInputPressed(AbilitySpec);
if (AbilitySpec.IsActive() == false)
{
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, AbilitySpec.Handle, AbilitySpec.ActivationInfo.GetActivationPredictionKey());
continue;
}
}
}
}
return을 continue로 변경한다.
'UE 5 스터디 > Gameplay Ability System(GAS)' 카테고리의 다른 글
29-9. 저장 - (10) 월드 불러오기와 역직렬화(Deserialize) (0) | 2025.04.10 |
---|---|
29-8. 저장 - (9) 월드 저장과 직렬화 (0) | 2025.04.10 |
29-6. 저장 - (7) 플레이어 데이터 저장 및 불러오기, 디버그 (0) | 2025.04.07 |
29-5. 저장 - (6) 진행 상황 저장 (0) | 2025.04.07 |
29-4. 저장 - (5) 플레이어 스타트를 상속받는 체크 포인트 액터 (0) | 2025.04.07 |