Sparta Unreal 부트캠프 131일차

정찬호·2026년 6월 11일

[TIL] 보스 전투 안정화와 SpecialAttack 트러블슈팅

Today I Learned | Unreal Engine 5 | StateTree | GAS | AI Perception | Collision | Boss Combat


오늘 작업 개요

오늘은 보스 전투 기능을 PR 가능한 상태로 정리하기 위해 여러 문제를 이어서 확인했습니다.

크게 보면 다음 흐름으로 작업했습니다.

1. 보스 일반 공격과 이동 제어 문제 점검
2. SpecialAttack 실행 실패 원인 추적
3. 보스 사망 상태가 StateTree / ABP에 반영되지 않는 문제 분석
4. 보스 스폰 위치 이상 문제 원인 확인
5. EnemyCombatComponent / DamageEffect 구조 확인
6. PR 메시지와 커밋 메시지 정리

오늘 해결한 문제도 있었지만, SpecialAttack은 아직 정상 동작하지 않습니다. 이 항목은 완료가 아니라 수정 중으로 남겨두어야 합니다.


보스 공격 이동 문제와 StateTree 책임 분리

보스 일반 공격은 이전 작업에서 정상 재생되게 만들었지만, 공격 도중 보스가 미끄러지듯 이동하는 문제가 있었습니다. 처음에는 ANS_EnemyMovementLock으로 공격 중 이동을 막으면 충분할 것이라고 생각했습니다.

하지만 실제로는 NotifyState가 정상 실행되어도 이동이 완전히 멈추지 않았습니다. StateTree의 MoveTo Task, EnemyAttack Task, ChaseLocation 갱신이 서로 영향을 주고 있었기 때문입니다.

제가 확인한 흐름은 다음과 같았습니다.

Attack State 진입
→ EnemyAttack Task가 공격 준비
→ MoveTo 또는 ChaseLocation 갱신이 계속 영향
→ 몽타주 재생 중에도 캐릭터가 이동
→ 공격 위치가 어긋나거나 공격이 끊김

이를 해결하기 위해 공격 상태에서 별도 MoveTo Task를 제거하고, 접근 이동과 공격 실행을 StateTreeTask_EnemyAttack 쪽으로 모으는 방향으로 정리했습니다. 공격 중 이동 잠금은 EnemyCombatComponent가 관리하고, 잠금 중에는 MaxWalkSpeed를 0으로 설정한 뒤 해제 시 원래 값으로 복구하도록 했습니다.

이번 과정에서 배운 점은 단순히 "이동을 막는 플래그"만으로는 충분하지 않다는 것입니다. StateTree에서 여러 Task가 동시에 이동을 제어하면, 어떤 Task가 마지막으로 이동 명령을 냈는지가 실제 결과를 결정합니다. 공격처럼 타이밍이 중요한 상태에서는 이동 책임을 한 곳으로 모아야 한다고 느꼈습니다.


SpecialAttack 실행 실패 추적

보스의 SpecialAttack은 오늘 가장 오래 붙잡고 있던 문제였습니다.

처음에는 bSpecialAttackable이 계속 true로 남아 StateTree가 SpecialAttack을 반복 선점하는 문제가 있었습니다. 이를 막기 위해 다음 조건들을 확인했습니다.

- 유효한 Target이 있는가
- 공격 토큰을 요청할 수 있는가
- 현재 Attack / SpecialAttack 패턴이 활성 상태가 아닌가
- SpecialAttack 패턴이 거리 조건과 Cooldown을 통과하는가
- SpecialAttack 재평가 락이 걸려 있지 않은가

EnemyCombatComponent에는 SpecialAttack 재평가 락을 추가했고, RetrieveEnemyTargetEvaluator에서는 bSpecialAttackable 계산 조건을 강화했습니다. 이로 인해 SpecialAttack이 Chase 입력까지 먹어버리는 문제는 완화되었습니다.

하지만 이후에도 SpecialAttack 자체가 정상 실행되지 않았습니다. 로그를 추가하면서 확인한 결과, 한 시점에는 StateTree의 EnemyPatternAttack Task에서 TargetActor 바인딩이 풀려 있었습니다. 이 때문에 Task는 실행되지만 실제 대상이 없어 GA 호출까지 이어지지 못했습니다.

바인딩을 복구한 뒤 SpecialAttack 실행 흐름이 일부 확인되었지만, 현재 기준으로는 다시 SpecialAttack이 정상 동작하지 않는 상황입니다.

현재 상태는 다음과 같이 정리합니다.

SpecialAttack 상태: 수정 중

확인한 것:
- StateTree에서 SpecialAttack 상태 진입 자체는 발생할 수 있음
- 이전에 TargetActor 바인딩 누락 문제가 있었음
- bSpecialAttackable 재평가 락은 Chase 선점 문제를 줄이는 데 효과가 있었음

아직 해결하지 못한 것:
- SpecialAttack GA가 안정적으로 실행되지 않음
- 투사체 발사가 정상적으로 보장되지 않음
- 1회 실행 이후 재사용 조건이 안정적으로 검증되지 않음
- 발사 시점의 플레이어 위치를 향해 투사체를 날리는 보정이 필요함

오늘의 중요한 판단은 SpecialAttack을 "완료된 기능"으로 보지 않는 것입니다. PR이나 작업 공유 시에도 이 항목은 반드시 수정 중으로 표시해야 합니다.


보스 사망 상태가 반영되지 않는 문제

보스에게 데미지를 주어 사망시키면 이벤트 Payload는 정상적으로 발행되지만, StateTree와 ABP는 Dead 상태로 넘어가지 않는 문제가 있었습니다.

처음에는 사망 이벤트 자체가 호출되지 않는다고 의심했지만, Payload가 정상 동작하는 것을 보고 방향을 바꿨습니다. 문제는 이벤트 발행이 아니라 상태 태그와 애니메이션 / StateTree가 바라보는 기준의 불일치였습니다.

현재 구조에서 보스는 ARetrieveEnemyCharacter를 상속합니다. 그리고 EnemyCharacter에 등록된 태그 이벤트와 ABP 캐시는 대부분 State.Enemy.* 기준으로 되어 있습니다.

State.Enemy.Dead
State.Enemy.Attack
State.Enemy.SpecialAttack
State.Enemy.Hit
State.Enemy.Groggy

그런데 보스 사망 처리에서는 State.Boss.Dead를 부여하고 있었습니다.

OwnedASC->AddLooseGameplayTag(RetrieveGameplayTags::State_Boss_Dead);

이 경우 ASC에는 보스 사망 태그가 들어가지만, 기존 ABP 캐시와 StateTree가 보는 State.Enemy.Dead는 변하지 않습니다. 그래서 데미지로 사망했을 때 죽은 것으로 처리되지 않는 것처럼 보였습니다.

오늘 얻은 결론은 다음과 같습니다.

전투 상태 / 애니메이션 상태:
State.Enemy.* 로 통일

보스 전용 의미:
Monster.Type.Boss
BossStatsRow
BossPhaseComponent
GameplayEvent.Boss.Die
Channel.Quest.GuardianDefeated
Channel.Game.QueenDefeated

즉 현재 구조에서는 State.Boss.*를 억지로 살리는 것보다 State.Enemy.*를 공통 상태로 쓰는 편이 더 안전하다고 판단했습니다.

또한 ARetrieveBossCharacter::HandleDeathStarted()는 제거하면 안 된다고 판단했습니다. 보스는 일반 Enemy와 달리 GameplayEvent.Boss.Die, 가디언 처치 메시지, 여왕 처치 메시지를 보내야 하기 때문입니다.

다만 Super::HandleDeathStarted()를 그대로 호출하면 GameplayEvent.Enemy.Die까지 발행될 수 있어 위험합니다. 그래서 보스에서는 공통 이동 정지만 직접 호출하고, 나머지 보스 전용 후처리는 직접 수행하는 구조가 적절하다고 정리했습니다.

ARetrieveCombatCharacter::HandleDeathStarted(OwningActor);

이번 문제를 통해 상속 구조에서 "공통 처리"와 "전용 이벤트"를 분리해야 한다는 점을 다시 배웠습니다. Super 호출이 항상 정답은 아니며, 부모 함수가 어떤 부수 효과를 갖는지 먼저 확인해야 합니다.


사망 애니메이션과 Ragdoll 충돌 가능성

사망 상태를 확인하는 과정에서 애니메이션 시퀀스와 Ragdoll이 동시에 실행되면 사망 애니메이션이 씹힐 수 있다는 점도 점검했습니다.

Ragdoll이 시작되면 SkeletalMesh의 포즈 제어권이 물리 시뮬레이션으로 넘어갑니다. 따라서 사망 몽타주나 시퀀스를 재생하려 해도 물리가 먼저 메쉬를 제어하면 애니메이션이 보이지 않거나 바로 끊긴 것처럼 보일 수 있습니다.

정리하면 다음과 같습니다.

사망 애니메이션을 보여주고 싶다
→ 즉시 Ragdoll을 켜면 안 됨

사망 애니메이션 후 Ragdoll을 원한다
→ 몽타주 종료 Notify 또는 Timer 이후 Ragdoll 시작

처음부터 물리 사망을 원한다
→ 사망 애니메이션 재생을 기대하면 안 됨

보스 사망 연출은 몽타주나 시퀀스를 먼저 보여준 뒤, 필요한 경우 후속 처리로 Ragdoll 또는 Destroy를 실행하는 방식이 더 적절하다고 판단했습니다.


보스 스폰 위치 이상과 Collision 문제

보스를 Spawner로 생성하거나 BP를 직접 배치했을 때 땅 밑으로 박히거나 하늘 위로 튀는 문제가 있었습니다.

처음에는 Spawner가 SpawnPoint 위치를 그대로 Actor Location으로 사용하고 있어서, 보스의 큰 Capsule Half Height가 보정되지 않는 문제를 의심했습니다.

SpawnPoint.Z를 Actor Location으로 사용
→ Character Actor Location은 Capsule 중심
→ 큰 보스 Capsule은 바닥과 겹칠 수 있음

하지만 이후 확인 결과 실제 원인은 PCG와 EnemyCharacter의 Collision Channel 문제였습니다. PCG 생성물과 Enemy Capsule이 스폰 순간 충돌하면서 디페너트레이션 보정이 발생했고, 그 결과 보스가 땅 밑이나 공중으로 밀려나는 현상이 발생한 것으로 정리했습니다.

이번 문제를 통해 스폰 위치 이상은 단순히 Transform 문제만 볼 것이 아니라 다음을 함께 확인해야 한다는 것을 배웠습니다.

- Spawn Transform
- Capsule Half Height
- SpawnCollisionHandlingMethod
- PCG / 지형 / 장식물 Collision
- Pawn Collision Profile

DamageEffect 구조 확인

Detail 패널에서 Combat 드롭다운이 여러 개 보여 DamageEffect가 여러 개 있는지 확인했습니다.

코드 기준으로는 EnemyCombatComponentDamageEffectClass가 하나만 존재했습니다.

UPROPERTY(EditDefaultsOnly, Category = "Retrieve|Combat|Hitbox")
TSubclassOf<UGameplayEffect> DamageEffectClass;

EnemyCharacterBossCharacter에는 별도의 DamageEffect 필드가 없었습니다. Detail 패널에서 여러 Combat 항목이 보인 것은 실제 DamageEffect가 여러 개라서가 아니라 Category가 나뉘어 있기 때문이었습니다.

Retrieve|Combat
Retrieve|Combat|BasicAttack
Retrieve|Combat|Hitbox
Retrieve|Combat|SpecialAttack

현재 구조에서는 일반 몬스터와 보스가 같은 EnemyCombatComponent 구조를 사용합니다. 패턴별로 다른 DamageEffect를 주려면 FMonsterPatternRow에 DamageEffect를 추가하거나, EffectTag를 기반으로 별도 매핑하는 구조가 필요합니다.


피격 시 플레이어 인식 방향

플레이어가 적을 공격했을 때, 시야에 없어도 적이 플레이어를 인식하도록 만들기 위해 AI Perception의 Damage Sense 사용 가능성을 검토했습니다.

Unreal에는 "HitSense"라는 기본 Sense는 없고, 이 용도에는 UAISense_Damage를 사용합니다.

UAISense_Damage::ReportDamageEvent(
    GetWorld(),
    DamagedActor,
    InstigatorActor,
    DamageAmount,
    InstigatorActor->GetActorLocation(),
    DamagedActor->GetActorLocation()
);

GAS로 데미지를 적용한다고 해서 AI Perception Damage Sense가 자동으로 발동하는 것은 아닙니다. 데미지를 적용하는 GA 또는 실제 Health 감소가 확정되는 위치에서 직접 ReportDamageEvent()를 호출해야 합니다.

현재 프로젝트에서는 플레이어 공격 GA에서 데미지 적용 직후 보고하는 방식이 가장 명확해 보입니다.


Git / PR 정리 과정

PR을 급하게 준비하면서 커밋 메시지와 PR 메시지를 작성했습니다. 소스와 에셋은 분리 커밋하는 방향으로 정리했습니다.

또 Fork에서 git fetch --prune origin 이후 무한 로딩이 발생했습니다.

로그상 fetch 자체보다는 자동 git gc 단계에서 실패한 것으로 보였습니다.

Auto packing the repository for optimum performance.
See "git help gc" for manual housekeeping.
Nothing new to pack.
error: task 'gc' failed

처음에는 Fork 종료, 남은 git 프로세스 종료, 로컬 자동 gc 비활성화 같은 임시 대응을 검토했습니다.

이후 유진님이 알려주신 명령으로 수동 gc를 실행해 문제를 해결했습니다.

git gc --aggressive --prune=now

이 문제를 통해 GUI Git 클라이언트에서 gc 단계가 실패하면 fetch/pull 자체가 멈춘 것처럼 보일 수 있다는 점을 배웠습니다. 이때는 Fork의 메시지를 그대로 fetch 문제로만 보지 말고, git 내부 정리 작업이 실패했는지 확인해야 합니다.

_LFS_CACHE/가 미추적 파일로 보였기 때문에 PR에 포함되지 않도록 주의해야 합니다.


오늘 해결한 문제

문제해결 / 정리
보스 공격 중 미끄러짐공격 중 이동 락과 MaxWalkSpeed = 0 복구 방식으로 1차 해결했습니다.
MoveTo Task와 EnemyAttack Task 충돌공격 상태의 이동 책임을 EnemyAttack Task 쪽으로 모으는 방향으로 정리했습니다.
보스 스폰 위치 이상Spawner 단독 문제가 아니라 PCG / Enemy Collision Channel 문제로 확인했습니다.
DamageEffect가 여러 개처럼 보이는 문제실제 DamageEffectClass는 EnemyCombatComponent에 1개만 존재함을 확인했습니다.
보스 사망 Payload는 나가지만 Dead 상태가 안 먹는 문제State.Boss.DeadState.Enemy.* 기반 구조 불일치가 원인임을 확인했습니다.
Fork fetch 무한 로딩git gc --aggressive --prune=now 수동 실행으로 해결했습니다.

아직 수정 중인 문제

문제현재 상태
SpecialAttack 실행 실패수정 중입니다. 현재 정상 실행되지 않습니다.
SpecialAttack 1회 이후 재사용 조건수정 중입니다. Cooldown / 재평가 락 / StateTree 조건 재검증이 필요합니다.
투사체 발사 방향수정이 필요합니다. 발사 시점의 플레이어 위치를 향하도록 보정해야 합니다.
보스 사망 Dead 상태 최종 처리수정이 필요합니다. State.Enemy.Dead 통일 방향으로 정리해야 합니다.
Damage Sense 적용검토는 완료했고, 구현 여부 결정이 필요합니다.

오늘의 학습 요약

StateTree에서 이동과 공격이 동시에 작동하면 몽타주 타이밍은 쉽게 깨집니다.
보스가 Enemy를 상속한다면 전투 상태 태그도 Enemy 공통 태그를 쓰는 편이 단순합니다.
보스 전용성은 상태 태그보다 BossStats, BossPhaseComponent, Boss 전용 GameplayEvent로 표현하는 것이 안전합니다.
SpecialAttack은 아직 완료된 기능이 아니며, 현재는 실행 실패 원인을 계속 추적해야 하는 수정 중 상태입니다.
스폰 위치 이상은 Transform뿐 아니라 Collision Channel과 디페너트레이션까지 함께 봐야 합니다.


프로젝트: Retrieve / Enemy & Boss Combat

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

0개의 댓글