Make Unreal REAL.
article thumbnail

Behavior Tree와 Task, Service를 이용해 AI 몬스터를 구현하고 끝났다 싶었는데, 어느 순간 보니 캐릭터에게 데미지 전달이 안 되고 있었다.

  • 전에 테스트 했었을 때는 됐었는데 갑자기 안 되기 시작해서, 처음에는 BP가 깨져서 생기는 등의 버그인 줄 알았다.

 

내가 AI를 공격할 때 (좌) / AI가 나를 공격할 때 (우)

 

내가 AI에게 10의 데미지를 준 것은 뜨지만, AI가 나를 때린 것은 뜨지 않는다.

 

 

캐릭터가 데미지를 입는 부분은 로그 출력밖에 하지 않기 때문에 이 곳의 문제는 아니다.

  • 그럼 몬스터의 공격 부분에서 캐릭터의 TakeDamage() 함수를 호출하지 않았다는 뜻이다.

 

 

몬스터가 공격할 때 호출되는 Attack() 함수이다.

  • AttackSection 섹션의 공격 애니메이션을 재생하고, Attack 애님 노티파이를 통해 실제 공격 로직을 수행한다.

 

하지만, AI의 공격 애니메이션이 정상적으로 재생되는 걸로 봐선 이 곳의 문제도 아니다.

 

 

그럼 다음은 Attack 애님 노티파이가 발생하지 않는 것인지 확인했다.

 

 

내가 AI를 때린 Warning 로그 이후, AI가 다가와서 나를 때리면서 Attack 애님 노티파이의 Error 로그가 정상적으로 출력됐다..

 

 

그럼 애님 노티파이의 OnAttack 델리게이트가 발동하지 않았다는 것인데 이럴 수가 있나..?

 

몬스터의 PostInitializeComponents() 함수에서 Anim Instance의 OnAttack 델리게이트에 OnAttackTarget() 함수를 등록하고, Attack 애님 노티파이에 이 델리게이트가 발동해야 한다.

 

 

하지만 OnAttackTarget() 함수에서 문제가 발생했다면 ARCHECK()에서 로그가 찍혀야 하는데, 그렇지 않는다는 것은 이 함수 자체가 호출되지 않았다는 것이고, 결국 OnAttack 델리게이트의 발동에 문제가 있었다는 뜻이다.

 

대체 이게 무슨 문제인지 감이 안 잡혀 테스트를 좀 더 해보았고, 어떨 때는 데미지가 찍히다가, 또 어떨 때는 찍히지 않는 것을 발견했다.

 

내가 AI를 때린 로그 (좌) / AI가 나를 때린 로그 (우)

 

그러다 위 2개 경우에서 나는 차이점을 발견했다.

  • 내가 AI를 1번 때린 이후부터 내게 데미지를 입히지 않는다는 것이다.

 

문제가 생기는 경우를 알고 나니 더 당황스러웠다..

  • AI가 한 대 맞으면 겁이라도 먹어서 내게 데미지를 입히지 않는다는 것인가..

 

그래서 AI가 피격당할 때 수행하는 로직을 살펴보기로 했다.

 

 

자신을 때린 플레이어를 추격하기 위해 블랙보드에 타겟을 설정하고, 상태를 Damaged로 변경한다.

  • 상태를 변경하는 SetState() 함수에서도 특이점은 없다.

 

그래서 마지막으로 Behavior Tree를 살펴봤다.

 

Damaged 상태가 되면, Damaged 상태에 해당하는 애니메이션을 가져와 재생하고 상태를 Chase로 변경한다.

  • Play Anim Task는 기존 Play Animation Task를 상속해 만들었고, 상태에 해당하는 애니메이션을 Data Asset에서 가져와 Super::ExecuteTask() 함수를 통해 재생한다.

 

 

혹시 Damaged 애니메이션 재생 후, Set State (Chase) 부분에서 상태가 변경되면서 Abort로 인해 애니메이션 재생이 도중에 끊기는 건가 싶기도 했지만, Blocking 방식으로 재생되므로 재생이 끝난 후 상태가 바뀌게 된다.

 

그럼 마지막으로 의심해 볼 것은 Play Anim Task 자체이다.

  • 노드 연결을 끊어 Damaged 애니메이션을 재생하지 않도록 한 후 실행해보았다.

 

 

된다 ! !

 

내가 AI를 때린 후에도, AI가 내게 데미지를 입힌다.

 

 

그럼 원인을 알았으니 이제 분석해보자.

 

Play Anim Task는 기존의 Play Animation Task를 상속받아, 재생할 애니메이션만 상태에 따라 설정해 재생하는 Task다.

 

 

부모 클래스인 Play Animation Task를 살펴보자.

 

Animation Mode를 저장해뒀다가, 애니메이션을 Single 모드로 재생한 후 복원하는 부분이 눈에 띈다.

 

 

SetAnimationMode() 함수가 무엇을 하는지 살펴봤더니, 왠지 Anim Instance를 초기화할 것 같은 이름의 ClearAnimScriptInstance()와 InitializeAnimScriptInstance()라는 함수가 눈에 띈다.

 

 

결국 SetAnimationMode() 함수로 Animation Mode가 바뀔 때, Anim Instance를 정리해 Garbage collecting 해버린다는 것을 알았다..

 

그래서 기존에 Anim Instance의 OnAttack 델리게이트에 등록되어 있던 공격 로직 함수도 초기화되어 사라져버린 것이다.

 

 

정리하면 다음과 같다.

Anim Instance의 델리게이트가 발동하지 않았던 원인
1. Behavior Tree에서 Play Animation Task로 애니메이션을 재생한다.
2. Animation Mode가 Single로 바뀌면서 기존 Anim Instance가 정리된다.
3. 재생이 끝난 후 다시 Blueprint 모드로 바뀌지만, Anim Instance는 새로 생성된 상태다.
4. 그래서 이미 정리되버린 Anim Instance의 델리게이트에 등록되어 있던 함수들은 실행될 수 없다.

 

애니메이션 BP 모드에서 SkeletalMeshComponent::Play Animation() 함수를 실행하면 Anim Instance가 초기화된다는 사실은 꿈에도 몰랐고, 아마 다른 사람들도 그럴 수 있을 것이라고 생각했다.

 

그래서 언리얼 엔진 Github 레포지토리에 PR을 넣기 위해 코드를 분석해봤다.

 

애니메이션 모드를 바꾸는 이 SetAnimationMode() 함수에서 Anim Instance가 초기화되지 않도록 하는 옵션을 제공하도록 하면 된다.

 

 

ClearAnimScriptInstance() 함수는 아까 살펴봤고, InitializeAnimScriptInstance() 함수를 살펴본다.

 

전달되는 bForceReinit 인자에 따라 새로운 데이터로 초기화할지, 기존 Anim Instance에서 값을 가져올지 결정하는 것 같다.

 

 

그럼 이제 엔진 코드를 수정해본다.

 

우선 Play Animation Task에서 현재 AnimationMode가 BP이면 Anim Instance를 초기화하지 않도록 전달하는 인자를 만든다.

 

 

선언부에 인자를 추가하고 전달 받은 인자를 SetAnimationMode() 함수에 포워딩해준다.

 

 

SetAnimationMode() 함수의 선언부에서도 인자를 추가해준다.

  • 최대한 언리얼 표준 코딩 규약을 준수해 주석과 변수명을 작성했다.
  • 함수를 호출하는 다른 부분에서 오류가 나지 않도록 Default parameter를 사용했다.

 

 

분석한 내용을 바탕으로, 전달 받은 인자에 따라 기존 Anim Instance를 초기화하지 않도록 수정했다.

 

 

직접 엔진을 빌드해 테스트 해보려 했지만, i7-13700K CPU로도 거의 1시간 가량 걸렸고 그 마저도 알 수 없는 문제들이 너무 많이 발생해 실제 확인해보지는 못 했다.

 

테스트 해보지 못 했고 혹시 내가 잘못 생각한 것일 수도 있으니 Draft PR로 열었다.

 

 

문제가 명확하지 않다는 답변을 받아서 이 문제가 발생하는 간단한 코드를 첨부하여 설명했고, 다음과 같은 피드백을 받았다.

  • 델리게이트 등록을 Anim Instance의 BeginPlay()나 InitializeAnimation() 함수에서 할 수 있지만, 종속성이 반전되므로 좋은 방법은 아니다.
  • Play Animation Task 대신, 몽타주를 재생하는 별도 Task를 만들어 사용해도 된다.

 

 

나보다는 훨신 실력자가 해주신 조언이니 바로 수긍하기로 했다.

 

그래서 몬스터 AI의 피격과 죽음 애니메이션은 Github PR과 단톡방 조언을 토대로 BP와 Single 모드를 혼합해 쓰지 않고, BP에서 모두 처리하도록 수정했다.

  • Play Animation Task 대신 AnimationEnd 애님 노티파이가 발생할 때까지 대기하는 Wait Animation End Task를 만들어 대체했다.

 

이번 과정을 통해 남에게 무조건 물어보는 것보다 스스로 고민해보면서 원인을 찾다보면 더 깊이 있게 이해할 수 있는 것들이 많다는 것을 깨달았다.

  • 하지만 남의 도움도 필요할 땐 받아야 하고, 더 좋은 의견과 피드백을 통해 성장할 수 있다는 것도 알게 되었다.

'문제 해결' 카테고리의 다른 글

UWidgetComponent의 위젯 생성 시점  (0) 2023.03.16
Decorator 실행 흐름이 갱신되지 않는 문제  (0) 2023.02.24
알고리즘 시간 초과 해결  (0) 2023.02.18
문제의 축소  (0) 2023.02.03
profile

Make Unreal REAL.

@diesuki4

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그