몬스터 시스템을 테스트하는 과정에서 여러 문제가 동시에 발견되었다.
각 문제를 디버깅하면서 데이터 설정, 충돌 구조, 데미지 처리 방식, 코드 구조 전반을 개선하게 되었다.
드래곤 몬스터의 FirePillar 스킬이 정상적으로 발동되고 VFX도 출력되지만 실제로 구조물에 데미지가 적용되지 않는 현상이 발생했다.
로그에서는 다음 흐름이 확인되었다.
[SkillPresent] VFX SpawnAtLocation Cascade OK
즉
까지는 정상적으로 동작하고 있었다.
하지만 실제 게임에서는 불기둥이 생성되어도 피해가 발생하지 않았다.
FirePillar 스킬 프리셋(Row)을 확인해보니 다음 값이 설정되어 있었다.
Radius = 0
FirePillarActor 내부 로직은 다음과 같다.
DamageSphere->SetSphereRadius(Radius)
→ OverlapActors 탐색
→ Victims 배열 생성
→ ApplyDamage
하지만 Radius가 0이면
Overlap 범위 = 0
→ Victims 배열 = empty
→ 데미지 로직 실행되지 않음
즉
VFX는 보이지만 실제 피해 판정 영역이 존재하지 않는 상태였다.
FirePillarActor는 피해 대상 필터링을 다음 조건으로 수행하고 있었다.
Pawn
또는
APotatoPlaceableStructure 기반 구조물
코드
if (APotatoPlaceableStructure* S = Cast<APotatoPlaceableStructure>(Other))
{
return (S->StructureData && S->StructureData->bIsDestructible && S->CurrentHealth > 0.f);
}
하지만 실제 타겟은
BP_WareHouse_C_1
이 액터가
피해 대상에서 제외된다.
기존 FirePillarActor 구조는 다음과 같았다.
TickDamage()
└ DotComponent.ApplyDot()
이 구조에서는
이 서로 다른 타이머로 동작한다.
예시
불기둥 LifeTime = 5s
DOT Duration = 1.2s
DOT TickInterval = 0.4s
이 경우
즉
영역 스킬과 DOT 시스템이 구조적으로 분리된 상태였다.
스킬 데이터 수정
Radius = 250 ~ 400
이제
SphereOverlap
→ Victims 배열 생성
→ ApplyDamage 실행
정상 동작한다.
FirePillar 피해 대상 구조물을 다음 기반 클래스로 통일하였다.
APotatoPlaceableStructure
이를 통해
모든 시스템을 공통 구조로 사용할 수 있게 했다.
DOT 컴포넌트를 제거하고 틱 데미지 방식으로 변경했다.
기존
TickDamage
└ DotComponent.ApplyDot()
변경
TickDamage
└ ApplyDamage(DPS * TickInterval)
코드
const float DamagePerTick = DotDps * TickInterval;
UGameplayStatics::ApplyDamage(
Victim,
DamagePerTick,
InstigatorController,
DamageCauser,
UDamageType::StaticClass()
);
장점
기존
SetTimer(TickInterval)
수정
SetTimer(TickInterval, InitialDelay = 0)
즉
Spawn → 즉시 첫 데미지
Skill Trigger
↓
FirePillarActor Spawn
↓
SphereOverlap Victim 탐색
↓
DamagePerTick 계산
↓
ApplyDamage
↓
LifeTime 종료
↓
Destroy
몬스터 AI와 스킬 시스템의 여러 cpp 파일 내부에
static helper functions
형태의 CPP-local helper 코드가 반복적으로 존재하고 있었다.
대표 함수
FindFirstCollisionPrimitive
GetClosestPoint2DOnTarget
ComputeApproachPoint2D
DistancePointToSegment2D
GetMonsterCapsuleRadiusSafe
GetPlayerPawnSafe
ScheduleTimerSafe
문제점
특히
BTService_UpdateAttackTarget.cpp
에서는 AI 로직보다 helper 코드가 더 많은 상태였다.
CPP-local helper들을 도메인별 Utils 모듈로 분리했다.
리팩터링 전
Monster
├ AI
│ └ BTService_UpdateAttackTarget.cpp
│ └ static helpers
리팩터링 후
Monster
├ Utils
│ ├ PotatoTargetGeometry
│ ├ PotatoMonsterRuntimeUtils
│ ├ PotatoPlayerQueryUtils
│ ├ PotatoAnimUtils
│ └ PotatoMath2D
역할
주요 함수
FindFirstCollisionPrimitive
GetClosestPoint2DOnTarget
ComputeApproachPoint2D
역할
주요 함수
GetMonsterCapsuleRadiusSafe
GetAnimInstanceSafe
DisableMovementSafe
ScheduleTimerSafe
역할
주요 함수
GetPlayerPawnSafe
IsValidAttackablePlayer
파일 단위 Helper
→
시스템 단위 Utility
구조로 개선되었다.
효과
몬스터가 NavMesh + MoveTo 기반으로 이동할 때 특정 구조물을 넘어가지 못하고 멈추는 문제가 발생했다.
NavMesh 상에서는 연결되어 있었지만 실제 이동은 실패했다.
몬스터는 Character 기반이므로 실제 충돌은 CapsuleComponent가 담당한다.
이때
NavMesh 경로 존재
하지만 Capsule이 지형에 걸림
상황이 발생했다.
Capsule을 줄이면 이동은 가능했지만
피격 판정이 작아지는 문제
가 발생했다.
이동 충돌과 피격 충돌을 분리하였다.
구조
| 역할 | 컴포넌트 |
|---|---|
| 이동 | CapsuleComponent |
| 피격 | Mesh / HitCapsule |
CapsuleComp->SetCollisionResponseToChannel(ProjectileChannel, ECR_Ignore);
MeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
MeshComp->SetCollisionResponseToChannel(ProjectileChannel, ECR_Block);
플레이어 히트스캔 공격 시 게임이 멈추는 현상이 발생했다.
콜스택
FireHitscan
→ ApplyDamage
→ TakeDamage
→ TryPlayHitReact
→ ScheduleTimerSafe
히트스캔에서
LineTraceMulti
를 사용하고 있었기 때문에
가 동시에 히트되면서
같은 몬스터에게 여러 번 ApplyDamage가 호출되고 있었다.
결과
한 발에 수십 번 데미지
→ HitReact 타이머 폭증
→ 프레임 급락
한 발당 Actor당 1회만 데미지 적용하도록 필터링했다.
TSet<TWeakObjectPtr<AActor>> DamagedThisShot;
for (const FHitResult& Hit : Hits)
{
AActor* HitActor = Hit.GetActor();
if (!IsValid(HitActor)) continue;
if (DamagedThisShot.Contains(HitActor)) continue;
DamagedThisShot.Add(HitActor);
UGameplayStatics::ApplyDamage(
HitActor,
Damage,
GetInstigatorController(),
this,
UDamageType::StaticClass()
);
}
특히 스킬 시스템 디버깅에서는 다음 흐름을 단계적으로 확인하는 것이 중요하다.
Trigger
→ Cooldown
→ Spawn
→ Overlap
→ Target Filter
→ Damage Apply
이 흐름을 기준으로 확인하면 대부분의 스킬 버그를 빠르게 추적할 수 있다.