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에 등록하고 상호 참조하는 구조를 구현하세요.