먼저 캐릭터에게 Health가 있어야 회복하든 말든 할 것이다.
작은 프로젝트나 프로토타입용 프로젝트라면 Health를 캐릭터에게 멤버변수로 직접 선언해주겠지만, GAS를 본격적으로 사용하겠다면 AttributeSet에 선언해주면 된다.
// MyAttributeSet.h
// Getter, Setter, Initter를 자동 생성해주는 매크로를 호출하기 위해 정의하는 구문
// 이 구문이 없으면 Getter, Setter, Initter를 변수마다 모두 작성해야 함
// 즉 Boilerplate 코드를 줄여주는 구문
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName
// 일반적인 자료형 대신 FGameplayAttributeData를 사용할 경우, 클라이언트 예측 등의 네트워크 로직이 자동으로 적용됨
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Vital Attributes")
FGameplayAttributeData Health;
// 최상단 define을 통해 Getter, Setter, Initter 자동 생성
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Health);
//AttributeSet.cpp
UAuraAttributeSet::UAuraAttributeSet()
{
// 자동 생성된 Initter로 생성자에서 값 할당
InitHealth(50.f);
}
void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 기존까지 사용하던 매크로와 달리 NOTIFY가 추가됐다.
// OnChanged와 Always 2가지를 설정할 수 있다.
// OnChanged는 값에 변화가 있는 경우만 OnRep 함수가 호출되고, Always는 변화하지 않았더라도 호출된다.
DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
}
void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) const
{
// GAS 후처리 흐름을 자동으로 실행하는 매크로로서, 클라이언트 예측 등의 로직이 여기에 포함
GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth);
}
이제 AttributeSet에 Health가 있다. (MaxHealth는 생략했다.)
맵에 배치할 수 있는 포션 액터를 만들어보자.
// EffectActor.h
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Applied Effects")
TSubclassOf<UGameplayEffect> InstantGameplayEffectClass;
// EffectActor.cpp
void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
// FunctionLibrary를 통해 Target의 AbilitySystemComponent 가져오기
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
if (TargetASC == nullptr) return;
check(GameplayEffectClass);
// GameplayEffect가 어떻게 적용됐는지에 대한 정보를 가진 구조체 선언
// 누가 때렸는지, 누가 맞았는지, 어떤 속성의 공격인지, 데미지는 몇인지 등 로그 같은 개념
FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
// 어떤 객체에 의해 발생한 Effect인지 추가
EffectContextHandle.AddSourceObject(this);
// GameplayEffectSpecHandle 생성, Spec이란 Effect의 틀로서, 여러 곳에서 같은 효과를 적용하고 싶을 때 사용하면 좋음
// Spec 없이 재사용을 염두하지 않고 간편하게 사용하는 경우 ApplyGameplayEffectToSelf 호출
FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass, 1.f, EffectContextHandle);
TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
}
StaticMesh나 콜리전 역할을 하는 SphereComponent는 블루프린트에서 구현한다.
StaticMesh가 아닌 SkeletalMesh가 필요하다면? 그에 따른 cpp 클래스를 또 선언할 것인가?
SphereComponent로 판정을 볼 게 아니라면? WidgetComponent에 버튼을 표시하고 그걸 눌렀을 때 회복할 거라면?
변수가 이렇게나 다양한데 StaticMeshCoponent와 SphereComponent로 굳혀버리면 클래스가 너무 많아지지 않을까?
그럴 바엔 기능 구현만 한 액터를 만들고, 그것을 상속받는 블루프린트에서 이벤트 트리거를 구현해주는 편이 낫다.
위에서 선언한 InstantGameplayEffectClass에 할당해줄 에셋이다.
해당 에셋 내부에 적용될 GameplayEffect를 초기화한 모습이다.
Duration Policy는 지속시간에 관련된 부분이다.
Attribute는 자동 추적, Modifier엔 Add 말고도 Divide, Multiply가 있다.
정리하면 한 번에 25 수치를 Health에 증가시킨다는 뜻이다.
GameplayEffect에 대해선 더 자세히 게시글을 작성하겠다.
사진과 같이 할당해주면 잘 작동한다.