PART 4 - 강의 3/8
AttributeSet 구현
캐릭터 스탯 시스템의 핵심 - 속성 정의와 복제
01
AttributeSet 기본 구조
ATTRIBUTE_ACCESSORS 매크로와 속성 정의
MyAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.generated.h"
// 속성 접근자 매크로 - 필수!
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class MYGAME_API UMyAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UMyAttributeSet();
// 리플리케이션 등록
virtual void GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 속성 변경 전 호출 (클램핑)
virtual void PreAttributeChange(
const FGameplayAttribute& Attribute, float& NewValue) override;
// GameplayEffect 실행 후 호출 (데미지 처리)
virtual void PostGameplayEffectExecute(
const FGameplayEffectModCallbackData& Data) override;
// ========== 체력 관련 ==========
UPROPERTY(BlueprintReadOnly, Category = "Health",
ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health)
UPROPERTY(BlueprintReadOnly, Category = "Health",
ReplicatedUsing = OnRep_MaxHealth)
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth)
// ========== 마나 관련 ==========
UPROPERTY(BlueprintReadOnly, Category = "Mana",
ReplicatedUsing = OnRep_Mana)
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Mana)
UPROPERTY(BlueprintReadOnly, Category = "Mana",
ReplicatedUsing = OnRep_MaxMana)
FGameplayAttributeData MaxMana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxMana)
// ========== 전투 스탯 ==========
UPROPERTY(BlueprintReadOnly, Category = "Combat",
ReplicatedUsing = OnRep_AttackPower)
FGameplayAttributeData AttackPower;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, AttackPower)
UPROPERTY(BlueprintReadOnly, Category = "Combat",
ReplicatedUsing = OnRep_DefensePower)
FGameplayAttributeData DefensePower;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, DefensePower)
// ========== 메타 속성 (리플리케이션 X) ==========
// 데미지 계산용 임시 속성
UPROPERTY(BlueprintReadOnly, Category = "Meta")
FGameplayAttributeData IncomingDamage;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, IncomingDamage)
protected:
// OnRep 함수들
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
UFUNCTION()
virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth);
UFUNCTION()
virtual void OnRep_Mana(const FGameplayAttributeData& OldMana);
UFUNCTION()
virtual void OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana);
UFUNCTION()
virtual void OnRep_AttackPower(const FGameplayAttributeData& OldAttackPower);
UFUNCTION()
virtual void OnRep_DefensePower(const FGameplayAttributeData& OldDefensePower);
private:
void ClampAttribute(const FGameplayAttribute& Attribute, float& NewValue) const;
};
02
AttributeSet 구현
초기화, 리플리케이션, 클램핑
MyAttributeSet.cpp
#include "MyAttributeSet.h"
#include "Net/UnrealNetwork.h"
#include "GameplayEffectExtension.h"
#include "AbilitySystemComponent.h"
UMyAttributeSet::UMyAttributeSet()
{
// 기본값 설정
InitHealth(100.0f);
InitMaxHealth(100.0f);
InitMana(50.0f);
InitMaxMana(50.0f);
InitAttackPower(10.0f);
InitDefensePower(5.0f);
}
void UMyAttributeSet::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// REPNOTIFY_Always: 값이 같아도 항상 OnRep 호출
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health,
COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxHealth,
COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Mana,
COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxMana,
COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, AttackPower,
COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, DefensePower,
COND_None, REPNOTIFY_Always);
// IncomingDamage는 메타 속성이므로 복제하지 않음
}
void UMyAttributeSet::PreAttributeChange(
const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
// 값 클램핑
ClampAttribute(Attribute, NewValue);
}
void UMyAttributeSet::ClampAttribute(
const FGameplayAttribute& Attribute, float& NewValue) const
{
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
}
else if (Attribute == GetManaAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxMana());
}
else if (Attribute == GetMaxHealthAttribute())
{
NewValue = FMath::Max(NewValue, 1.0f);
}
else if (Attribute == GetMaxManaAttribute())
{
NewValue = FMath::Max(NewValue, 0.0f);
}
}
03
PostGameplayEffectExecute
데미지 처리와 사망 로직
MyAttributeSet.cpp (계속)
void UMyAttributeSet::PostGameplayEffectExecute(
const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
// 데미지 처리
if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalDamage = GetIncomingDamage();
SetIncomingDamage(0.0f); // 메타 속성 리셋
if (LocalDamage > 0.0f)
{
// 방어력 적용
// 공식: FinalDamage = Damage * (100 / (100 + Defense))
const float Defense = GetDefensePower();
const float DamageAfterDefense =
LocalDamage * (100.0f / (100.0f + Defense));
// 체력 감소
const float NewHealth = GetHealth() - DamageAfterDefense;
SetHealth(FMath::Clamp(NewHealth, 0.0f, GetMaxHealth()));
// 데미지 로그
UE_LOG(LogTemp, Log,
TEXT("Damage: %.2f -> After Defense: %.2f, New Health: %.2f"),
LocalDamage, DamageAfterDefense, GetHealth());
// 사망 처리
if (GetHealth() <= 0.0f)
{
HandleDeath(Data);
}
}
}
// MaxHealth 변경 시 현재 체력 비율 유지
if (Data.EvaluatedData.Attribute == GetMaxHealthAttribute())
{
// 현재 체력이 MaxHealth를 초과하지 않도록
if (GetHealth() > GetMaxHealth())
{
SetHealth(GetMaxHealth());
}
}
}
void UMyAttributeSet::HandleDeath(const FGameplayEffectModCallbackData& Data)
{
// 사망 이벤트 브로드캐스트
if (AActor* OwningActor = GetOwningActor())
{
// 사망 태그 추가
if (UAbilitySystemComponent* ASC = GetOwningAbilitySystemComponent())
{
FGameplayTagContainer DeathTag;
DeathTag.AddTag(FGameplayTag::RequestGameplayTag(
FName("State.Dead")));
ASC->AddLooseGameplayTags(DeathTag);
// 모든 활성 어빌리티 취소
ASC->CancelAllAbilities();
}
UE_LOG(LogTemp, Log, TEXT("%s has died"), *OwningActor->GetName());
}
}
// ========== OnRep 함수들 ==========
void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth);
}
void UMyAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxHealth, OldMaxHealth);
}
void UMyAttributeSet::OnRep_Mana(const FGameplayAttributeData& OldMana)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Mana, OldMana);
}
void UMyAttributeSet::OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxMana, OldMaxMana);
}
void UMyAttributeSet::OnRep_AttackPower(const FGameplayAttributeData& OldAttackPower)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, AttackPower, OldAttackPower);
}
void UMyAttributeSet::OnRep_DefensePower(const FGameplayAttributeData& OldDefensePower)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, DefensePower, OldDefensePower);
}
04
속성 변경 감지
델리게이트를 통한 UI 업데이트
C++
// UI나 다른 시스템에서 속성 변경 감지하기
void AMyHUD::BindToAttributeChanges()
{
if (AMyCharacter* Character = Cast<AMyCharacter>(GetOwningPawn()))
{
if (UAbilitySystemComponent* ASC = Character->GetAbilitySystemComponent())
{
if (UMyAttributeSet* AttributeSet = Character->GetAttributeSet())
{
// Health 변경 감지
HealthChangedDelegate = ASC->GetGameplayAttributeValueChangeDelegate(
AttributeSet->GetHealthAttribute())
.AddUObject(this, &AMyHUD::OnHealthChanged);
// MaxHealth 변경 감지
MaxHealthChangedDelegate = ASC->GetGameplayAttributeValueChangeDelegate(
AttributeSet->GetMaxHealthAttribute())
.AddUObject(this, &AMyHUD::OnMaxHealthChanged);
// Mana 변경 감지
ManaChangedDelegate = ASC->GetGameplayAttributeValueChangeDelegate(
AttributeSet->GetManaAttribute())
.AddUObject(this, &AMyHUD::OnManaChanged);
}
}
}
}
void AMyHUD::OnHealthChanged(const FOnAttributeChangeData& Data)
{
// Data.OldValue - 이전 값
// Data.NewValue - 새 값
float HealthPercent = 0.0f;
if (UMyAttributeSet* AS = GetAttributeSet())
{
HealthPercent = Data.NewValue / AS->GetMaxHealth();
}
// UI 업데이트
UpdateHealthBar(HealthPercent);
// 데미지/힐 표시
float Delta = Data.NewValue - Data.OldValue;
if (Delta < 0)
{
ShowDamageNumber(FMath::Abs(Delta));
}
else if (Delta > 0)
{
ShowHealNumber(Delta);
}
}
SUMMARY
핵심 요약
- ATTRIBUTE_ACCESSORS 매크로로 속성 접근자 자동 생성
- PreAttributeChange에서 값 클램핑 처리
- PostGameplayEffectExecute에서 데미지/사망 로직 구현
- 메타 속성(IncomingDamage)은 계산용으로만 사용, 복제하지 않음
- GetGameplayAttributeValueChangeDelegate로 UI 바인딩
PRACTICE
도전 과제
배운 내용을 직접 실습해보세요
실습 1: RPG AttributeSet 기본 구현
UAttributeSet을 상속하여 Health, MaxHealth, Mana, MaxMana, AttackPower, Defense를 UPROPERTY(BlueprintReadOnly, ReplicatedUsing)로 선언하세요. GAMEPLAYATTRIBUTE_REPNOTIFY 매크로를 사용하세요.
실습 2: Attribute 클램핑 구현
PreAttributeChange()에서 Health가 0~MaxHealth 범위를 벗어나지 않도록 클램핑하세요. PostGameplayEffectExecute()에서 데미지 적용 후 체력이 0 이하일 때 사망 처리 로직을 구현하세요.
심화 과제: 다중 AttributeSet 설계
URPGCombatAttributeSet(AttackPower, Defense, CritRate), URPGVitalAttributeSet(Health, Mana, Stamina), URPGProgressAttributeSet(Experience, Level)로 분리하세요. 여러 AttributeSet을 하나의 ASC에 등록하고 상호 참조하는 구조를 구현하세요.