Unreal 개발 본 캠프 62일차

HappyCircle·2026년 3월 3일

Unreal 개발

목록 보기
79/163

📘TIL- 몬스터 시스템 디버깅 및 구조 개선 TIL


개요

몬스터 시스템을 테스트하는 과정에서 여러 문제가 동시에 발견되었다.

  • FirePillar 스킬 데미지 미적용
  • CPP-local helper 코드 난립
  • NavMesh 이동 중 장애물 통과 실패
  • 히트스캔 공격 시 데미지 과다 적용

각 문제를 디버깅하면서 데이터 설정, 충돌 구조, 데미지 처리 방식, 코드 구조 전반을 개선하게 되었다.


FirePillar 스킬 데미지 미적용 문제

문제

드래곤 몬스터의 FirePillar 스킬이 정상적으로 발동되고 VFX도 출력되지만 실제로 구조물에 데미지가 적용되지 않는 현상이 발생했다.

로그에서는 다음 흐름이 확인되었다.

[SkillPresent] VFX SpawnAtLocation Cascade OK

  • 스킬 실행
  • VFX 스폰

까지는 정상적으로 동작하고 있었다.

하지만 실제 게임에서는 불기둥이 생성되어도 피해가 발생하지 않았다.


분석

1. Radius 값이 0

FirePillar 스킬 프리셋(Row)을 확인해보니 다음 값이 설정되어 있었다.

Radius = 0

FirePillarActor 내부 로직은 다음과 같다.

DamageSphere->SetSphereRadius(Radius)
→ OverlapActors 탐색
→ Victims 배열 생성
→ ApplyDamage

하지만 Radius가 0이면

Overlap 범위 = 0
→ Victims 배열 = empty
→ 데미지 로직 실행되지 않음

VFX는 보이지만 실제 피해 판정 영역이 존재하지 않는 상태였다.


2. 구조물 타입 필터링 문제

FirePillarActor는 피해 대상 필터링을 다음 조건으로 수행하고 있었다.

Pawn
또는
APotatoPlaceableStructure 기반 구조물

코드

if (APotatoPlaceableStructure* S = Cast<APotatoPlaceableStructure>(Other))
{
    return (S->StructureData && S->StructureData->bIsDestructible && S->CurrentHealth > 0.f);
}

하지만 실제 타겟은

BP_WareHouse_C_1

이 액터가

  • APotatoPlaceableStructure 기반이 아니거나
  • bIsDestructible 설정이 없거나
  • 체력 시스템이 다르면

피해 대상에서 제외된다.


3. DOT 기반 데미지 구조 문제

기존 FirePillarActor 구조는 다음과 같았다.

TickDamage()
 └ DotComponent.ApplyDot()

이 구조에서는

  • 불기둥 LifeTime
  • DOT Duration
  • DOT TickInterval

서로 다른 타이머로 동작한다.

예시

불기둥 LifeTime = 5s
DOT Duration = 1.2s
DOT TickInterval = 0.4s

이 경우

  • 불기둥이 사라졌는데 DOT가 남음
  • 불기둥이 있는데 DOT가 갱신되지 않음
  • 피해 타이밍이 불기둥과 어긋남

영역 스킬과 DOT 시스템이 구조적으로 분리된 상태였다.


해결

Radius 정상 값 적용

스킬 데이터 수정

Radius = 250 ~ 400

이제

SphereOverlap
→ Victims 배열 생성
→ ApplyDamage 실행

정상 동작한다.


구조물 타입 통일

FirePillar 피해 대상 구조물을 다음 기반 클래스로 통일하였다.

APotatoPlaceableStructure

이를 통해

  • 체력
  • 파괴 처리
  • 데미지 처리

모든 시스템을 공통 구조로 사용할 수 있게 했다.


DOT 구조 제거

DOT 컴포넌트를 제거하고 틱 데미지 방식으로 변경했다.

기존

TickDamage
 └ DotComponent.ApplyDot()

변경

TickDamage
 └ ApplyDamage(DPS * TickInterval)

코드

const float DamagePerTick = DotDps * TickInterval;

UGameplayStatics::ApplyDamage(
    Victim,
    DamagePerTick,
    InstigatorController,
    DamageCauser,
    UDamageType::StaticClass()
);

장점

  • 불기둥 지속시간과 피해 타이밍 일치
  • DOT 잔여 데미지 문제 제거
  • 구조 단순화
  • 디버깅 쉬움

첫 데미지 즉시 적용

기존

SetTimer(TickInterval)

수정

SetTimer(TickInterval, InitialDelay = 0)

Spawn → 즉시 첫 데미지

최종 FirePillar 구조

Skill Trigger
↓
FirePillarActor Spawn
↓
SphereOverlap Victim 탐색
↓
DamagePerTick 계산
↓
ApplyDamage
↓
LifeTime 종료
↓
Destroy

CPP-local Helper 난립 문제

문제

몬스터 AI와 스킬 시스템의 여러 cpp 파일 내부에

static helper functions

형태의 CPP-local helper 코드가 반복적으로 존재하고 있었다.

대표 함수

FindFirstCollisionPrimitive
GetClosestPoint2DOnTarget
ComputeApproachPoint2D
DistancePointToSegment2D
GetMonsterCapsuleRadiusSafe
GetPlayerPawnSafe
ScheduleTimerSafe

문제점

  1. 동일 코드가 여러 파일에 복사됨
  2. 버그 수정 시 여러 파일 수정 필요
  3. 파일 길이 증가
  4. 코드 재사용 어려움

특히

BTService_UpdateAttackTarget.cpp

에서는 AI 로직보다 helper 코드가 더 많은 상태였다.


해결

CPP-local helper들을 도메인별 Utils 모듈로 분리했다.

리팩터링 전

Monster
 ├ AI
 │  └ BTService_UpdateAttackTarget.cpp
 │      └ static helpers

리팩터링 후

Monster
 ├ Utils
 │  ├ PotatoTargetGeometry
 │  ├ PotatoMonsterRuntimeUtils
 │  ├ PotatoPlayerQueryUtils
 │  ├ PotatoAnimUtils
 │  └ PotatoMath2D

주요 Utils

Target Geometry Utils

역할

  • 충돌 기반 최근접점 계산
  • 접근 지점 계산

주요 함수

FindFirstCollisionPrimitive
GetClosestPoint2DOnTarget
ComputeApproachPoint2D

Monster Runtime Utils

역할

  • Capsule radius 조회
  • AnimInstance 접근
  • Timer 안전 스케줄링

주요 함수

GetMonsterCapsuleRadiusSafe
GetAnimInstanceSafe
DisableMovementSafe
ScheduleTimerSafe

Player Query Utils

역할

  • Player Pawn 조회
  • 공격 가능한 플레이어 판별

주요 함수

GetPlayerPawnSafe
IsValidAttackablePlayer

결과

파일 단위 Helper
→
시스템 단위 Utility

구조로 개선되었다.

효과

  • 코드 중복 감소
  • 유지보수성 향상
  • AI / Skill / Combat 공용 로직 재사용 가능

문제

몬스터가 NavMesh + MoveTo 기반으로 이동할 때 특정 구조물을 넘어가지 못하고 멈추는 문제가 발생했다.

NavMesh 상에서는 연결되어 있었지만 실제 이동은 실패했다.


분석

몬스터는 Character 기반이므로 실제 충돌은 CapsuleComponent가 담당한다.

이때

NavMesh 경로 존재
하지만 Capsule이 지형에 걸림

상황이 발생했다.

Capsule을 줄이면 이동은 가능했지만

피격 판정이 작아지는 문제

가 발생했다.


해결

이동 충돌과 피격 충돌을 분리하였다.

구조

역할컴포넌트
이동CapsuleComponent
피격Mesh / HitCapsule

Capsule은 이동 전용

CapsuleComp->SetCollisionResponseToChannel(ProjectileChannel, ECR_Ignore);

Mesh는 피격 전용

MeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
MeshComp->SetCollisionResponseToChannel(ProjectileChannel, ECR_Block);

히트스캔 공격 데미지 과다 적용 문제

문제

플레이어 히트스캔 공격 시 게임이 멈추는 현상이 발생했다.

콜스택

FireHitscan
→ ApplyDamage
→ TakeDamage
→ TryPlayHitReact
→ ScheduleTimerSafe

분석

히트스캔에서

LineTraceMulti

를 사용하고 있었기 때문에

  • PhysicsAsset 여러 본
  • Capsule
  • Mesh

가 동시에 히트되면서

같은 몬스터에게 여러 번 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()
    );
}

최종 정리

  1. 스킬 데이터 값은 실제 판정 로직에 직접 영향을 준다
  2. 영역 스킬은 DOT보다 TickDamage 구조가 안정적일 수 있다
  3. 이동 충돌과 피격 충돌은 분리하는 것이 좋다
  4. 히트스캔 MultiTrace에서는 Actor 중복 필터링이 필수다
  5. 파일 내부 Helper는 Utils 모듈로 승격하는 것이 유지보수에 유리하다

특히 스킬 시스템 디버깅에서는 다음 흐름을 단계적으로 확인하는 것이 중요하다.

Trigger
→ Cooldown
→ Spawn
→ Overlap
→ Target Filter
→ Damage Apply

이 흐름을 기준으로 확인하면 대부분의 스킬 버그를 빠르게 추적할 수 있다.

profile
개발합시다!

0개의 댓글