카테고리 없음

내일배움캠프_Unreal/TIL - Day 142 (260624)

하임 2026. 6. 24. 23:11

무기 공격 시스템 통합


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_AttackCancelWindowState.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 로딩은 검증 안 됨.

✅ 안전 절차

  1. 데이터를 새 위치(자산)로 먼저 마이그레이션 (원본 지우기 전에)
  2. 그 필드를 읽는 코드부터 제거/재배선
  3. 그 다음 필드 삭제 → 빌드 → 에디터에서 DT 직접 열어 로드 확인 ("dropped property" 경고는 정상)
  4. 안 열리면 즉시 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 오픈 검증.
  • 한 태그가 "없음 센티넬"로 쓰이면 함부로 못 지움.