Sparta Unreal 부트캠프 130일차

정찬호·2026년 6월 10일

[TIL] 보스/몬스터 공격 이동 버그 트러블슈팅

Today I Learned | Unreal Engine 5 | StateTree | GAS | AI Movement | AnimNotifyState


보스/일반 몬스터 공격 구조 점검

오늘은 보스 몬스터와 일반 몬스터의 공격 상태에서 발생하던 이동, 충돌, StateTree 문제를 집중적으로 디버깅했다.

처음에는 보스 공격이 정상적으로 실행되지 않았다. 이후 공격 몽타주가 재생되도록 고쳤지만, 공격 중 플레이어 쪽으로 미끄러지듯 이동하는 문제가 남았다. 일반 몬스터 쪽에서는 Enemy Attack! 로그 직후 End Attack!이 바로 출력되거나, 플레이어 주변을 빙빙 돌기만 하는 문제가 발생했다.

핵심 원인은 Attack State 안에서 StateTreeMoveToTaskStateTreeTask_EnemyAttack이 동시에 이동을 제어하고 있었던 점이었다.


StateTree MoveTo와 Attack Task 충돌

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

Attack State
├─ StateTreeTask_EnemyAttack
└─ StateTreeMoveToTask

StateTreeTask_EnemyAttack 안에서 공격 전 접근 이동을 처리하도록 수정했는데, 에셋 쪽 Attack State에 기존 MoveTo Task가 남아 있었다. 이 때문에 공격 Task가 StopMovement()를 호출해도 MoveTo Task가 다시 이동을 시도했고, 공격 중 미끄러짐 또는 공격 즉시 종료가 발생했다.

해결은 ST_Monster_NormalST_Monster_BossAttack State에서 MoveTo Task를 제거하는 것이었다.

Attack State
└─ StateTreeTask_EnemyAttack

이후 일반 몬스터의 공격 즉시 종료 문제는 정상화되었다.

이 과정에서 단순히 MoveTo Task를 제거하는 것만으로는 부족했다. Attack State에 이동 Task가 사라지면 공격 가능 거리까지 접근하는 로직도 같이 사라지기 때문이다. 그래서 StateTreeTask_EnemyAttack 내부에 접근 이동 로직을 직접 넣었다.

EnemyAttack Task
→ 공격 전: ChaseLocation으로 MoveToLocation 요청
→ 공격 시작 직전: StopMovement / AbortMove / StopMovementImmediately
→ 공격 시작 후: 이동 요청 중단, 공격 종료 감시

즉 이동 책임을 다음처럼 옮긴 셈이다.

이전 구조변경 후 구조
StateTree MoveTo Task가 접근 담당EnemyAttack Task가 접근 담당
EnemyAttack Task는 공격 실행만 담당EnemyAttack Task가 접근/정지/공격 실행을 순차적으로 담당
MoveTo와 공격 Task가 병렬 실행공격 Task 안에서 이동과 공격 순서를 직접 제어

이 변경을 통해 StateTree의 병렬 Task 충돌을 줄이고, 공격 시작 시점에 이동을 확실히 끊을 수 있게 되었다.


공격 중 미끄러짐 원인 추적

MoveTo Task를 제거한 뒤에도 보스가 공격 중 미끄러지는 문제가 남아 있었다.

처음에는 ChaseLocation이 공격 중 계속 갱신되는 것이 원인이라고 판단했다. 그래서 EnemyCombatComponent에 이동 락 플래그를 추가하고, ANS_EnemyMovementLock을 만들어 몽타주 구간에서 ChaseLocation 갱신을 막았다.

const bool bFreezeChaseLocation =
	InstanceData.CachedCombatComponent.IsValid()
	&& InstanceData.CachedCombatComponent->IsMovementLockedByAttack();

하지만 로그를 확인해 보니 ANS_EnemyMovementLock은 정상 실행되고 있었다.

[EnemyMovementLock] Begin Owner=BP_Guardian_Wind_C_4 Combat=EnemyCombatComponent
[EnemyMovementLock] End Owner=BP_Guardian_Wind_C_4 Combat=EnemyCombatComponent

즉 NotifyState가 실행되지 않는 문제가 아니라, 이동 락이 실제 이동을 막을 만큼 강하지 않은 것이 문제였다.

최종적으로 SetMovementLockedByAttack(true)에서 기존 MaxWalkSpeed를 저장하고, MaxWalkSpeed = 0으로 만든 뒤 해제 시 원래 값으로 복구하도록 수정했다.

if (bLocked)
{
	MovementLockOriginalMaxWalkSpeed = MoveComp->MaxWalkSpeed;
	MoveComp->StopMovementImmediately();
	MoveComp->MaxWalkSpeed = 0.f;
}
else
{
	MoveComp->MaxWalkSpeed = MovementLockOriginalMaxWalkSpeed;
	MovementLockOriginalMaxWalkSpeed = -1.f;
}

이후 공격 중 미끄러짐 문제가 해결되었다.


AnimNotifyState의 역할 정리

처음에는 ANS_EnemyMovementLock에서 직접 AIController::StopMovement()를 호출하도록 만들었다. 하지만 이 방식은 StateTree의 이동 Task와 충돌하면서 공격이 바로 종료되는 원인이 될 수 있었다.

그래서 NotifyState는 이동을 직접 멈추지 않고, 이동 락 플래그만 켜고 끄는 역할로 정리했다.

NotifyBegin
→ SetMovementLockedByAttack(true)

NotifyEnd
→ SetMovementLockedByAttack(false)

실제 이동 정지는 EnemyCombatComponentStateTreeTask_EnemyAttack 쪽에서 처리하도록 역할을 분리했다.


보스 충돌 튕김 문제

보스가 플레이어와 충돌할 때 튕겨 날아가는 문제도 확인했다. 원인은 보스/몬스터 캡슐 Collision Object Type이 Pawn이 아닌 Enemy 채널로 강제되는 것과 관련이 있었다.

팀원분이 BP_Enemy_Base의 캡슐 프리셋을 표준 Pawn으로 정규화했고, C++ 생성자에서 Enemy 채널을 강제하던 코드는 주석 처리했다.

// GetCapsuleComponent()->SetCollisionObjectType(ECC_GameTraceChannel1);
// GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Overlap);

이후 보스 튕김 문제는 해결되었다.

이 문제는 단순한 충돌 프리셋 문제가 아니라, 루트모션/모션 워핑과도 연결되어 있었다. 루트모션 공격에서 몬스터와 플레이어 캡슐이 강하게 밀어내면 디페너트레이션이 비정상적으로 발생할 수 있다. 해결 방법은 크게 세 가지가 있었다.

1. 서로의 캡슐 크기를 줄인다.
2. 루트모션 중 Pawn끼리 Overlap 처리한다.
3. Motion Warping으로 접근 위치를 보정한다.

이번 프로젝트에서는 이미 공격 몽타주 쪽에 모션 워핑 세팅이 준비되어 있었기 때문에, 캡슐을 표준 Pawn 충돌 기준으로 유지하고 모션 워핑이 정상 작동하도록 맞추는 방향이 가장 적절했다.

처음에는 Enemy 전용 채널을 사용해 타겟 탐색과 충돌을 분리하려고 했지만, 이 방식은 모션 워핑 기반 접근 보정의 전제와 충돌할 수 있었다. 결국 보스/일반 몬스터의 캡슐은 Pawn으로 유지하고, 공격 접근 문제는 모션 워핑과 거리 데이터로 해결하는 쪽이 맞다고 판단했다.


거리 조절 기준 정리

거리 조절도 몬스터 크기와 역할에 따라 기준을 나누어야 했다.

Boss가 너무 가까이서 공격함
→ 공격 토큰을 가진 상태의 위치 문제
→ OrbitInnerRadius 조정

Wolf들이 너무 가까이 몰림
→ 비공격 대기/Strafe 위치 문제
→ OrbitOuterRadius 조정

Strafe 중 플레이어를 뚫고 지나가려는 문제는 슬롯 이동 폭과 거리 값이 함께 영향을 준다. 빠른 완화책으로는 OrbitOuterRadius, StrafeOffRange, MoveAcceptableRadius 조정과 ShiftOrbitSlotMaxSlotShiftSteps 감소가 있다.


시행착오 정리

시도결과
Attack State의 MoveTo Task 유지EnemyAttack Task 내부 이동과 충돌
Attack State에서 MoveTo Task만 제거공격 가능 거리까지 접근하지 못하는 문제가 생김
접근 이동을 EnemyAttack Task 내부로 이전접근 후 공격 실행 순서를 한 Task에서 제어 가능
ANS_EnemyMovementLock에서 StopMovement() 호출공격 즉시 종료 가능성 발생
ChaseLocation 갱신만 중단목적지 갱신은 막았지만 실제 이동은 남음
StopMovementImmediately()만 호출잔여 이동이 완전히 사라지지 않음
MaxWalkSpeed = 0 후 복구공격 중 미끄러짐 해결
캡슐 ObjectType을 Enemy 채널로 강제모션 워핑 기반 충돌 해결 방향과 충돌 가능
캡슐을 표준 Pawn으로 유지보스 튕김 문제 해결

오늘의 설계 판단 변화

처음에는 StateTree의 MoveTo Task를 유지하고, 공격 중에만 ChaseLocation 갱신을 막으면 충분할 것이라고 생각했다. 하지만 실제로는 StateTree의 MoveTo Task와 공격 Task가 병렬로 살아 있기 때문에, 공격 Task 내부 상태를 MoveTo Task가 알 수 없었다.

오늘의 결론은 다음과 같다.

StateTree는 큰 상태 전환을 담당한다.
공격 중 세부 이동 제어는 공격 Task가 담당한다.
몽타주 구간별 이동 금지는 AnimNotifyState가 신호만 전달한다.
실제 이동 차단은 EnemyCombatComponent가 수행한다.

역할을 분리하니 문제를 더 명확하게 볼 수 있었다.

StateTree
→ Combat / Attack / Strafe / Chase 같은 큰 흐름

EnemyAttack Task
→ 접근, 이동 정지, 공격 실행, 공격 종료 감시

ANS_EnemyMovementLock
→ 몽타주 구간에서 이동 락 신호 전달

EnemyCombatComponent
→ 이동 락 플래그와 MaxWalkSpeed 제어

이번 디버깅을 통해 단순히 “어디선가 움직인다”를 쫓는 것이 아니라, 누가 이동 명령을 내리고, 누가 속도를 유지하고, 누가 상태를 종료시키는지 책임을 나누어 추적해야 한다는 점을 배웠다.


오늘의 요약

StateTree에서 이동 Task와 공격 Task가 동시에 이동을 제어하면 충돌이 발생한다.
공격 중 이동 제어는 StateTree MoveTo보다 공격 Task와 몽타주 NotifyState 중심으로 관리하는 편이 안정적이다.
AnimNotifyState는 직접 이동 명령을 끊기보다 “이 구간은 이동 금지”라는 상태를 전달하는 역할이 적합하다.
실제 이동을 막으려면 ChaseLocation 갱신 중단만으로는 부족하고, CharacterMovement의 속도와 MaxWalkSpeed까지 제어해야 한다.
충돌 문제는 캡슐 Object Type과 Motion Warping 전제 조건을 함께 봐야 한다.
문제가 복잡할수록 StateTree, Task, Component, Montage Notify의 책임을 분리해서 봐야 원인을 좁힐 수 있다.


Retrieve 프로젝트 AI/Enemy 전투 디버깅 기록

profile
게임 개발 지망생입니다.

0개의 댓글