#1. 목표
GAS 프레임워크에서 Ability System Component에 등록된 AttributeSet의 각 Attribute 항목의 수치 변화 이벤트를 C++과 블루프린트 환경 모두에서 감지할 수 있는 인터페이스를 구현합니다. 그룹프로젝트 내 협력 과정에서 HUD 구현에 필요한 캐릭터의 HP, SP, 그리고 MP 등의 Attribute 항목의 수치 값을 실시간으로 가져올 수 있는 인터페이스가 요구되었습니다. 따라서, UI 구현 시 C++ 환경뿐만 아니라 Blueprint 환경 모두에서 캐릭터의 Attribute 값을 읽어올 수 있는 인터페이스를 구현하게 되었습니다.
#2. 관련 이슈
#2. C++ 환경
1. 개념
ASC의 데이터 멤버 "FActiveGameplayEffectsContainer"에서 관리하는 AttributeValueChangeDelegates(FOnGameplayAttributeValueChange) 목록에 새로운 콜백 함수를 등록함으로서, 특정 Attribute의 값 변화 이벤트를 감지할 수 있습니다. HUD 클래스는 이를 활용해 캐릭터의 현재 HP, MP, 그리고 SP 등의 상태 정보를 가져와 Widget에 반영할 수 있게 됩니다.
2. 구현 목표
Player State 객체(ASC가 관리되는 장소)에서 Attribute 수치 변화 이벤트를 감지하고 이를 알리기 위한 Multicast Delegate를 구현합니다. 이를 통해, C++ 환경에서 UI를 구현할 경우 단순히 Player State의 Multicast Delegate에 자체적으로 정의한 콜백 함수만 등록해 주면 Attribute 수치 변화를 감지할 수 있게 됩니다.
3. 코드
1. PlayerStateBase.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "AbilitySystemInterface.h"
#include "02_GameplayAbility/BaseAttributeSet.h"
#include "02_GameplayAbility/BaseAbilitySet.h"
#include "PlayerStateBase.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogPlayerStateBase, Log, All)
/*
* @목적 : ASC에 등록된 AttributeSet의 각 Attribute 값 변화 이벤트를 전파하는 이벤트
* @설명 : Attribute 값 변화 이벤트 발생 시 이를 UI 등 다양한 곳에 알리기 위함
* @참조 : -
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FAnyAttributeValueChanged, FGameplayAttribute, Attribute, float, OldValue, float, NewValue);
class UPawnData;
class UBaseAttributeSet;
class UBaseAbilitySystemComponent;
/**
* Player State contaions pawn's info interacting with others
*/
UCLASS()
class AGEOFWOLVES_API APlayerStateBase : public APlayerState, public IAbilitySystemInterface
{
GENERATED_BODY()
//...
#pragma region Attribute Callbacks
protected:
/*
* @목적 : 캐릭터의 Attribute 수치 변화 이벤트에 등록할 콜백 함수
* @설명 : Ability System Component에서 관리하는 Attribute 항목의 수치 변화 이벤트에 등록할 콜백 함수입니다.
* HUD 구현을 위해 PS에서 제공하는 AttributeBase 관련 인터페이스로 활용 가능합니다(C++환경).
* @참조 : APlayerStateBase::InitializeGameplayAbilitySystem()
*/
void OnAttributeValueChanged(const FOnAttributeChangeData& Data);
public:
/*
* @목적 : ASC에 등록된 AttributeSet의 각 Attribute 값 변화 이벤트를 전파하는 이벤트
* @설명 : Attribute 값 변화 이벤트 발생 시 이를 UI 등 다양한 곳에 알리기 위함
* @참조 : -
*/
FAnyAttributeValueChanged OnAnyAttributeValueChanged;
#pragma endregion
};
ASC의 Attribute 수치 변화 이벤트에 등록할 콜백 함수(OnAttributeValueChanged)와 Multicast Delegate(OnAnyAttributeValueChanged)를 선언합니다. 콜백 함수는 Player State 자체적으로 Attribute 별 수치 변화 이벤트를 감지하기 위함이고, FAnyAttributeValueChanged 유형의 이벤트는 외부에서 해당 이벤트에 콜백 함수를 등록함으로써, Attribute 수치 변화 이벤트를 감지하기 위한 인터페이스로 활용하기 위함입니다.
2. PlayerStateBase.cpp
void APlayerStateBase::InitializeGameplayAbilitySystem()
{
check(PawnData);
if (const auto& Controller = Cast<AController>(GetOwner()))
{
if (const auto& Pawn = Controller->GetPawn())
{
AbilitySystemComponent->InitAbilityActorInfo(Pawn, Pawn);
if (PawnData->IsValidLowLevel())
{
UBaseAbilitySet* SetToGrant = PawnData->AbilitySet;
if (IsValid(SetToGrant))
{
// @설명 : 캐릭터의 기본 AttributeSet을 ASC에 최초 등록합니다.
{
SetToGrant->GiveStartupAttributeSetToAbilitySystem(AbilitySystemComponent, SetGrantedHandles, this);
// 각 Attribute 항목 수치 변화 이벤트에 콜백함수를 등록합니다.
for (auto& AS : AbilitySystemComponent->GetSpawnedAttributes())
{
if (IsValid(AS))
{
AttributeSet = AS;
TArray<FGameplayAttribute> Attributes = AttributeSet->GetAllAttributes();
for (const FGameplayAttribute& Attribute : Attributes)
{
FDelegateHandle DelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(this, &APlayerStateBase::OnAttributeValueChanged);
}
}
}
}
// @설명 : 캐릭터의 기본 Gameplay Effect를 ASC에 최초 등록/적용합니다.
{
SetToGrant->GiveStartupGameplayEffectToAbilitySystem(AbilitySystemComponent, SetGrantedHandles, this);
}
// @설명 : 캐릭터의 기본 Gameplay Ability를 ASC에 최초 등록/적용합니다.
{
SetToGrant->GiveStartupGameplayAbilityToAbilitySystem(AbilitySystemComponent, SetGrantedHandles, this);
}
// @TODO : ASC에 Startup GA, GE, AttributeSet의 등록 완료 이벤트 호출
}
}
}
}
}
void APlayerStateBase::OnAttributeValueChanged(const FOnAttributeChangeData& Data)
{
if (OnAnyAttributeValueChanged.IsBound())
{
OnAnyAttributeValueChanged.Broadcast(Data.Attribute, Data.OldValue, Data.NewValue);
}
}
"ASC -> Player State -> 외부"로 정리할 수 있습니다. Player State에선 "OnAttriuteValueChanged" 콜백 함수를 ASC의 모든 Attribute에 대하여 그 수치 변화 이벤트에 등록하고, 콜백 함수 내부에선 "FAnyAttributeValueChanged" 유형의 Multicast 이벤트에 등록된 외부 콜백 함수들을 호출해 줍니다.
3. TestWidget.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "GameplayEffectTypes.h"
#include "TestWidget.generated.h"
class APlayerStateBase;
/**
* @목적 : 캐릭터의 Attribute 관련 인터페이스 구현의 테스트를 위해 임시로 선언한 UI 클래스
* @설명 : 캐릭터의 ASC의 Attribute 값 변화 이벤트에 UI 클래스의 콜백 함수를 등록하여, 특정 Attribute 값을 실시간으로 가져옵니다.
* @참조 : -
*/
UCLASS()
class AGEOFWOLVES_API UTestWidget : public UUserWidget
{
GENERATED_BODY()
public:
UTestWidget(const FObjectInitializer& ObjectInitializer);
protected:
//~ Begin UUserWidget Interfaces
virtual void NativeOnInitialized();
virtual void NativePreConstruct();
virtual void NativeConstruct();
virtual void NativeDestruct();
//~ End UUserWidget Interface
#pragma region Attribute Value
protected:
/*
* @목적 : OwningPawn의 PlayerState의 Attribute 값 변화 이벤트에 등록할 콜백 함수입니다.
* @설명 : 해당 콜백 함수를 통해 HP, SP, MP 등의 캐릭터의 실시간 Attribute 값들을 가져와 UI에 반영할 수 있습니다.
*/
UFUNCTION()
void OnAttributeValueChanged(FGameplayAttribute Attribute, float OldValue, float NewValue);
#pragma endregion
};
정상적으로 동작하는지 임시 UI 클래스 "TestWidget" 클래스를 선언하여, Player State 클래스에서 제공하는 Attribute 수치 변화 이벤트 관련 인터페이스를 활용해 봤습니다. 우선, UFUNCTION()과 함께, FAnyAttributeValueChanged 유형의 함수 시그니처와 동일한 콜백 함수를 하나 만들어줬습니다.
4. TestWidget.cpp
void UTestWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
if (const auto PS = GetOwningPlayerState<APlayerStateBase>())
{
PS->OnAnyAttributeValueChanged.AddDynamic(this, &UTestWidget::OnAttributeValueChanged);
}
}
void UTestWidget::OnAttributeValueChanged(FGameplayAttribute Attribute, float OldValue, float NewValue)
{
if (Attribute.IsValid())
{
if (Attribute.AttributeName == "Health")
{
UE_LOG(LogTemp, Log, TEXT("Health : %f"), NewValue);
}
else if (Attribute.AttributeName == "Mana")
{
UE_LOG(LogTemp, Log, TEXT("Mana : %f"), NewValue);
}
else if (Attribute.AttributeName == "Stamina")
{
UE_LOG(LogTemp, Log, TEXT("Stamina : %f"), NewValue);
}
}
}
위처럼 UI 객체의 초기화 시점에 Player State에서 제공하는 이벤트 "OnAnyAttributeValueChanged"에 콜백 함수만 등록해 주면, 정상적으로 Attribute 수치 변화 이벤트를 감지할 수 있습니다. 게임 플레이 로그를 통해 위 결과를 확인해 볼 수 있습니다!
4. 참고
TestWidget 클래스 및 PlayerCharacter 클래스 내부에서 해당 Widget을 생성하고 ViewPort에 추가하는 코드는 테스트 코드이며, 본격적인 UI 구현 시작 전에 해당 클래스 및 코드 내용들을 모두 제거해 주세요! 주석을 통해 지워야 하는 코드들을 명시적으로 표시해 놓았습니다.
#3. Blueprint 환경
1. 개념
BlueprintAsyncActionBase 유형의 사용자 정의 클래스를 활용하여 UI 구현 과정에서 필요한 Attribute 관련 인터페이스를 제공합니다. UI 구현 과정에서 C++ 환경뿐만 아니라, Blueprint의 Event Graph 또한 활용되는 것을 고려하여, Attribute 수치 값 변화 이벤트를 Blueprint Node를 통해 노출시켜 개발자에게 편의성을 제공합니다.
2. BlueprintAsyncActionBase
"BlueprintAsyncActionBase"는 UE의 비동기 작업을 블루프린트에서 쉽게 사용할 수 있도록 설계된 클래스입니다. 해당 클래스를 상속받아 사용자 정의 비동기 작업을 구현할 수 있습니다. 주요 특징은 비동기 작업 지원, 커스텀 이벤트 생성, 그리고 별도의 하드 코딩 작업 없이 블루프린트만 사용해서 비동기 작업을 쉽게 통합할 수 있게 해 줍니다. 하지만, 비동기 작업 특성상 로직의 복잡성이 증가해 디버깅에 어려움이 있으며, 스레드 관리 혹은 리소스 할당과 관련된 성능 최적화를 고려해야 합니다.
3. 코드
1. AsyncTaskAttributeChanged.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AbilitySystemComponent.h"
#include "AsyncTaskAttributeChanged.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FAttributeValueChanged, FGameplayAttribute, Attribute, float, NewValue, float, OldValue);
/**
* @목적 : ASC의 Attribute 항목의 수치 값 변화 이벤트에 귀기울이는 잠복성 블루프린트 노드입니다.
* @설명 : UI 구현에 사용되며, Blueprint 환경에서 활용되는 노드를 C++ 환경에서 구현한 후, Editor에 노출시킵니다.
*/
UCLASS(BlueprintType, meta = (ExposedAsyncProxy = AsyncTask))
class AGEOFWOLVES_API UAsyncTaskAttributeChanged : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
#pragma region Attribute Value Change Interface
public:
UPROPERTY(BlueprintAssignable)
FAttributeValueChanged OnAttributeValueChanged;
/*
* @목적 : Attribute 항목의 수치 값 변화 이벤트에 등록되는 콜백 함수를 가진 AsyncAction의 생성자
* @설명 : 해당 콜백 함수는 ASC의 데이터 멤버, FActiveGameplayEffectsContainer,에서 관리하는 AttributeValueChangeDelegates(FOnGameplayAttributeValueChange) 목록에 추가됩니다.
* 어떤 GE에 의해 ASC에서 관리하는 Attribute 항목의 수치 값에 변화가 발생하면, AttributeValueChangeDelegates 목록에 저장된 콜백 함수들을 호출합니다.
* @참조 : -
*/
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTaskAttributeChanged* ListenToAttributeValueChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute);
/*
* @목적 : 두 개 이상의 Attribute 항목의 수치 값 변화 이벤트에 등록되는 콜백 함수를 갖는 AsyncAction의 생성자
* @설명 : 위와 동일
* @참조 : -
*/
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTaskAttributeChanged* ListenToAttributesValueChange(UAbilitySystemComponent* AbilitySystemComponent, TArray<FGameplayAttribute>& Attributes);
/*
* @목적 : Async Task의 메모리 해제
* @설명 : ListenToAttributeChange/ListenToAttributesChange 정적 함수 호출 시 매번 새로운 AsyncTask 객체를 생성하여, 메모리 관리가 중요합니다.
* 모든 동작을 완료하고, 해당 AsyncTask 객체의 메모리 해제를 위해 아래 함수를 호출해주어야합니다.
* @참조 : -
*/
UFUNCTION(BlueprintCallable)
void EndTask();
#pragma endregion
#pragma region Attribute Value Change Internal Settings
protected:
/*
* @목적 : Attribute 항목의 수치 값 변화 이벤트에 등록되는 콜백 함수
* @설명 : 해당 AsyncAction이 생성 시점에 전달받은 FGameplayAttribute 정보를 통해 ASC의 특정 Attribute 혹은 Attributes의 수치 값 변화 이벤트에 등록되는 콜백함수
* @참조 : -
*/
void AttributeChanged(const FOnAttributeChangeData& Data);
/*
* @목적 : 해당 AsyncTask의 소임을 다하고, Attribute 수치 값 변화 이벤트에 등록된 콜백 함수를 정리하고, 메모리 누수를 방지하기 위해 ASC의 참조를 들고있습니다.
* @설명 : -
* @참조 : -
*/
TWeakObjectPtr<UAbilitySystemComponent> ASC;
FGameplayAttribute AttributeListenTo;
TArray<FGameplayAttribute> AttributesListenTo;
#pragma endregion
};
AsyncTaskAttributeChanged 클래스는 정적 멤버 함수 2개 (ListenToAttributeValueChange/ListenToAttributesValueChange)를 활용해주어 진 ASC의 특정 Attribute의 수치 변화 이벤트를 구독하는 AsyncTask를 반환합니다.
"AsyncTaskAttributeChanged" 구현 과정에서 '팩토리 메서드' 패턴을 활용합니다. UE에서 제공하는 GAS 관련 '팩토리 메서드' 패턴은 객체의 생성을 서브클래스에 위임하는 디자인 패턴 중 하나로, 클래스의 인스턴스화 로직을 클라이언트로부터 분리해 캡슐화하는 것을 목적으로 합니다. "AsyncTaskAttributeChanged" 클래스 내에 정의된 정적 메서드들이 팩토리 역할을 수행하여, 특정 조건(ASC와 Attribute 타입)에 따라 UAsyncTaskAttributeChanged 인스턴스를 생성하고 반환합니다.
2. AsyncTaskAttributeChanged::ListenToAttributeValueChange()
UAsyncTaskAttributeChanged* UAsyncTaskAttributeChanged::ListenToAttributeValueChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute)
{
if (!IsValid(AbilitySystemComponent) && !Attribute.IsValid())
{
return nullptr;
}
// AsyncTask의 인스턴스 생성
UAsyncTaskAttributeChanged* AsyncTask = NewObject<UAsyncTaskAttributeChanged>();
AsyncTask->ASC = AbilitySystemComponent;
AsyncTask->AttributeListenTo = Attribute;
// AsyncTask의 콜백 함수를 ASC의 Attribute 수치 값 변화 이벤트에 등록
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(AsyncTask, &UAsyncTaskAttributeChanged::AttributeChanged);
return AsyncTask;
}
간단합니다. 인자로 전달받은 ASC와 Attribute를 활용해 해당 ASC에 등록된 Attribute의 수치 변화 이벤트를 구독해 주고, AsyncTask를 반환합니다.
4. 게임 플레이 화면
위에서 정의한 AsyncTask 객체 노드를 블루프린트 환경에 노출시켜 테스트 화면을 녹화했습니다. Sprint 시 캐릭터의 Stamina(Attribute)의 수치 변화 이벤트를 구독하는 AsyncTask 블루프린트 노드를 통해 현재 캐릭터의 Stamina 수치를 출력했습니다. 자세한 내용은 BP_AkaOni 파일의 Event Graph를 참고해 주세요.
5. 주의
Async Task가 더 이상 필요 없어진 시점에 반드시 "EndTask"를 호출해주어 리소스 관리를 해주어야 합니다!
'그룹프로젝트 > Dev' 카테고리의 다른 글
[GroupProject_AOW]#UI-1: UI 코드 고정 형식 (1) | 2024.09.26 |
---|