카테고리 없음

내일배움캠프_Unreal/TIL - Day 146 (260630)

하임 2026. 6. 30. 21:03

GAS 이벤트 기반 패리 카운터 & 가드공격 Bounds화


1. 패리 카운터 - 입력 게이팅에서 이벤트 트리거로

문제: 카운터가 "상태 태그 + 재입력"에 묶여 있었음

패리 성공 후 카운터를, State.Player.CanCounter 상태를 켜두고 플레이어가 Attack을 다시 눌러야 나가는 방식이었음.

  • 성공했는데 한 번 더 입력해야 하고, 그 사이 윈도우를 놓치면 불발.
  • CanCounter 상태 부여/해제와 카운터 윈도우 GE(CounterWindowEffect)까지 관리해야 함 → 상태 머신이 무거움.

해결: 패리 성공 순간 게임플레이 이벤트로 카운터 GA를 자동 발동

카운터 GA가 특정 이벤트를 수신하면 스스로 발동하도록 트리거를 등록. 입력·상태가 아니라 이벤트가 발동 조건.

❌ Before — 상태 태그로 게이팅 (외부 입력이 실제 트리거)

// GA_ParryCounter 생성자
ActivationRequiredTags.AddTag(RetrieveGameplayTags::State_Player_CanCounter);

✅ After — 이벤트 자체가 발동 트리거

// GA_ParryCounter 생성자
FAbilityTriggerData CounterTrigger;
CounterTrigger.TriggerTag = RetrieveGameplayTags::GameplayEvent_Parry_Counter;
CounterTrigger.TriggerSource = EGameplayAbilityTriggerSource::GameplayEvent;
AbilityTriggers.Add(CounterTrigger);

개념 구분

  • ActivationRequiredTags = "이 태그가 있어야 발동 가능" — 발동 트리거는 여전히 외부(입력).
  • AbilityTriggers(GameplayEvent) = 이벤트 수신 = 발동. 별도 입력/상태 불필요.

2. 이벤트 송신 - SendGameplayEventToActor + 페이로드로 대상 전달

패리 성공 처리부(GA_ParryBase::HandleParrySuccess)에서 이벤트를 쏨. 페이로드에 맞은 대상을 실어 보내 카운터가 누굴 칠지까지 함께 넘김.

FGameplayEventData CounterEvent;
CounterEvent.EventTag       = RetrieveGameplayTags::GameplayEvent_Parry_Counter;
CounterEvent.Instigator     = CounterAvatar;
CounterEvent.Target         = LastParriedAttacker.Get();
CounterEvent.OptionalObject = LastParriedAttacker.Get();
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(
    CounterAvatar, RetrieveGameplayTags::GameplayEvent_Parry_Counter, CounterEvent);
  • 부수 효과: 이벤트 방식으로 가니 CounterWindowEffect(카운터 윈도우 GE) 적용 코드가 통째로 불필요 → 상태 GE 하나 제거.
  • 주의 🐛: 신규 태그 GameplayEvent.Parry.Counter.cpp(UE_DEFINE_...)와 .h(UE_DECLARE_...) 양쪽 선언해야 링크됨. 한쪽만 하면 링크 에러.

3. 가드공격 Bounds화 - 검과 방패의 다른 형상

평타·점프에서 만든 URetrieveWeaponTraceLibrary가드공격도 재사용. 공격 소스에 따라 다른 형상 API 호출.

트레이스 소스 API 형상
무기(검) BuildBoundsTrace 최장축 구체 스윕
방패 BuildBoundsSphere 중심 구체 1개
// 검 — 무기 메시 바운드 스윕. 성공하면 여기서 return, 실패 시 아래 소켓 폴백
FWeaponTraceSegment WeaponSeg;
if (URetrieveWeaponTraceLibrary::BuildBoundsTrace(TraceMesh, 1.0f, 0.f, SegCount, WeaponSeg)
    && WeaponSeg.IsValidTrace())
{
    OutPoints = WeaponSeg.Points;
    ResolvedTraceRadius = WeaponSeg.Radius;
    return;
}
// ↓ (Bounds 실패 시) 기존 소켓 경로 폴백 유지
  • 평타는 레거시 소켓 경로를 삭제했지만, 가드공격은 Bounds 우선 + 소켓 폴백을 남겨 안전하게 전환.

4. 방패 메시를 소켓으로 조회 - GetEquippedMeshBySocket

방패는 검과 별도 메시 컴포넌트라, 여러 파트 중 "어느 게 방패냐"를 알아야 트레이스가 됨.
→ 부착 소켓 이름("Shield")으로 파트를 찾는 getter를 새로 추가.

UMeshComponent* UWeaponComponent::GetEquippedMeshBySocket(FName AttachSocketName) const
{
    for (const FRetrieveEquippedWeaponPart& Part : WeaponAttachParts)
        if (Part.DrawnSocket == AttachSocketName && IsValid(Part.Mesh))
            return Part.Mesh;
    return nullptr;
}

// 호출부 (BuildShieldTracePoints)
UMeshComponent* ShieldMesh = CachedWeaponComponent->GetEquippedMeshBySocket(RetrieveWeaponSockets::Shield);
  • 기존 GetWeaponMeshForTrace(두 소켓을 다 가진 메시 1개 반환)로는 방패를 특정할 수 없어, 부착 위치 기준 조회가 별도로 필요했음.

5. mutable ResolvedTraceRadius - const 산출 메서드가 반경을 되돌려 씀

Bounds 트레이스는 점뿐 아니라 반경도 메시 크기에서 산출됨. 그래서 스윕 반경을 데이터테이블 행의 고정 TraceRadius가 아니라 산출된 반경으로 써야 함.

  • 문제: 트레이스 점을 만드는 Build...TracePoints()들이 전부 const 메서드인데, 산출 반경을 멤버에 저장해 스윕 단계로 넘겨야 함.
  • 해결: mutable로 선언해 const 메서드에서 대입 허용. 산출 성공이면 그 값, 실패(폴백)면 행 반경으로.
mutable float ResolvedTraceRadius = 0.f;   // 헤더

// SweepAndApplyDamage — 산출값 우선, 없으면 행 반경 폴백
const float TraceRadius = ResolvedTraceRadius > 0.f
    ? ResolvedTraceRadius
    : CachedWeaponData.TraceRadius;

교훈: 계산 결과가 "점"과 "반경"처럼 여러 개면, const 산출 메서드에서 out 파라미터 하나로는 부족 → mutable 멤버로 부수 산출값을 실어 나르는 패턴이 유용(단, 상태 결합이 늘어나니 남용 주의).


비교 정리

구분 ❌ Before ✅ After
카운터 발동 State.Player.CanCounter + Attack 재입력(수동) AbilityTriggers(GameplayEvent) 자동
발동 트리거 주체 외부 입력 이벤트 수신 자체
카운터 윈도우 GE CounterWindowEffect 적용 불필요 → 제거
대상 전달 상태에서 별도 조회 이벤트 페이로드(Target)로 동봉
가드공격 트레이스 소켓 기반 Bounds 우선 + 소켓 폴백
방패 메시 특정 불가(소켓 2개 메시만) GetEquippedMeshBySocket("Shield")
스윕 반경 행 고정 TraceRadius 산출 반경(mutable ResolvedTraceRadius)

핵심 요약

  • GA 자동 발동은 ActivationRequiredTags(상태 게이팅)보다 AbilityTriggers + SendGameplayEventToActor(이벤트 트리거)가 결정적 — 입력·상태 관리가 통째로 사라진다.
  • 이벤트 페이로드(Instigator/Target)에 대상까지 실어 보내면 수신 측이 상태를 되조회할 필요가 없다.
  • 신규 게임플레이 태그는 .h/.cpp 양쪽 선언 필수(안 하면 링크 에러).
  • 트레이스 라이브러리는 형상 API(검=스윕 / 방패=구체)로 나눠 재사용하고, 방패처럼 별도 메시는 부착 소켓으로 조회한다.
  • Bounds는 반경도 산출값 → const 산출 메서드가 mutable 멤버로 반경을 되돌려 쓰고, 스윕은 산출 반경 우선·행 반경 폴백.