무기 공격 데이터 통합
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). CombatInputPriority10 > 0이라 같은 입력에서 대시가 평타를 선점해버림.- 교훈: 같은 입력 태그를 두 어빌리티가 공유하면 우선순위로 묶임. InputTag 배선부터 확인.
(2) "콤보 캔슬이 안 된다"의 진실
- 캔슬 윈도우는 정상이었음. 실제론 일반 몹 HitReaction에 피격당해 몽타주가 끊긴 것.
- 캔슬 원리 정리:
- 콤보 몽타주의
AnimNotifyState_AttackCancelWindow가State.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)에 있을 수 있다.
'내일배움캠프_Unreal > TIL' 카테고리의 다른 글
| 내일배움캠프_Unreal/TIL - Day 147 (260701) (0) | 2026.07.01 |
|---|---|
| 내일배움캠프_Unreal/TIL - Day 145 (260629) (0) | 2026.06.29 |
| 내일배움캠프_Unreal/TIL - Day 141 (260623) (0) | 2026.06.23 |
| 내일배움캠프_Unreal/TIL - Day 140 (260622) (0) | 2026.06.22 |
| 내일배움캠프_Unreal/TIL - Day 137 (260617) (0) | 2026.06.17 |