UI연동
이전 포스팅에서 GE를 통해 Attribute에 영향을 주는 것까지 구현하였다.
이번 포스팅에서는 Attribute가 GE에 의해 변경되면 UI가 업데이트되도록 구현해 보자.
그러기 위해서는 Helath값을 나타내는 UserWidget을 생성하고 이를 캐릭터에 붙일 수 있게 WidgetComponent를 제작하여 진행할 것이다.
UserWidget에서는 ASC로부터 Attribute가 변경될 때 Delegate를 받아 업데이트할 수 있게 구현할 예정이다.
UserWidget & WidgetComponent 제작
캐릭터에 HpBar를 붙이기 위해 WidgetComponent가 필요하다.
UserWidet에서는 Attribute가 변경될 때마다 업데이트되어야 하기 때문에 캐릭터의 ASC가 있어야 한다.
WidgetComponent에서 해당 widget에게 Owner의 ASC를 전달해 주는 역할을 해야 한다.
widget을 초기화하는 InitWidget()이 적절한 함수이다.
//.h
virtual void InitWidget() override;
//.cpp
void UGASWidgetComponent::InitWidget()
{
Super::InitWidget();
UGASWidget* GASUserWidget = Cast<UGASWidget>(GetWidget());
if (GASUserWidget)
{
GASUserWidget->SetAbilitySystemComponent(GetOwner());
}
}
UserWidget에서는 ASC를 전달받아 설정한 뒤, Attribute가 변경되었을 때 실행되는 Delegate인 GetGameplayAttributeValueChangeDelegate를 통해 해당 Attribute를 받아와 바인딩하여 Callback함수를 실행하면 된다.
지금은 HpBar만 만들지만 다양한 widget을 추가적으로 만들 수 있기 때문에 ASC를 받아 설정하는 부분을 담당하는 부모 클래스를 만들고 세부적인 구현은 자식 클래스에서 구현하도록 설계하는 것이 좋다.
//GASWidget.h
...
public:
virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
virtual void SetAbilitySystemComponent(AActor* InOwner);
protected:
UPROPERTY(EditAnywhere, Category = GAS)
TObjectPtr<UAbilitySystemComponent> ASC;
//GASWidget.cpp
...
UAbilitySystemComponent* UGASWidget::GetAbilitySystemComponent() const
{
return ASC;
}
void UGASWidget::SetAbilitySystemComponent(AActor* InOwner)
{
if (IsValid(InOwner))
{
ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(InOwner);
}
}
//HpBarUserWidget.h
...
virtual void OnHealthChanged(const FOnAttributeChangeData& ChangeData);
virtual void OnMaxHealthChanged(const FOnAttributeChangeData& ChangeData);
void UpdateHpBar();
//HpBarUserWidget.cpp
...
void UGASHpBarUserWidget::SetAbilitySystemComponent(AActor* InOwner)
{
Super::SetAbilitySystemComponent(InOwner);
if (ASC)
{
ASC->GetGameplayAttributeValueChangeDelegate(UGASHpBarUserWidget::GetHealthAttribute()).AddUObject(this, &UABGASHpBarUserWidget::OnHealthChanged);
ASC->GetGameplayAttributeValueChangeDelegate(UGASHpBarUserWidget::GetMaxHealthAttribute()).AddUObject(this, &UABGASHpBarUserWidget::OnMaxHealthChanged);
const UCharacterAttributeSet* CurrentAttributeSet = ASC->GetSet<UCharacterAttributeSet>();
if (CurrentAttributeSet)
{
CurrentHealth = CurrentAttributeSet->GetHealth();
CurrentMaxHealth = CurrentAttributeSet->GetMaxHealth();
if (CurrentMaxHealth > 0.f) UpdateHpBar();
}
}
}
void UGASHpBarUserWidget::OnHealthChanged(const FOnAttributeChangeData& ChangeData)
{
CurrentHealth = ChangeData.NewValue;
UpdateHpBar();
}
void UGASHpBarUserWidget::OnMaxHealthChanged(const FOnAttributeChangeData& ChangeData)
{
CurrentMaxHealth = ChangeData.NewValue;
UpdateHpBar();
}
void UGASHpBarUserWidget::UpdateHpBar()
{
if (PbHpBar)
{
PbHpBar->SetPercent(CurrentHealth / CurrentMaxHealth);
}
if (TxtHpStat)
{
TxtHpStat->SetText(FText::FromString(FString::Printf(TEXT("%.0f/%.0f"), CurrentHealth, CurrentMaxHealth)));
}
}
이렇게 제작한 widget을 캐릭터에 WidgetComponent를 통해 부착하면 된다.
//.h
...
UPROPERTY(VisibleAnywhere)
TObjectPtr<class UABGASWidgetComponent> HpBar;
//.cpp
AGASCharacterPlayer::AGASCharacterPlayer()
{
...
HpBar = CreateDefaultSubobject<UGASWidgetComponent>(TEXT("Widget"));
HpBar->SetupAttachment(GetMesh());
HpBar->SetRelativeLocation(FVector(0.f, 0.f, 180.f));
static ConstructorHelpers::FClassFinder<UUserWidget> HpBarWidgetRef(TEXT("위젯경로"));
if (HpBarWidgetRef.Succeeded())
{
HpBar->SetWidgetClass(HpBarWidgetRef.Class);
HpBar->SetWidgetSpace(EWidgetSpace::Screen);
HpBar->SetDrawSize(FVector2D(200.f, 20.f));
HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
...
}
캐릭터 Death
캐릭터가 피해를 받아 Health Attribute가 줄어들다 0보다 작거나 같아진다면 캐릭터가 사망하도록 구현해 보자.
GE에 의해 Health Attribute가 줄어드는 부분에서 변경된 Health값이 0보다 작은지 체크해 주면 된다.
만약 0보다 작거나 같다면 Delegate를 발동시키고 캐릭터에서는 AttributeSet에서 이 Delegate를 바인딩하여 Callback함수를 실행하면 된다.
이때, AttributeSet을 받아올 때는 const로 받아와야 한다.
그렇기 때문에 AttributeSet안에 있는 Delegate에 바인딩하는 것이 허용되지 않는다.
이를 구현하기 위해서는 해당 Delegate에 mutable 키워드를 이용하여 예외적으로 변경가능하게 만들어 주어야 한다.
//AttributeSet.h
public:
...
mutable FOutOfHealthDelegate OnOutOfHealth;
//AttributeSet.cpp
void UCharacterAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
float MinimumHealth = 0.f;
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), MinimumHealth, GetMaxHealth()));
}
else if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
SetHealth(FMath::Clamp(GetHealth() - GetDamage(), MinimumHealth, GetMaxHealth()));
SetDamage(0.f);
}
if ((GetHealth() < 0.f) && !bOutOfHealth)
{
Data.Target.AddLooseGameplayTag(ABTAG_CHARACTER_ISDEAD);
OnOutOfHealth.Broadcast();
}
bOutOfHealth = (GetHealth() < 0.f);
}
void AGASCharacterNPC::PossessedBy(AController* NewController)
{
...
AttributeSet->OnOutOfHealth.AddDynamic(this, &ThisClass::OnOutOfHealth);
...
}
void AGASCharacterNPC::OnOutOfHealth()
{
//Play Anim...
SetDead();
}
Delegate를 발동하는 부분을 보면 특이한 함수가 하나 있다.
Data.Target.AddLooseGameplayTag(ABTAG_CHARACTER_ISDEAD);
AddLooseGameplayTag는 ASC에 원하는 태그를 부여하는 함수이다.
PostGameplayEffectExcute함수의 Data에는 Target의 ASC에 대한 정보가 있기 때문에 이를 통해 Tag를 부여하는 부분이다.
무적 기능
GE의 Duration Policy에는 즉각적으로 영향을 주는 Instant라는 옵션도 있지만, 일정 기간이나 영원히 지속되는 옵션도 있다.
이를 이용하여 잠깐동안 무적이 되는 기능을 구현해 보자.
우선 무적 기능을 실행하는 GA를 만들어야 한다.
해당 GA는 Ability가 실행되면 FGameplayEffectSpecHandle을 만들어 GE를 실행한 뒤 종료한다.
무적 기능을 수행하는 GE는 다음과 같다.
해당 GE는 3초간 무적 상태 태그를 부여한다.
제작한 GA, GE를 적용하는 부분은 다음과 같다.
NPC 캐릭터는 게임이 시작되면 InvinsibleGA를 부여받고 3초가 지난 뒤 해당 GA를 실행한다.
그러면 해당 GA의 ActivateAbility가 실행되어 무적 태그를 ASC에 추가한다.
캐릭터에 태그가 추가되어 무적 상태가 된다면 이를 나타내기 위한 UI가 변화하는 것이 자연스럽다.
따라서, 무적 상태가 되면 HpBar의 색이 변경되게 구현해 보자.
//UGASHpBarUserWidget.cpp
void UGASHpBarUserWidget::SetAbilitySystemComponent(AActor* InOwner)
{
...
if (ASC)
{
...
ASC->RegisterGameplayTagEvent(ABTAG_CHARACTER_INVINSIBLE, EGameplayTagEventType::NewOrRemoved).AddUObject(this, &UGASHpBarUserWidget::OnInvinsibleTagChanged);
PbHpBar->SetFillColorAndOpacity(HealthColor);
...
}
}
void UGASHpBarUserWidget::OnInvinsibleTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
if (NewCount > 0)
{
PbHpBar->SetFillColorAndOpacity(InvinsibleColor);
PbHpBar->SetPercent(1.f);
}
else
{
PbHpBar->SetFillColorAndOpacity(HealthColor);
UpdateHpBar();
}
}
RegisterGameplayTagEvent를 통해 Owner로부터 전달받은 ASC의 태그가 변경되어 이벤트가 발생할 때 특정 함수를 실행할 수 있다.
이때, 실행되는 Callback함수의 두 번째 매개변수는 지정한 태그의 개수이다.
태그의 개수가 0보다 크다면 무적 태그가 추가된 것이기 때문에 무적을 표현하게끔 HpBar를 설정해 주면 된다.
하지만, 지금까지의 내용은 태그에 의한 상태들만 표시해 줄 뿐이지 실제로 무적이 된 상태는 아니다.
실제로 무적 효과를 주기 위해서는 AttributeSet에서 GE에 영향을 주기 전에 태그를 검사하여 여부를 결정하면 된다.
//CharacterAttributeSet.cpp
bool UCharacterAttributeSet::PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data)
{
...
if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
if (Data.EvaluatedData.Magnitude > 0.f)
{
//Check Tag
if (Data.Target.HasMatchingGameplayTag(ABTAG_CHARACTER_INVINSIBLE))
{
Data.EvaluatedData.Magnitude = 0.f;
return false;
}
}
}
...
}
HasMatchingGameplayTag라는 함수를 이용하여 타깃 ASC에 무적태그가 존재하는지 확인한 뒤, 만약 존재한다면 대미지를 0으로 만들어 피해를 입지 않게 만들면 된다.
버프 기능
무적 기능과 비슷하게 GE의 Duration Policy를 설정하여 일시적인 버프를 구현할 수 있다.
공격을 성공하면 최대 4개까지 Stack 되는 공격 사거리 버프를 주는 GE이다.
공격 판정을 수행하는 GA에서 공격 판정 결과를 처리하는 부분에서 해당 GE를 Owner에게 부여해 주면 된다.
void UGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
{
...
FGameplayEffectSpecHandle BuffEffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackBuffEffect);
if (BuffEffectSpecHandle.IsValid())
{
ApplyGameplayEffectSpecToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, BuffEffectSpecHandle);
}
}
...
}
그렇다면, 공격을 성공하면 AttackRadius가 점차 커지게 된다.
하지만, 이는 영구히 증가하는 게 아니라 일시적으로 증가하는 것이다.
즉, AttributeSet의 Base Value가 변경되는 것이 아니라 Current Value가 변경된다는 것을 유의해야 한다.