내일배움캠프_Unreal/TIL

내일배움캠프_Unreal/TIL - Day 143 (260625)

하임 2026. 6. 25. 23:39

무기 공격 데이터 통합


1. 문제: 불필요하게 많은 클래스

강공격이 원소/무기별로 별도 클래스로 쪼개져 있었음. 동작은 거의 같은데 파라미터(폭발 반경, 데미지 배율, 런치 속도)만 다른데도 클래스가 계속 늘어남.

GA_HeavyAttack_Base      ← 공통 로직
 ├─ GA_HeavyAttack_Fire   ← 광역 폭발  (반경만 다름)
 ├─ GA_HeavyAttack_Water  ← 광역 + 빙결
 └─ GA_HeavyAttack_Wind   ← 자기 런치
GA_StaffHeavyAttack      ← 스태프 전용 투사체 (별도 계층)

문제는 "데이터 차이"를 "코드 분기"로 표현하고 있었다는 것. 새 원소가 생기면 클래스를 또 만들어야 하고, 공격 파라미터 한 줄 바꾸려고 C++ 빌드를 돌려야 함.


2. 해결: 데이터를 로직에서 분리

핵심 원칙

"무엇으로 때리는가(데이터)"와 "어떻게 실행하는가(로직)"를 분리한다.

  • 데이터 → UWeaponAttackDefinition (DataAsset). 무기별로 1개 에셋.
  • 로직 → UGA_HeavyAttack (GameplayAbility). 전 무기·전 원소 공용 1개.

❌ Before — Row에 인라인으로 흩어진 공격 데이터

// FRetrieveWeaponDataRow 안에 공격 필드가 뒤섞여 있고,
// 강공 파라미터는 아예 GA 클래스의 UPROPERTY로 하드코딩됨
struct FRetrieveWeaponDataRow {
    FWeaponStaffAttack StaffAttack;       // 스태프만 읽음
    FAttackData        SprintAttack;      // 죽은 필드(대체됨)
    FAttackData        JumpAttack;         // 죽은 필드
    UDataTable*        WeaponAttackTable;  // C++ 소비처 없음
    TSoftObjectPtr<UAnimMontage> ParrySuccessMontage;
    // ...
};

✅ After — 공격 데이터 전용 DataAsset으로 일원화

// UWeaponAttackDefinition — 무기 1개당 1 에셋, 원소별 variant를 배열로 보유
UCLASS(BlueprintType)
class RETRIEVE_API UWeaponAttackDefinition : public UDataAsset
{
    // 강공격: 원소별 variant 배열 + 태그로 resolve
    UPROPERTY(EditAnywhere, Category = "HeavyAttack")
    TArray<FWeaponHeavyAttack> HeavyVariants;

    const FWeaponHeavyAttack* ResolveHeavyVariant(const FGameplayTag& ElementTag) const;
    // Combo / Sprint / Bash / Jump / ParryCounter / ParrySuccess 도 동일 패턴
};

어빌리티는 "현재 원소 태그"로 데이터에서 variant만 뽑아와 실행함.

// UGA_HeavyAttack — 5개 클래스를 대체한 단일 어빌리티
CachedVariant = *Def->ResolveHeavyVariant(CachedElementTag); // 데이터에서 분기
switch (CachedVariant.AttackType) {
    case EAttackExecutionType::Projectile: RunProjectilePath(); break; // 원거리
    default:                               RunExecutorPath();   break; // 근접(공용 실행기)
}

3. 분기를 코드가 아니라 enum + EditCondition으로

AttackType 하나로 동작 종류를 나누고, 에디터에서는 EditCondition으로 해당 타입에 필요한 필드만 노출 → 데이터 저작자가 헷갈리지 않음.

enum class EAttackExecutionType : uint8 {
    Cleave        UMETA(DisplayName = "단일 강타"),
    WorldActor    UMETA(DisplayName = "월드 액터 (소환물)"),
    Projectile    UMETA(DisplayName = "투사체"),
    Dash          UMETA(DisplayName = "돌진"),
    AreaOfEffect  UMETA(DisplayName = "주변 AoE")
};

// 투사체 필드는 Projectile 타입일 때만 디테일 패널에 표시됨
UPROPERTY(EditAnywhere, Category = "Heavy|Attack|Projectile",
          meta = (EditCondition = "AttackType == EAttackExecutionType::Projectile"))
TSubclassOf<AStaffProjectile> ProjectileClass;

4. 기존 버스트 엔진을 "공용 실행기"로 일반화

강공 실행 로직을 새로 짜지 않고, 이미 검증된 버스트 스킬 실행 엔진을 재사용함. 단, 이름이 버스트 전용이라 중립화가 필요했음.

항목 Before After
enum 이름 EBurstAttackType EAttackExecutionType
의미 버스트 전용 공격 실행 타입 (공용)
실행 진입점 BeginBurstSkill BeginAttackExecution (+ 기존은 래퍼 유지)

리팩토링 원칙: 기존 호출부를 깨지 않도록 래퍼를 남겨 점진 전환(저위험). 완전 통합은 마지막 단계로 미룸.


5. ⭐ CoreRedirects — 이름 바꿔도 .uasset 안 깨뜨리기

UE에서 C++ 클래스/enum/프로퍼티 이름을 바꾸면 이를 참조하던 바이너리 에셋(.uasset)이 참조를 잃고 깨짐. Config/DefaultEngine.ini[CoreRedirects]에 옛 이름 → 새 이름 매핑을 적어두면 로드 시 자동 치환됨.

[CoreRedirects]
; 클래스 개명
+ClassRedirects=(OldName="/Script/Retrieve.AttackComboDefinition",NewName="/Script/Retrieve.WeaponAttackDefinition")
; enum 개명
+EnumRedirects=(OldName="/Script/Retrieve.EBurstAttackType",NewName="/Script/Retrieve.EAttackExecutionType")
; 프로퍼티 / 함수 개명도 동일
+PropertyRedirects=(OldName="...DialogueRow.kind",NewName="...DialogueRow.Kind")
+FunctionRedirects=(OldName="...RetrieveEnemyCharacter.Activate",NewName="...ActivateEnemy")

주의: USTRUCT의 필드 삭제는 redirect로 못 막음. 에셋을 열어 마이그레이션 → 재저장 → 커밋하는 별도 단계가 필요(미검증 시 에셋 손상 위험).


6. AbilitySet 재편 — 공용 행동은 베이스로

원소/무기 무관 공통 공격(Attack·Sprint·Jump·Heavy·Burst 등)은 캐릭터 베이스(Sovereign) AbilitySet으로 올리고, 무기 세트엔 고유 행동만(예: SwordShield의 Guard) 남김. 공용 GA들은 CanActivateAbility에서 무기 장착 여부를 게이팅 → 무기 없으면 발동 안 함.


7. 디버깅 노트🐛

(1) 평타 누르면 대시 공격이 나감

  • 원인: Sovereign에서 GA_SprintAttack의 InputTag가 Ability.Player.Attack으로 잘못 박힘(정상은 Ability.Player.SprintAttack).
  • CombatInputPriority 10 > 0이라 같은 입력에서 대시가 평타를 선점해버림.
  • 교훈: 같은 입력 태그를 두 어빌리티가 공유하면 우선순위로 묶임. InputTag 배선부터 확인.

(2) "콤보 캔슬이 안 된다"의 진실

  • 캔슬 윈도우는 정상이었음. 실제론 일반 몹 HitReaction에 피격당해 몽타주가 끊긴 것.
  • 캔슬 원리 정리:
    • 콤보 몽타주의 AnimNotifyState_AttackCancelWindowState.Attack.CancelOpen + 허용 intent 목록을 등록.
    • 버퍼된 intent(= InputTag)가 그 목록에 있어야 캔슬 성립.
    • Dash는 CancelAbilitiesWithTag로 윈도우와 무관하게 끊음.
  • 교훈: "안 되는 것처럼 보이는 버그"는 진짜 원인이 다른 시스템(피격 반응)에 있을 수 있음.

+ 스태미너 HUD 추가

  • WBP_HUD에 스태미너 게이지 위젯 추가, 공격 GA들에 남아 있던 디버그 라인 제거.
  • 데이터/로직 정리(#175) 직후라 비교적 가벼운 폴리싱 커밋.

비교 정리

구분 ❌ Before ✅ After
강공 클래스 수 5개 (Base + Fire/Water/Wind + Staff) 1개 (UGA_HeavyAttack)
원소별 파라미터 GA 클래스에 하드코딩 DataAsset의 variant 배열
공격 데이터 위치 Weapon Row에 인라인 산재 UWeaponAttackDefinition 일원화
동작 분기 클래스 상속 EAttackExecutionType + EditCondition
실행 엔진 버스트 전용 공용 실행기로 일반화·재사용
새 원소 추가 클래스 신설 + 빌드 에디터에서 variant 한 줄 추가

핵심 요약

  • 데이터 차이를 코드 분기로 표현하면 클래스가 폭발한다. 차이가 파라미터뿐이면 DataAsset으로 빼라.
  • 동작 종류는 enum + EditCondition으로 나누면 코드 1개로 다 처리 + 에디터 UX까지 깔끔해짐.
  • 이미 검증된 시스템(버스트 엔진)은 이름만 중립화해 재사용. 호출부는 래퍼로 점진 전환(저위험).
  • C++ 이름 변경 시 [CoreRedirects]로 .uasset 참조를 살린다. 단, 필드 삭제는 redirect 불가 → 에셋 마이그레이션 별도 단계.
  • "안 되는 것처럼 보이는 버그"는 진짜 원인이 다른 시스템(InputTag 우선순위, HitReaction)에 있을 수 있다.