내일배움캠프_Unreal/TIL

내일배움캠프_Unreal/TIL - Day 128 (260604)

하임 2026. 6. 4. 21:15

전투 GA 우선순위 정리 & 데이터 드리븐 전환

 

1. 차단 규칙 일원화 — ActivationBlockedTags vs BlockAbilitiesWithTag
문제 상황: 같은 규칙("버스트 중 가드 불가")이 Guard.ActivationBlockedTags += Bursting(상태 기반)과 Burst.BlockAbilitiesWithTag += Guard(어빌리티 기반) 양쪽에 중복돼 있어, 어디를 봐야 할지 추론이 어려웠다.
해결책 (역할 분리):
ActivationBlockedTags = 순수 상태 게이트만 (Dead/Staggered/Knockdown/Dodging, + 자기 재발동 차단).
BlockAbilitiesWithTag = 어빌리티 상호배제 — "내가 도는 동안 막을 것들"을 소유 어빌리티 한 곳에서 선언.
배운 점: "A 중 B 불가"는 B를 막는 주체(A) 한 곳에 모아야 한 눈에 읽힌다. 강공격·버스트는 다른 액션을 전부 Block하는 확정 발동, 대시는 Attack/Guard만 Cancel하고 강공격·버스트는 취소 못 하는 식으로 우선순위가 명확해졌다.


2. 공중 행동의 양방향 가드
문제 상황: 공중 발동만 막았더니(bBlockActivationWhileAirborne), 지상에서 공격을 누른 채 점프하면 결국 공중 공격이 되어 의도가 깨졌다.
해결책: 반대 방향도 막는다. 행동(공격/가드/대시/강공격/버스트) 중에는 점프 자체를 금지 — RetrieveCharacterMovementComponent::CanAttemptJump()라는 단일 chokepoint에 상태 태그 게이트를 넣어, 점프가 GA든 직접 호출이든 전부 커버.
배운 점: 상태 전이 규칙은 "들어가는 것"과 "나가는 것"을 쌍으로 점검해야 빈틈이 없다. 그리고 같은 규칙은 가능한 한 하나의 관문에서 처리.


3. GAS 어트리뷰트는 "소비자"가 있어야 의미가 있다 (Cold 이속 디버깅)
문제 상황: Water 강공격의 Cold 효과로 적 이속을 줄이려고 MoveSpeed 모디파이어를 0.1·10.0으로 바꿔봐도 전혀 변화가 없었다.
원인: 적은 stock UCharacterMovementComponent를 쓰는데, MoveSpeed 어트리뷰트를 읽는 코드는 플레이어 전용 ALS 기반 RetrieveCharacterMovementComponent::GetMaxSpeed() 뿐이었다. → 적의 실제 속도(MaxWalkSpeed)는 어트리뷰트와 연결조차 안 돼 있었음.
해결책: 적이 MoveSpeed 어트리뷰트 변화를 구독해 MaxWalkSpeed = BaseMaxWalkSpeed × (MoveSpeed / Reference) 로 갱신하도록 배선(플레이어와 동일한 비율 방식). Cold GE는 Duration + MoveSpeed Multiply(0.3) 로 설정.
배운 점: 어트리뷰트는 데이터 보관소일 뿐, 누군가 GetXXX()로 읽어 실제 시스템(이동/데미지/UI)에 반영해야 비로소 효과가 난다. "모디파이어를 바꿔도 무반응"이면 소비 지점이 있는지부터 의심하자.


4. 강약 전달 경로 — 이벤트 이름표 vs 페이로드(TargetTags)
문제 상황: DT_WeaponData 3타를 Stagger로 바꿔도 적은 계속 Flinch만 했다.
분석: 데미지 후 BroadcastHitEvent는 피격자에게 항상 GameplayEvent.Hit.Normal(이름표)을 보내고, 강약(HitReact.Type.Stagger)은 페이로드의 TargetTags에만 담는다.
플레이어 UHitReactionComponent::ResolveReactType → TargetTags를 읽어 분기 ✅
적 반응 GA(블루프린트) → TargetTags를 안 읽고 이름표/다른 값으로 분기 → 콤보는 항상 Hit.Normal이라 늘 Flinch ❌
배운 점: "어디에 정보를 싣느냐(이름표 vs 페이로드)"와 "양쪽이 같은 규약으로 읽느냐"가 핵심. 플레이어/적이 다른 방식으로 강약을 판정하던 비대칭이 곧 버그였다. (→ 적 피격 반응도 플레이어처럼 C++로 통일하는 게 근본 해결.)


5. 하드코딩 → 데이터 드리븐 (3가지 형태)
느슨한 필드 → DataAsset 프로파일: 흩어진 5개 피격 몽타주/GE 필드를 URetrieveHitReactionProfile(TMap<ERetrieveHitReactType, 엔트리>) 하나로 통합. 기존 ULockOnConfig 패턴과 일관. 새 반응 타입은 코드 없이 행 추가.
매직넘버 → 어트리뷰트: 실드 경감 RawDamage * 0.5f → ShieldDamageReduction 어트리뷰트(GuardDamageReduction과 동일 패턴). GE로 런타임 조정 가능.
GA 상수 → 무기 데이터: 공격 TraceRadius(GA 고정 60) → FRetrieveWeaponDataRow로 이동. 무기별 히트 두께 조절.
배운 점: 데이터화의 목표는 "값 빼기"가 아니라 확장 시 코드를 안 건드리게 만드는 것. 기존에 이미 있는 패턴(LockOnConfig, GuardDamageReduction)에 맞춰 통일하면 팀이 읽기 쉽다.


6. 데이터로 "분기"를 빼기
콤보 피니셔 충전 보너스: FWeaponComboStep.ChargeBonusEventTag + 네이티브 태그 GameplayEvent.Combo.Finisher. 3타 적중 시 스텝당 1회(가드 플래그) 자신에게 이벤트 발행 → ElementGaugeComponent가 Instigator==Owner + ChargeRuleTable 매핑으로 충전. 코드와 충전량이 디커플링.
버스트 락 타입별: FSkillCombination.bLockMovement/RotationDuringCast 플래그로 시전 중 잠금을 데이터에서 결정(Dash형은 이동 허용). loose 태그를 활성/해제하는 방식.
배운 점: if 분기를 코드에 박지 말고 데이터 한 칸으로 빼면, 디자이너가 직접 튜닝하고 코드는 "규칙 실행기"로 단순해진다.


7. C++ / GAS 함정 메모
연산자 우선순위 + 널 역참조: if (MoveComp && MoveComp->IsFalling() || Character->bPressedJump) 는 &&가 ||보다 우선이라 의도와 다르게 묶이고, Character가 null이면 크래시. → 괄호 명시 + 선행 널체크. (겸사겸사 중복 오버라이드를 베이스 공용 플래그로 통일)
UHT는 TObjectPtr<const T> 미지원: UPROPERTY 포인터는 const 한정자를 못 받는다. → 비const로 두고 사용처에서만 읽기.

 

회고 / 다음에 적용할 점
상태 전이 규칙은 진입/이탈을 쌍으로, 같은 규칙은 한 관문에서.
"값을 바꿔도 무반응"이면 어트리뷰트/데이터의 소비 지점이 실제로 연결돼 있는지 먼저 본다.
정보 전달은 이름표 vs 페이로드 어느 쪽인지, 그리고 읽는 쪽이 모두 같은 규약을 따르는지 확인.
데이터화는 "확장 시 코드 무수정"을 기준으로, 기존 패턴에 맞춰 통일.
(다음 작업) 적 피격 반응 C++ 통일, AttackComboDefinition(원소별 콤보 variant) 전환.