AFO_Refactor 개발일지 #4
GAS 실전 적용
AFGE_Damage · AFGE_ManaCost · AFGE_Slow · PostGameplayEffectExecute · 버그 수정
🎯 이번 작업 목표
#3에서 GAS 기반 셋업(ASC, AttributeSet, Interface)을 완료했다. 이번엔 실제 전투 코드를 GAS로 교체한다.
| 기존 | 교체 후 |
|---|---|
Health -= Damage 직접 차감 |
AFGE_Damage GameplayEffect |
PS->ConsumeMana() 직접 차감 |
AFGE_ManaCost GameplayEffect |
AFStatusEffectComponent 타이머 |
AFGE_Slow Duration GameplayEffect |
HandleDeath() 직접 호출 |
PostGameplayEffectExecute 콜백 |
⚔️ Phase 2 Step 1 — AFGE_Damage
SetByCallerMagnitude 패턴
데미지 수치는 스킬마다 다르므로, GE 클래스에 고정값을 넣는 대신 런타임에 수치를 주입하는 SetByCaller 패턴을 사용했다.
// AFGE_Damage.cpp — Data.Damage 태그로 수치 수신 대기 UAFGE_Damage::UAFGE_Damage() { DurationPolicy = EGameplayEffectDurationType::Instant; FSetByCallerFloat SetByCaller; SetByCaller.DataTag = FGameplayTag::RequestGameplayTag("Data.Damage"); ModInfo.ModifierMagnitude = FGameplayEffectModifierMagnitude(SetByCaller); Modifiers.Add(ModInfo); } // AFAttributeComponent.cpp — 호출 시점에 실제 수치 주입 SpecHandle.Data->SetSetByCallerMagnitude( FGameplayTag::RequestGameplayTag("Data.Damage"), -FinalDamage // 음수 = HP 감소 ); ASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
💡 트러블슈팅: GE 생성자에서 FGameplayEffectModifierMagnitude의 필드에 직접 대입하는 방식이 UE5.5에서 protected라 빌드 실패. FSetByCallerFloat를 생성자로 전달하는 방식으로 수정했다.
🔔 PostGameplayEffectExecute — 사망 처리 연결
GE가 AttributeSet에 적용된 직후 호출되는 콜백이다. HP 클램핑, UI 동기화, 사망 처리를 여기서 담당한다.
void UAFAttributeSet::PostGameplayEffectExecute(
const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 1. HP 클램핑
SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
// 2. PlayerState UI 동기화
AAFPlayerState* PS = Cast<AAFPlayerState>(GetOwningActor());
if (PS) PS->SetHealth(GetHealth(), GetMaxHealth());
// 3. 사망 처리 — IsDead() 가드로 중복 호출 방지
if (GetHealth() <= 0.f && PS && !PS->IsDead())
{
Character->StartDeath(InstigatorController);
}
}
}
트러블슈팅 — HandlePlayerDeath 조기 return 버그
문제 코드
// AFAttributeSet.cpp if (GetHealth() <= 0.f && PS && !PS->IsDead()) { PS->SetDead(true); // ← 여기서 먼저 true로 설정 Character->StartDeath(InstigatorController); // → HandlePlayerDeath 진입 시 IsDead() == true // → if (VictimPS->IsDead()) return; ← 즉시 return! // → 킬 카운트, 리스폰, 킬 로그 전부 실행 안 됨 }
수정 — SetDead(true)를 HandlePlayerDeath에게 맡김
// PostGameplayEffectExecute에서 SetDead(true) 제거 if (GetHealth() <= 0.f && PS && !PS->IsDead()) { // SetDead(true) 제거 — HandlePlayerDeath가 처리 Character->StartDeath(InstigatorController); }
🔮 Phase 2 Step 3 — AFGE_ManaCost
기존 PS->ConsumeMana()를 GE 기반으로 교체했다. TryConsumeMana() 헬퍼 함수를 추가해서 마나 잔량 체크 → GE 적용을 하나의 함수로 묶었다.
BEFORE
if (!PS->ConsumeMana(SkillEManaCost)) return;
AFTER
bool AAFPlayerCharacter::TryConsumeMana(float Amount)
{
UAbilitySystemComponent* ASC = GetAbilitySystemComponent();
if (!ASC) return false;
const UAFAttributeSet* AS = ASC->GetSet<UAFAttributeSet>();
if (!AS || AS->GetMana() < Amount) return false; // 잔량 체크
FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(
UAFGE_ManaCost::StaticClass(), 1.f, Context);
Spec.Data->SetSetByCallerMagnitude(
FGameplayTag::RequestGameplayTag("Data.ManaCost"), -Amount);
ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());
return true;
}
// 사용처
if (!TryConsumeMana(SkillEManaCost)) return;
🐌 Phase 2 Step 4 — AFGE_Slow
기존 타이머 기반 슬로우를 GE Duration으로 교체했다. Duration을 SetByCaller로 런타임에 전달한다.
UAFGE_Slow::UAFGE_Slow()
{
DurationPolicy = EGameplayEffectDurationType::HasDuration;
FSetByCallerFloat DurationCaller;
DurationCaller.DataTag = FGameplayTag::RequestGameplayTag("Data.SlowDuration");
DurationMagnitude = FGameplayEffectModifierMagnitude(DurationCaller);
// Effect.Slowed 태그 부착 — 중복 슬로우 방지
// UE5.3+ 방식: UTargetTagsGameplayEffectComponent 사용
UTargetTagsGameplayEffectComponent* TagComponent =
CreateDefaultSubobject<UTargetTagsGameplayEffectComponent>(TEXT("TargetTagsComponent"));
GEComponents.Add(TagComponent);
}
💡 트러블슈팅: GE 생성자에서 FindOrAddComponent() 호출 시 크래시 발생. UE 엔진 소스 확인 결과 생성자(IsInConstructor) 내에서 AddComponent를 호출하면 Fatal 조건에 걸린다. CreateDefaultSubobject + GEComponents.Add() 방식으로 수정했다.
🔧 MaxHealth/MaxMana 불일치 문제 해결
GAS 도입 후 발견된 추가 문제였다. UAFAttributeSet 생성자에 고정값(300/1000)이 있는데, 캐릭터별 스탯 DataTable 값이 반영되지 않았다.
// 해결: PS->SetHealth() 호출 시 AttributeSet도 함께 동기화 void AAFPlayerState::SetHealth(float NewHealth, float NewMaxHealth) { // 레거시 변수 업데이트 CurrentHealth = FMath::Clamp(NewHealth, 0.f, NewMaxHealth); MaxHealth = NewMaxHealth; // AttributeSet도 함께 동기화 if (AbilitySystemComponent) { if (UAFAttributeSet* AS = const_cast<UAFAttributeSet*>( AbilitySystemComponent->GetSet<UAFAttributeSet>())) { AS->InitializeHealthValues(NewMaxHealth); AS->SetHealth(FMath::Clamp(NewHealth, 0.f, NewMaxHealth)); } } }
🐛 추가 버그 수정
AnimInstance 바인딩 타이밍 누락
호스트(클라이언트1) 폰은 월드 시작 직후 스폰되어 BeginPlay() 시점에 GetAnimInstance()가 null일 수 있다. 이 경우 OnMontageEnded 바인딩이 영구 누락되어 스킬 사용 후 이동이 멈추는 버그가 발생했다.
// 수정: AnimInstance null이면 OnAnimInitialized 폴백 바인딩 if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance()) { AnimInstance->OnMontageEnded.AddDynamic( this, &AAFPlayerCharacter::OnAttackMontageEnded); } else { GetMesh()->OnAnimInitialized.AddDynamic( this, &AAFPlayerCharacter::OnAnimInstanceInitialized); }
bIsUsingSkill 플래그 오남용
여러 캐릭터 클래스에서 bIsUsingSkill = true를 설정하고 복구 경로가 없어 이동이 영구 잠금되는 버그 3개를 수정했다.
| 버그 | 원인 | 수정 |
|---|---|---|
| 다크나이트 강공격 후 멈춤 | 강공격 판정에서 bIsUsingSkill = true 설정 |
해당 라인 제거 |
| 늑대인간 Q 후 멈춤 | 즉발 버프인데 bIsUsingSkill = true, 복구 경로 없음 |
플래그 제거 + 중복 발동 가드 추가 |
| 강공격 종료 시 공통 | bIsUsingSkill 체크로 UnlockMovement 건너뜀 |
강공격 종료 시 bIsUsingSkill = false 강제 리셋 |
✅ PIE 테스트 결과
✅ HP 감소 → 체력바 갱신 정상
✅ HP 0 → 사망 몽타주 → 리스폰 정상
✅ 킬 스코어 반영 정상
✅ 마나 소모 및 부족 시 스킬 차단 정상
✅ 슬로우 적용/해제 정상
✅ 스킬 사용 후 이동 복원 정상
📌 이번 작업에서 배운 것
상태 플래그 설정 순서가 핵심이다.
HandlePlayerDeath에 IsDead() 중복 방지 가드가 있는데, GAS 콜백에서 SetDead(true)를 먼저 호출하면 가드에 걸려서 킬 카운트/리스폰/킬 로그가 전부 실행되지 않는다. 어떤 함수가 상태를 변경하는지, 그 순서가 다른 로직에 영향을 주는지 항상 추적해야 한다.
호스트와 클라이언트는 초기화 타이밍이 다르다.
리슨 서버(호스트) 폰은 월드 시작 직후 스폰되어 AnimInstance가 아직 초기화되지 않은 상태일 수 있다. 클라이언트 폰은 늦게 스폰되어 이 문제를 피한다. "클라이언트1에서만 발생하는 버그"의 원인은 대부분 이 타이밍 차이에서 온다.
📌 다음 포스팅
[AFO_Refactor #5] 최적화 — 드로우콜 측정 + Before/After
stat scenerendering으로 드로우콜 측정, 머티리얼 인스턴싱 적용, Tick 제거 Before/After 수치 기록.
'개인프로젝트 > AFO_Refactor' 카테고리의 다른 글
| [AFO_Refactor #3] GAS 도입 — 왜, 무엇을, 어떻게 (Phase 1 셋업) (0) | 2026.06.17 |
|---|---|
| [AFO_Refactor #2] 매 프레임 Cast 체인 제거 - bIsSprinting 베이스 클래스 이동 (0) | 2026.06.15 |
| [AFO_Refactor #1] Critical 버그 수정 - AnimNotify 권한 누락, Aurora 오타, 팀 비교 반전 (0) | 2026.06.10 |
| [AFO_Refactor #0] 프로젝트 셋업 + 코드 분석 - 팀 프로젝트를 GAS 기반으로 리팩토링하기 (0) | 2026.06.09 |