무기 공격 시스템 통합
1. 데이터 주도 어빌리티 + 범용 AbilitySet
어빌리티가 "무기별 동작"을 들고 있으면 무기 종류만큼 클래스가 늘어남. 데이터를 자산으로 빼면 어빌리티 1개 + 무기별 DataAsset로 끝남.
❌ Before — 무기/원소마다 클래스
GA_HeavyAttack_Fire / _Water / _Wind / _Staff ... // 무기·원소 조합만큼 증식
✅ After — 단일 어빌리티가 현재 원소 variant를 데이터에서 resolve
const FWeaponHeavyAttack* V = WeaponData.AttackDefinition.LoadSynchronous()
->ResolveHeavyVariant(ResolveCurrentElementTag());
// V->AttackType 으로 실행 분기 (AoE / Projectile / Dash ...)
AbilitySet도 같은 원리. 어빌리티가 범용이면 무기마다 재그랜트할 이유가 없음.
- 범용 공격(Attack/Sprint/Jump/ParryCounter/Heavy/Burst) → 캐릭터 셋에 한 번만 grant
- 무기 고유 행동(예: 방패 Guard)만 무기 셋에 grant → 무기 교체 시 자동 해제(생명주기)
- 단, 범용 어빌리티는
CanActivateAbility에서 무기 장착·데이터 유무를 직접 게이팅해야 함(무기 없을 때 발동 방지).
2. 실행 엔진 일반화 — "값 spec + 래퍼" 패턴 (저위험 리팩토링)
이미 잘 도는 컴포넌트(버스트 실행기)를 다른 기능(강공)도 쓰게 만들 때, DataTable에 쓰이는 구조체를 수술하지 않고 일반화하는 법.
❌ 컴포넌트가 특정 행 타입(FSkillCombination*)을 직접 참조 → 그 기능 전용
✅ 실행에 필요한 필드만 담은 값 타입(spec)을 신설 → 컴포넌트는 spec만 소비
// 기존 진입점은 "래퍼"로 유지 → 기존 호출자(버스트) 무변경
void UPlayerBurstComponent::BeginBurstSkill(const FSkillCombination* Row) {
FAttackExecutionSpec Spec; // 행 → spec 으로 복사
Spec.AttackType = Row->AttackType; // ... 실행 필드만
BeginAttackExecution(Spec); // 공용 진입점에 위임
}
- 새 기능(강공)은
BeginAttackExecution(spec)를 직접 호출. - 핵심:
FSkillCombination(DataTable 행)을 안 건드림 → 자산 손상 위험 0. "완전한 공용 구조체 이동"은 검증 끝난 마지막에.
3. GAS 입력 파이프라인 — InputTag · 우선순위 · 캔슬 윈도우
디버깅하다 제일 많이 배운 곳. 같은 입력에 여러 어빌리티가 묶일 때 무엇이 발동하는가.
- 입력 → 버퍼 →
CombatInputPriority내림차순으로 발동 결정 (높은 게 선점) - 버퍼된 intent 태그 = AbilitySet에서 그 어빌리티에 박은
InputTag
🐞 이슈: 평타만 눌렀는데 SprintAttack(우선순위 10)이 나감.
원인 = AbilitySet에서 SprintAttack의 InputTag가Ability.Player.Attack으로 잘못 박힘 → 평타 입력에 같이 버퍼되어 우선순위로 선점.
정상값Ability.Player.SprintAttack로 고치니 해결. InputTag 한 칸이 발동 분기를 좌우함.
진행 중 공격을 끊는 캔슬:
- 몽타주의
AnimNotifyState_AttackCancelWindow가State.Attack.CancelOpen+AllowedCancelIntents를 등록 - 버퍼된 intent가 그 목록에 있어야 캔슬-인 허용
- 예외: Dash는
CancelAbilitiesWithTag(Ability.Type.Attack)로 윈도우와 무관하게 끊음 - → "Dash만 끊기고 강공으로는 안 끊김"은 곧 캔슬 윈도우 노티 누락 / intent 태그 불일치 신호
🐞 이슈 : "콤보가 안 끊긴다" → 실은 일반몹 HitReaction 피격으로 액션이 끊기던 것. CancelWindow는 정상이었음. 증상의 원인을 입력 체계로 단정하지 말 것.
4. UE 타입 개명 = CoreRedirect 필수
C++ enum/class 이름을 바꾸면, 그걸 참조하던 .uasset이 참조를 잃음. DefaultEngine.ini의 [CoreRedirects]로 매핑해줘야 보존됨.
[CoreRedirects]
+EnumRedirects=(OldName="/Script/Retrieve.EBurstAttackType",NewName="/Script/Retrieve.EAttackExecutionType")
+ClassRedirects=(OldName="/Script/Retrieve.AttackComboDefinition",NewName="/Script/Retrieve.WeaponAttackDefinition")
- 파일 개명은
git mv로 이력 보존 후 클래스명·#include·*.generated.h정리. - 필드(프로퍼티) 이름까지 바꾸면
+PropertyRedirects도 필요 → 안 바꿔도 되면 타입만 바꾸고 필드명은 유지(특히 DataTable 행 프로퍼티).
5. DataTable에 쓰이는 USTRUCT 필드 삭제 위험
FTableRowBase 구조체의 UPROPERTY를 지우면 그 DataTable 자산이 안 열릴 수 있음(과거 사고 이력). 빌드는 컴파일만 보장하지 DT 로딩은 검증 안 됨.
✅ 안전 절차
- 데이터를 새 위치(자산)로 먼저 마이그레이션 (원본 지우기 전에)
- 그 필드를 읽는 코드부터 제거/재배선
- 그 다음 필드 삭제 → 빌드 → 에디터에서 DT 직접 열어 로드 확인 ("dropped property" 경고는 정상)
- 안 열리면 즉시 revert
6. Element.None — 센티넬 vs 게임플레이 값 구분
"원소는 항상 불/물/바람이니 None을 다 지우자"가 위험했던 이유: Element.None이 두 역할을 겸하고 있었음.
| 역할 | 예 | 삭제 가능? |
|---|---|---|
| 게임플레이 값 (버스트 None 행) | FindBurstForElement(.., None) 폴백 |
✅ 제거(다른 원소로 이전) |
| "원소 없음" 센티넬 | 플레이어 현재원소 기본값, 게이지 빈 슬롯, 데미지태그 게이팅(!= None), UI 회색 |
❌ 유지(기본값 무너짐) |
→ 한 태그가 기본값/빈 상태 표현으로 널리 쓰이면 그 부분은 못 지움. 사용처 전수 grep 후 역할 분리할 것.
+ 전투 효과는 "데미지 spec의 asset 태그"에서 파생
가드브레이크·원소 게이지 충전·히트 반응이 어디서 나오나 추적해보니, 전부 데미지 GE spec에 실린 태그를 CombatAttributeSet(방어 처리 + BroadcastHitEvent)가 읽어 분기·이벤트 발사하는 구조였음.
- → 새 공격 경로(공용 실행기)도 적중 시 같은 태그(원소/Attack.Type/GuardBreak/Hit 이벤트)를 spec에 주입해주면 기존 파이프라인이 그대로 작동.
- 태그를 안 실으면 데미지는 들어가도 가드브레이크/충전이 누락됨.
비교 정리
| 증상 | 진짜 원인 | 교훈 |
|---|---|---|
| 평타에 SprintAttack 발동 | AbilitySet InputTag 오설정 | 같은 입력 묶임 + 우선순위 선점 |
| 콤보가 안 끊김 | 실은 HitReaction 피격 | 증상→원인 단정 금지 |
| 자산이 클래스 참조 잃음 | 개명 시 redirect 누락 | Class/Enum/PropertyRedirects |
| DT 안 열림(과거) | 행 USTRUCT 필드 삭제 | 마이그레이션 먼저 + DT 오픈 검증 |
| None 지우니 기본값 깨짐 | 센티넬 겸용 | 역할 분리 후 삭제 |
핵심 요약
- 데이터 주도화 = 어빌리티는 범용 1개, 무기별 차이는 DataAsset이 담당. 범용 어빌리티는 캐릭터 셋에 한 번 grant + CanActivate 게이팅.
- 저위험 일반화는 "값 spec + 기존 진입점 래퍼"로. DataTable 구조체는 최대한 안 건드림.
- GAS 입력은 InputTag·우선순위·캔슬윈도우(intent 태그 매칭)가 발동을 좌우. 오발동·안끊김은 대개 이 셋 중 하나.
- UE 타입 개명엔 CoreRedirect, DataTable 행 필드 삭제는 마이그레이션 먼저 + DT 오픈 검증.
- 한 태그가 "없음 센티넬"로 쓰이면 함부로 못 지움.