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멤버로 반경을 되돌려 쓰고, 스윕은 산출 반경 우선·행 반경 폴백.